@brandon_m_behring/book-scaffold-astro 3.1.0 → 3.3.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/LATEX_TO_MDX_MAPPING.md +130 -0
- package/components/ChapterHeader.astro +31 -32
- package/dist/index.d.ts +70 -123
- package/dist/index.mjs +336 -129
- package/dist/schemas.d.ts +2 -1
- package/dist/schemas.mjs +161 -50
- package/package.json +3 -2
- package/pages/print.astro +9 -1
- package/src/lib/freshness.ts +19 -4
- package/styles/chapter.css +23 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# LaTeX → MDX component mapping
|
|
2
|
+
|
|
3
|
+
A consumer-facing reference for converting LaTeX book sources into MDX that consumes `@brandon_m_behring/book-scaffold-astro` components.
|
|
4
|
+
|
|
5
|
+
The scaffold ships **38 components**. Without this map, a `.tex → .mdx` conversion ends up rediscovering them by grep — and frequently rebuilding duplicates. This doc is the canonical "if your LaTeX source has `\begin{<env>}`, here's the component" reference.
|
|
6
|
+
|
|
7
|
+
> Pair with [PACKAGE_DESIGN.md](./PACKAGE_DESIGN.md) §17 for the broader migration story and the `defineMdxComponents` helper for consumer-shipped extensions.
|
|
8
|
+
|
|
9
|
+
## Components shipped by the scaffold
|
|
10
|
+
|
|
11
|
+
| LaTeX construct | MDX component | Import path | Signature | Notes |
|
|
12
|
+
|---|---|---|---|---|
|
|
13
|
+
| `\begin{tcolorbox}[narrativebox]` | `SkillBox` | `…/components/SkillBox.astro` | `title: string` | Recipe / how-to box |
|
|
14
|
+
| `\begin{tcolorbox}[conceptbox]` | `ConceptBox` | `…/components/ConceptBox.astro` | `term: string` | Single-term definition |
|
|
15
|
+
| `\begin{tcolorbox}[insightbox]` | `InsightBox` | `…/components/InsightBox.astro` | `title?: string` | Non-obvious observation |
|
|
16
|
+
| `\begin{keyconcept}` | `KeyIdea` | `…/components/KeyIdea.astro` | — | Crystallized takeaway |
|
|
17
|
+
| `\begin{warnbox}` / `\warningmargin{}` | `WarnBox` | `…/components/WarnBox.astro` | `title?: string` | Caveats / failure modes |
|
|
18
|
+
| `\begin{notebox}` | `NoteBox` | `…/components/NoteBox.astro` | `title?: string` | Chapter overviews |
|
|
19
|
+
| `\begin{paperbox}` | `PaperBox` | `…/components/PaperBox.astro` | `title?: string` | Paper restatement |
|
|
20
|
+
| `\begin{counterbox}` | `CounterBox` | `…/components/CounterBox.astro` | `title?: string` | Counter-evidence |
|
|
21
|
+
| `\begin{examplebox}` | `ExampleBox` | `…/components/ExampleBox.astro` | `title?: string` | Extended walkthrough |
|
|
22
|
+
| `\begin{openquestion}` | `OpenQuestion` | `…/components/OpenQuestion.astro` | `title?: string` | Research questions |
|
|
23
|
+
| `\begin{trythis}` | `TryThis` | `…/components/TryThis.astro` | `title?: string` | Practice exercise |
|
|
24
|
+
| `\begin{tipbox}` | `TipBox` | `…/components/TipBox.astro` | `title?: string` | Pro tips / shortcuts |
|
|
25
|
+
| `\begin{dynconnect}` | `DynConnect` | `…/components/DynConnect.astro` | `title?: string` | Cross-domain connection |
|
|
26
|
+
| `\begin{theorem}` / `\begin{proposition}` / `\begin{lemma}` / `\begin{corollary}` / `\begin{definition}` / `\begin{remark}` / `\begin{proof}` | `Theorem` | `…/components/Theorem.astro` | `kind, n?, name?, id?` | amsthm family — single component dispatches via `kind` prop |
|
|
27
|
+
| `\marginnote{}` | `MarginNote` | `…/components/MarginNote.astro` | — | Side commentary |
|
|
28
|
+
| `\sidenote{}` | `Sidenote` | `…/components/Sidenote.astro` | — | Auto-numbered marginalia (Tufte) |
|
|
29
|
+
| `\includegraphics + \caption` | `Figure` | `…/components/Figure.astro` | `src, caption?, id?` | XRef-registered |
|
|
30
|
+
| `\cite{}` / `\parencite{}` | `Citation` | `…/components/Citation.astro` | `src, as?` | Resolves `sources` collection |
|
|
31
|
+
| `\cite{}` (inline) | `Cite` | `…/components/Cite.astro` | `key` | Inline citation key |
|
|
32
|
+
| `\xref{}` / `\cref{}` | `XRef` | `…/components/XRef.astro` | `id` | Cross-reference resolver |
|
|
33
|
+
| `\code{path:N}` / inline file refs | `CodeRef` | `…/components/CodeRef.astro` | `path, line?, lineEnd?` | GitHub-linked source ref |
|
|
34
|
+
| (custom Shiki blocks) | `CodeBlock` | `…/components/CodeBlock.astro` | `lang, title?` | Wrapped fenced code |
|
|
35
|
+
| `\recovery{}` | `Recovery` | `…/components/Recovery.astro` | `pattern, symptom?` | Anti-pattern escape |
|
|
36
|
+
| `\casestudy{}` | `CaseStudy` | `…/components/CaseStudy.astro` | `date, title?` | Dated anecdote |
|
|
37
|
+
| `\weekref{}` (academic) | `WeekRef` | `…/components/WeekRef.astro` | `week` | Cross-chapter week ref |
|
|
38
|
+
|
|
39
|
+
Component subset table for tools-profile-specific UI (volatility dashboards, convergence timelines):
|
|
40
|
+
|
|
41
|
+
| Construct | Component | Use case |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| Volatility badge | `Tag` | `volatility` enum chip in chapter meta |
|
|
44
|
+
| Tool comparison | `ToolFilter` (island) | Interactive comparison gate |
|
|
45
|
+
| Version selector | `VersionSelector` (island) | Switch between tool versions |
|
|
46
|
+
| Convergence event | `Convergence` | "All tools converged here" timeline marker |
|
|
47
|
+
| Divergence event | `Divergence` | "Tool X went its own way" annotation |
|
|
48
|
+
| Pattern timeline | `PatternTimeline` | Multi-event convergence dashboard |
|
|
49
|
+
| Status badge | `StatusBadge` | 7→3-state translation (academic) |
|
|
50
|
+
| Source archive | `SourceArchive` | Tier-tagged source listing |
|
|
51
|
+
|
|
52
|
+
## What is NOT shipped (extension candidates)
|
|
53
|
+
|
|
54
|
+
The scaffold deliberately doesn't ship these. Add to your consumer via the [`defineMdxComponents`](#consumer-side-extensions-definemdxcomponents) helper described below; surface as a tracked issue if the gap recurs across pilots.
|
|
55
|
+
|
|
56
|
+
- `\begin{problem}` / `\begin{solution}` — interview-prep problem cards
|
|
57
|
+
- `\begin{vignette}` — multi-step scenario walkthroughs
|
|
58
|
+
- `\begin{decisiontree}` — branching decision logic
|
|
59
|
+
- `\begin{interviewcontext}` — interview-tied learning-outcome callouts
|
|
60
|
+
- `<AnkiCard>` — flashcard widget
|
|
61
|
+
- `<Term>` — glossary term reference with tooltip
|
|
62
|
+
- `<RedFlag>` — escalated warning beyond `WarnBox`
|
|
63
|
+
- `<NarrativeBox>` (with extended props) — if a consumer needs richer narrative annotations beyond `SkillBox`
|
|
64
|
+
|
|
65
|
+
## Consumer-side extensions: `defineMdxComponents`
|
|
66
|
+
|
|
67
|
+
When your book uses custom MDX components, create `src/mdx-components.ts` (or `.js` / `.mjs`) at your project root. The scaffold auto-detects it and threads the components through all auto-injected routes (`/print`, future `/pdf`, `/epub`).
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// consumer's src/mdx-components.ts
|
|
71
|
+
import { defineMdxComponents } from '@brandon_m_behring/book-scaffold-astro';
|
|
72
|
+
import AnkiCard from './components/AnkiCard.astro';
|
|
73
|
+
import NarrativeBox from './components/NarrativeBox.astro';
|
|
74
|
+
import Term from './components/Term.astro';
|
|
75
|
+
|
|
76
|
+
export default defineMdxComponents({
|
|
77
|
+
AnkiCard,
|
|
78
|
+
NarrativeBox,
|
|
79
|
+
Term,
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The `defineMdxComponents<T>()` helper is a TypeScript identity function — it returns the value unchanged, but preserves the exact key→component type mapping for IntelliSense. Same pattern as Vite/Astro `defineConfig`, Zod `z.object`, Drizzle `pgTable`.
|
|
84
|
+
|
|
85
|
+
To use a non-default path, pass it explicitly:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
// astro.config.mjs
|
|
89
|
+
export default defineBookConfig({
|
|
90
|
+
site: '...',
|
|
91
|
+
mdxComponentsModule: './src/my-custom-components.ts',
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Disabling auto-injected routes
|
|
96
|
+
|
|
97
|
+
The scaffold auto-injects per-profile defaults (see [PACKAGE_DESIGN.md](./PACKAGE_DESIGN.md) §6). Multi-book consumers (one Astro app, many books under `[book]/[chapter]`) typically want the flat `/chapters` route off:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
// astro.config.mjs
|
|
101
|
+
import { defineBookConfig } from '@brandon_m_behring/book-scaffold-astro';
|
|
102
|
+
|
|
103
|
+
export default defineBookConfig({
|
|
104
|
+
site: '...',
|
|
105
|
+
profile: 'course-notes',
|
|
106
|
+
routes: {
|
|
107
|
+
chapters: false, // override the profile default
|
|
108
|
+
convergence: false,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The shape is fixed (`references | search | print | chapters | convergence`) and TypeScript catches typos like `convergance: false`.
|
|
114
|
+
|
|
115
|
+
## Common conversion mistakes
|
|
116
|
+
|
|
117
|
+
Errors observed during the DLAI pilot (closed in v3.3.0 issue #5):
|
|
118
|
+
|
|
119
|
+
1. **Built `NarrativeBox` from scratch** → should have used `SkillBox`. Same vertical box semantic; `SkillBox` already has the `title` prop.
|
|
120
|
+
2. **Built `ConceptBox` (block container)** → conflicts with scaffold's `ConceptBox` (term-definition signature). Either rename the consumer one (`ConceptBlock`) or use scaffold's signature.
|
|
121
|
+
3. **Built `KeyConcept`** → should have used `KeyIdea`. Same crystallized-takeaway role; the scaffold's name comes from the Tufte-style margin-emphasis convention.
|
|
122
|
+
4. **Built `RedFlag`** → should have used `WarnBox`. Add a higher-severity variant via consumer-side `defineMdxComponents` if needed instead of duplicating WarnBox's semantic.
|
|
123
|
+
5. **Built `Sidenote` with category prop** → conflicts with scaffold's auto-numbered `Sidenote`. The scaffold uses CSS counters; consumer-side category metadata can wrap (e.g., `<TypedSidenote category="recovery"><Sidenote>...</Sidenote></TypedSidenote>`).
|
|
124
|
+
6. **Built duplicate `Citation` and `Figure`** → scaffold's versions are XRef-registered. Use them; extend behavior via wrapper components if richer attribution is needed.
|
|
125
|
+
|
|
126
|
+
## See also
|
|
127
|
+
|
|
128
|
+
- [PACKAGE_DESIGN.md](./PACKAGE_DESIGN.md) — full API contract + Phase A planning decisions
|
|
129
|
+
- [README.md](./README.md) — toolkit overview + getting started
|
|
130
|
+
- [CHANGELOG.md](../CHANGELOG.md) — release notes (issue #5 closed in v3.3.0)
|
|
@@ -9,9 +9,15 @@
|
|
|
9
9
|
* part, status, companion artifacts) appears in its place.
|
|
10
10
|
*
|
|
11
11
|
* v3.1.0 — academic flavor: Roman-numeral part labels, StatusBadge
|
|
12
|
-
* component, and an optional companion-artifacts block
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* component, and an optional companion-artifacts block.
|
|
13
|
+
*
|
|
14
|
+
* v3.2.0 — companions refactored from sibling <aside> to inline <span>
|
|
15
|
+
* elements inside the existing .chapter-meta flex row. v3.1.0 shipped
|
|
16
|
+
* <aside class="chapter-companions"> with no CSS; UA-default <ul> block
|
|
17
|
+
* layout added ~100px height at <=1280px, producing a uniform vertical
|
|
18
|
+
* pixel shift on all academic chapters. Inline rendering eliminates the
|
|
19
|
+
* extra block by construction. The data-companion attribute on each
|
|
20
|
+
* inline span preserves introspection.
|
|
15
21
|
*/
|
|
16
22
|
import type { CollectionEntry } from 'astro:content';
|
|
17
23
|
import { getFreshness, freshnessLabel } from '../src/lib/freshness';
|
|
@@ -92,13 +98,14 @@ const volatility = typeof d.volatility === 'string' ? d.volatility : null;
|
|
|
92
98
|
// generic basename strip so any book whose render-notebooks output lands
|
|
93
99
|
// under public/notebooks/ gets a correct deep link (v2.0 hardcoded a
|
|
94
100
|
// post_transformers-specific prefix; v3.1.0 generalizes).
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
//
|
|
102
|
+
// v3.2.0: each companion renders as an inline <span class="chapter-companion">
|
|
103
|
+
// inside .chapter-meta — no sibling <aside>, no <ul>, no "Companion artifacts:"
|
|
104
|
+
// label. Zero added vertical height vs v2.0.
|
|
105
|
+
const codePath = hasAcademicMeta && typeof d.code_path === 'string' ? d.code_path : null;
|
|
106
|
+
const testsPath = hasAcademicMeta && typeof d.tests_path === 'string' ? d.tests_path : null;
|
|
100
107
|
const notebookHtmlPath =
|
|
101
|
-
typeof d.notebook_path === 'string'
|
|
108
|
+
hasAcademicMeta && typeof d.notebook_path === 'string'
|
|
102
109
|
? `/notebooks/${(d.notebook_path as string)
|
|
103
110
|
.replace(/^.*\//, '')
|
|
104
111
|
.replace(/\.ipynb$/, '')}.html`
|
|
@@ -126,33 +133,25 @@ const notebookHtmlPath =
|
|
|
126
133
|
</span>
|
|
127
134
|
)}
|
|
128
135
|
{updated && <span>Updated {formatDate(updated)}</span>}
|
|
136
|
+
{codePath && (
|
|
137
|
+
<span class="chapter-companion" data-companion="code">
|
|
138
|
+
<CodeRef path={codePath} />
|
|
139
|
+
</span>
|
|
140
|
+
)}
|
|
141
|
+
{testsPath && (
|
|
142
|
+
<span class="chapter-companion" data-companion="tests">
|
|
143
|
+
<CodeRef path={testsPath} />
|
|
144
|
+
</span>
|
|
145
|
+
)}
|
|
146
|
+
{notebookHtmlPath && (
|
|
147
|
+
<span class="chapter-companion" data-companion="notebook">
|
|
148
|
+
<a href={notebookHtmlPath}>Notebook</a>
|
|
149
|
+
</span>
|
|
150
|
+
)}
|
|
129
151
|
</div>
|
|
130
152
|
<h1>{title}</h1>
|
|
131
153
|
{description && <p class="chapter-description">{description}</p>}
|
|
132
154
|
|
|
133
|
-
{hasCompanions && (
|
|
134
|
-
<aside class="chapter-companions">
|
|
135
|
-
<strong>Companion artifacts:</strong>
|
|
136
|
-
<ul>
|
|
137
|
-
{typeof d.code_path === 'string' && (
|
|
138
|
-
<li>
|
|
139
|
-
Implementation: <CodeRef path={d.code_path as string} />
|
|
140
|
-
</li>
|
|
141
|
-
)}
|
|
142
|
-
{typeof d.tests_path === 'string' && (
|
|
143
|
-
<li>
|
|
144
|
-
Tests: <CodeRef path={d.tests_path as string} />
|
|
145
|
-
</li>
|
|
146
|
-
)}
|
|
147
|
-
{notebookHtmlPath && (
|
|
148
|
-
<li>
|
|
149
|
-
Notebook: <a href={notebookHtmlPath}>{notebookHtmlPath}</a>
|
|
150
|
-
</li>
|
|
151
|
-
)}
|
|
152
|
-
</ul>
|
|
153
|
-
</aside>
|
|
154
|
-
)}
|
|
155
|
-
|
|
156
155
|
{hasToolsMeta && volatility && (
|
|
157
156
|
<div class="chapter-badge-row">
|
|
158
157
|
<span class="chapter-badge-row-label">Volatility:</span>
|
package/dist/index.d.ts
CHANGED
|
@@ -1,135 +1,82 @@
|
|
|
1
1
|
import { AstroUserConfig, AstroIntegration } from 'astro';
|
|
2
|
-
import { b as BookConfigOptions, d as BookScaffoldIntegrationOptions } from './types-
|
|
3
|
-
export { B as BOOK_PROFILES, a as BookConfigError, c as BookProfile, e as BookSchemasOptions, r as resolveProfile } from './types-
|
|
4
|
-
import
|
|
2
|
+
import { b as BookConfigOptions, d as BookScaffoldIntegrationOptions, v as volatilityLevels } from './types-s8NxCLU2.js';
|
|
3
|
+
export { A as AcademicChapter, B as BOOK_PROFILES, a as BookConfigError, c as BookProfile, e as BookSchemasOptions, C as ChapterFor, f as CourseNotesChapter, M as MinimalChapter, P as ProfileDefinition, R as RouteToggles, T as ToolsChapter, g as academicChapterSchema, h as academicParts, i as changeKinds, j as changelogSchema, k as chapterStatus, l as courseNotesChapterSchema, m as defineProfile, n as minimalChapterSchema, p as patternCategories, o as patternsSchema, r as resolveProfile, s as sourceTiers, q as sourcesSchema, t as toolSlugs, u as toolsChapterSchema } from './types-s8NxCLU2.js';
|
|
4
|
+
import 'astro/zod';
|
|
5
5
|
|
|
6
6
|
declare function defineBookConfig(opts: BookConfigOptions): Promise<AstroUserConfig>;
|
|
7
7
|
|
|
8
8
|
declare function bookScaffoldIntegration(opts: BookScaffoldIntegrationOptions): AstroIntegration;
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Identity helper — consumers wrap their mdx-components map for TS inference.
|
|
12
|
+
* Same pattern as Vite/Astro defineConfig: a generic identity function that
|
|
13
|
+
* preserves the exact shape (vs widening to Record<string, …>).
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
* be constructed at package-load time outside an Astro runtime context.
|
|
15
|
-
* `defineBookSchemas` in index.ts wraps these into Astro `defineCollection`
|
|
16
|
-
* calls at the consumer's content-config load time.
|
|
15
|
+
* export default defineMdxComponents({ AnkiCard, NarrativeBox });
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* The generic constraint Record<string, unknown> is intentionally loose:
|
|
18
|
+
* consumer components are `.astro` files whose runtime type
|
|
19
|
+
* (AstroComponentFactory) lives in `astro/runtime/server/index.js` — not a
|
|
20
|
+
* public Astro export, and importing from internals creates fragility. The
|
|
21
|
+
* looser constraint lets the helper compile cleanly across Astro versions;
|
|
22
|
+
* IntelliSense still surfaces exact keys.
|
|
20
23
|
*/
|
|
24
|
+
declare function defineMdxComponents<T extends Record<string, unknown>>(components: T): T;
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
declare
|
|
77
|
-
url: z.ZodString;
|
|
78
|
-
title: z.ZodString;
|
|
79
|
-
author: z.ZodOptional<z.ZodString>;
|
|
80
|
-
publish_date: z.ZodOptional<z.ZodDate>;
|
|
81
|
-
captured_at: z.ZodDate;
|
|
82
|
-
content_hash: z.ZodOptional<z.ZodString>;
|
|
83
|
-
tier: z.ZodEnum<{
|
|
84
|
-
"T1-official": "T1-official";
|
|
85
|
-
"T2-release-notes": "T2-release-notes";
|
|
86
|
-
"T3-practitioner": "T3-practitioner";
|
|
87
|
-
"T4-conjecture": "T4-conjecture";
|
|
88
|
-
}>;
|
|
89
|
-
tool: z.ZodEnum<{
|
|
90
|
-
"claude-code": "claude-code";
|
|
91
|
-
"gemini-cli": "gemini-cli";
|
|
92
|
-
"codex-cli": "codex-cli";
|
|
93
|
-
"cross-tool": "cross-tool";
|
|
94
|
-
}>;
|
|
95
|
-
perma_cc: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
96
|
-
local_cache: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
97
|
-
}, z.core.$strip>;
|
|
98
|
-
declare const changelogSchema: z.ZodObject<{
|
|
99
|
-
tool: z.ZodEnum<{
|
|
100
|
-
"claude-code": "claude-code";
|
|
101
|
-
"gemini-cli": "gemini-cli";
|
|
102
|
-
"codex-cli": "codex-cli";
|
|
103
|
-
"cross-tool": "cross-tool";
|
|
104
|
-
}>;
|
|
105
|
-
versions: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
106
|
-
version: z.ZodString;
|
|
107
|
-
date: z.ZodDate;
|
|
108
|
-
changes: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
109
|
-
pattern: z.ZodString;
|
|
110
|
-
kind: z.ZodEnum<{
|
|
111
|
-
added: "added";
|
|
112
|
-
removed: "removed";
|
|
113
|
-
changed: "changed";
|
|
114
|
-
deprecated: "deprecated";
|
|
115
|
-
}>;
|
|
116
|
-
note: z.ZodString;
|
|
117
|
-
source_key: z.ZodOptional<z.ZodString>;
|
|
118
|
-
}, z.core.$strip>>>;
|
|
119
|
-
}, z.core.$strip>>>;
|
|
120
|
-
}, z.core.$strip>;
|
|
121
|
-
declare const patternsSchema: z.ZodObject<{
|
|
122
|
-
name: z.ZodString;
|
|
123
|
-
description: z.ZodOptional<z.ZodString>;
|
|
124
|
-
category: z.ZodOptional<z.ZodEnum<{
|
|
125
|
-
safety: "safety";
|
|
126
|
-
scale: "scale";
|
|
127
|
-
context: "context";
|
|
128
|
-
interaction: "interaction";
|
|
129
|
-
extension: "extension";
|
|
130
|
-
other: "other";
|
|
131
|
-
}>>;
|
|
132
|
-
convergence_date: z.ZodOptional<z.ZodNullable<z.ZodDate>>;
|
|
133
|
-
}, z.core.$strip>;
|
|
26
|
+
/**
|
|
27
|
+
* src/lib/freshness.ts — volatility-aware staleness computation.
|
|
28
|
+
*
|
|
29
|
+
* Each chapter carries a `last_verified` date and a `volatility` class.
|
|
30
|
+
* This module maps those to a freshness status the reader can trust.
|
|
31
|
+
*
|
|
32
|
+
* Thresholds chosen to align with Ch 15's source-tier audit cadences:
|
|
33
|
+
* stable-principle → 365 days (principles drift annually at most)
|
|
34
|
+
* architectural-pattern → 180 days (between annual and quarterly; "on
|
|
35
|
+
* major release" isn't derivable from
|
|
36
|
+
* frontmatter)
|
|
37
|
+
* feature-surface → 90 days (quarterly)
|
|
38
|
+
*
|
|
39
|
+
* Status bands (fraction of threshold):
|
|
40
|
+
* fresh (<75%) — green dot, unobtrusive
|
|
41
|
+
* verify-soon (75-100%) — yellow, mild warning
|
|
42
|
+
* stale (>100%) — amber/red, "verify before trusting"
|
|
43
|
+
*
|
|
44
|
+
* Example assertions (verified by visual inspection during Stage 3.1):
|
|
45
|
+
* getFreshness(today, 'feature-surface').status === 'fresh'
|
|
46
|
+
* getFreshness(today-70d, 'feature-surface').status === 'verify-soon'
|
|
47
|
+
* getFreshness(today-100d, 'feature-surface').status === 'stale'
|
|
48
|
+
* getFreshness(today-200d, 'stable-principle').status === 'fresh'
|
|
49
|
+
* getFreshness(today-300d, 'stable-principle').status === 'verify-soon'
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
type VolatilityLevel = (typeof volatilityLevels)[number];
|
|
53
|
+
type FreshnessStatus = 'fresh' | 'verify-soon' | 'stale';
|
|
54
|
+
interface Freshness {
|
|
55
|
+
status: FreshnessStatus;
|
|
56
|
+
daysOld: number;
|
|
57
|
+
thresholdDays: number;
|
|
58
|
+
/** Days until stale; negative when already stale. */
|
|
59
|
+
daysUntil: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Compute freshness for a chapter given its last_verified date + volatility.
|
|
63
|
+
*
|
|
64
|
+
* Pure function; caller supplies `now` only in tests. Production callers omit.
|
|
65
|
+
*
|
|
66
|
+
* v3.3.0 (closes issue #1): tolerant of `lastVerified === undefined`. Returns
|
|
67
|
+
* `null` instead of crashing when the chapter schema omits the field (e.g.,
|
|
68
|
+
* academic profile chapters that don't track verification dates, or consumer
|
|
69
|
+
* schemas that don't declare last_verified).
|
|
70
|
+
*
|
|
71
|
+
* Callers compose with optional chaining:
|
|
72
|
+
* const status = getFreshness(d.last_verified, d.volatility)?.status;
|
|
73
|
+
*/
|
|
74
|
+
declare function getFreshness(lastVerified: Date | undefined, volatility: VolatilityLevel, now?: Date): Freshness | null;
|
|
75
|
+
/** Human-readable label for each status; used for ARIA + tooltips.
|
|
76
|
+
*
|
|
77
|
+
* v3.3.0: accepts `null` (the new return shape of getFreshness for undefined
|
|
78
|
+
* inputs). Returns a sentinel "unknown" label so callers can render a neutral
|
|
79
|
+
* affordance without a separate branch. */
|
|
80
|
+
declare function freshnessLabel(f: Freshness | null): string;
|
|
134
81
|
|
|
135
|
-
export { BookConfigOptions, BookScaffoldIntegrationOptions,
|
|
82
|
+
export { BookConfigOptions, BookScaffoldIntegrationOptions, type Freshness, type FreshnessStatus, type VolatilityLevel, bookScaffoldIntegration, defineBookConfig, defineMdxComponents, freshnessLabel, getFreshness, volatilityLevels };
|
package/dist/index.mjs
CHANGED
|
@@ -118,9 +118,223 @@ var init_katex_macros = __esm({
|
|
|
118
118
|
import mdx from "@astrojs/mdx";
|
|
119
119
|
import preact from "@astrojs/preact";
|
|
120
120
|
|
|
121
|
+
// src/profile-kit.ts
|
|
122
|
+
function defineProfile(p) {
|
|
123
|
+
return p;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/schemas.ts
|
|
127
|
+
import { z } from "astro/zod";
|
|
128
|
+
var toolSlugs = [
|
|
129
|
+
"claude-code",
|
|
130
|
+
"gemini-cli",
|
|
131
|
+
"codex-cli",
|
|
132
|
+
"cross-tool"
|
|
133
|
+
];
|
|
134
|
+
var volatilityLevels = [
|
|
135
|
+
"stable-principle",
|
|
136
|
+
"architectural-pattern",
|
|
137
|
+
"feature-surface"
|
|
138
|
+
];
|
|
139
|
+
var sourceTiers = [
|
|
140
|
+
"T1-official",
|
|
141
|
+
"T2-release-notes",
|
|
142
|
+
"T3-practitioner",
|
|
143
|
+
"T4-conjecture"
|
|
144
|
+
];
|
|
145
|
+
var changeKinds = ["added", "removed", "changed", "deprecated"];
|
|
146
|
+
var patternCategories = [
|
|
147
|
+
"safety",
|
|
148
|
+
"scale",
|
|
149
|
+
"context",
|
|
150
|
+
"interaction",
|
|
151
|
+
"extension",
|
|
152
|
+
"other"
|
|
153
|
+
];
|
|
154
|
+
var academicParts = [
|
|
155
|
+
"foundations",
|
|
156
|
+
"ssm-core",
|
|
157
|
+
"beyond-ssm",
|
|
158
|
+
"integration",
|
|
159
|
+
"synthesis"
|
|
160
|
+
];
|
|
161
|
+
var chapterStatus = [
|
|
162
|
+
"implemented",
|
|
163
|
+
"chapter_only",
|
|
164
|
+
"reading_only",
|
|
165
|
+
"prose_only",
|
|
166
|
+
"code_only",
|
|
167
|
+
"scaffolded",
|
|
168
|
+
"planned"
|
|
169
|
+
];
|
|
170
|
+
var academicChapterSchema = z.object({
|
|
171
|
+
week: z.number().int().min(1).max(99),
|
|
172
|
+
part: z.enum(academicParts),
|
|
173
|
+
title: z.string().min(1),
|
|
174
|
+
status: z.enum(chapterStatus),
|
|
175
|
+
roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
|
|
176
|
+
code_path: z.string().optional(),
|
|
177
|
+
tests_path: z.string().optional(),
|
|
178
|
+
notebook_path: z.string().optional(),
|
|
179
|
+
description: z.string().optional(),
|
|
180
|
+
draft: z.boolean().default(false)
|
|
181
|
+
});
|
|
182
|
+
var toolsChapterSchema = z.object({
|
|
183
|
+
title: z.string().min(1),
|
|
184
|
+
part: z.number().int().min(0).max(10),
|
|
185
|
+
chapter: z.number().int().min(0).max(99),
|
|
186
|
+
volatility: z.enum(volatilityLevels),
|
|
187
|
+
tools_compared: z.array(z.enum(toolSlugs)).min(1),
|
|
188
|
+
last_verified: z.date(),
|
|
189
|
+
sources: z.array(z.string()).default([]),
|
|
190
|
+
description: z.string().optional(),
|
|
191
|
+
draft: z.boolean().default(false),
|
|
192
|
+
updated: z.date().optional()
|
|
193
|
+
});
|
|
194
|
+
var minimalChapterSchema = toolsChapterSchema;
|
|
195
|
+
var courseNotesChapterSchema = z.object({
|
|
196
|
+
// Identity
|
|
197
|
+
title: z.string().min(1),
|
|
198
|
+
chapter: z.number().int().min(0).max(99),
|
|
199
|
+
part: z.number().int().min(0).max(20).default(1),
|
|
200
|
+
description: z.string().optional(),
|
|
201
|
+
// Source attribution
|
|
202
|
+
course: z.string().optional(),
|
|
203
|
+
instructor: z.string().optional(),
|
|
204
|
+
source_url: z.string().url().optional(),
|
|
205
|
+
// Pedagogy
|
|
206
|
+
learning_outcomes: z.array(
|
|
207
|
+
z.object({
|
|
208
|
+
id: z.string(),
|
|
209
|
+
verb: z.string(),
|
|
210
|
+
text: z.string()
|
|
211
|
+
})
|
|
212
|
+
).default([]),
|
|
213
|
+
tags: z.array(z.string()).default([]),
|
|
214
|
+
// Provenance + status (shared shape with tools profile)
|
|
215
|
+
last_verified: z.date(),
|
|
216
|
+
volatility: z.enum(volatilityLevels).default("architectural-pattern"),
|
|
217
|
+
sources: z.array(z.string()).default([]),
|
|
218
|
+
draft: z.boolean().default(false)
|
|
219
|
+
});
|
|
220
|
+
var sourcesSchema = z.object({
|
|
221
|
+
url: z.string().url(),
|
|
222
|
+
title: z.string().min(1),
|
|
223
|
+
author: z.string().optional(),
|
|
224
|
+
publish_date: z.date().optional(),
|
|
225
|
+
captured_at: z.date(),
|
|
226
|
+
content_hash: z.string().regex(/^sha256:[a-f0-9]+$/).optional(),
|
|
227
|
+
tier: z.enum(sourceTiers),
|
|
228
|
+
tool: z.enum(toolSlugs),
|
|
229
|
+
perma_cc: z.string().url().nullable().optional(),
|
|
230
|
+
local_cache: z.string().nullable().optional()
|
|
231
|
+
});
|
|
232
|
+
var changelogSchema = z.object({
|
|
233
|
+
tool: z.enum(toolSlugs),
|
|
234
|
+
versions: z.array(
|
|
235
|
+
z.object({
|
|
236
|
+
version: z.string().min(1),
|
|
237
|
+
date: z.date(),
|
|
238
|
+
changes: z.array(
|
|
239
|
+
z.object({
|
|
240
|
+
pattern: z.string(),
|
|
241
|
+
kind: z.enum(changeKinds),
|
|
242
|
+
note: z.string().min(1),
|
|
243
|
+
source_key: z.string().optional()
|
|
244
|
+
})
|
|
245
|
+
).default([])
|
|
246
|
+
})
|
|
247
|
+
).default([])
|
|
248
|
+
});
|
|
249
|
+
var patternsSchema = z.object({
|
|
250
|
+
name: z.string().min(1),
|
|
251
|
+
description: z.string().optional(),
|
|
252
|
+
category: z.enum(patternCategories).optional(),
|
|
253
|
+
convergence_date: z.date().nullable().optional()
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// src/profiles/academic.ts
|
|
257
|
+
var academicProfile = defineProfile({
|
|
258
|
+
name: "academic",
|
|
259
|
+
schema: academicChapterSchema,
|
|
260
|
+
routes: {
|
|
261
|
+
references: true,
|
|
262
|
+
search: true,
|
|
263
|
+
print: true,
|
|
264
|
+
chapters: false,
|
|
265
|
+
// academic consumers ship their own week-based /chapters listing
|
|
266
|
+
convergence: false
|
|
267
|
+
// tools-profile-specific
|
|
268
|
+
},
|
|
269
|
+
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
270
|
+
katex: true
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// src/profiles/tools.ts
|
|
274
|
+
var toolsProfile = defineProfile({
|
|
275
|
+
name: "tools",
|
|
276
|
+
schema: toolsChapterSchema,
|
|
277
|
+
routes: {
|
|
278
|
+
references: true,
|
|
279
|
+
search: true,
|
|
280
|
+
print: true,
|
|
281
|
+
chapters: true,
|
|
282
|
+
// tools profile ships a flat chapter index
|
|
283
|
+
convergence: true
|
|
284
|
+
// tools profile ships convergence dashboard
|
|
285
|
+
},
|
|
286
|
+
styles: [
|
|
287
|
+
"tokens.css",
|
|
288
|
+
"layout.css",
|
|
289
|
+
"callouts.css",
|
|
290
|
+
"chapter.css",
|
|
291
|
+
"typography.css",
|
|
292
|
+
"print.css",
|
|
293
|
+
"convergence.css",
|
|
294
|
+
"tool-filter.css"
|
|
295
|
+
]
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// src/profiles/minimal.ts
|
|
299
|
+
var minimalProfile = defineProfile({
|
|
300
|
+
name: "minimal",
|
|
301
|
+
schema: minimalChapterSchema,
|
|
302
|
+
routes: {
|
|
303
|
+
references: true,
|
|
304
|
+
search: true,
|
|
305
|
+
print: true,
|
|
306
|
+
chapters: false,
|
|
307
|
+
convergence: false
|
|
308
|
+
},
|
|
309
|
+
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// src/profiles/course-notes.ts
|
|
313
|
+
var courseNotesProfile = defineProfile({
|
|
314
|
+
name: "course-notes",
|
|
315
|
+
schema: courseNotesChapterSchema,
|
|
316
|
+
routes: {
|
|
317
|
+
references: true,
|
|
318
|
+
search: true,
|
|
319
|
+
print: true,
|
|
320
|
+
chapters: false,
|
|
321
|
+
// multi-book consumers route via [book]/[slug] themselves
|
|
322
|
+
convergence: false
|
|
323
|
+
},
|
|
324
|
+
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// src/profiles/index.ts
|
|
328
|
+
var PROFILES = {
|
|
329
|
+
academic: academicProfile,
|
|
330
|
+
tools: toolsProfile,
|
|
331
|
+
minimal: minimalProfile,
|
|
332
|
+
"course-notes": courseNotesProfile
|
|
333
|
+
};
|
|
334
|
+
var BOOK_PROFILES = Object.keys(PROFILES);
|
|
335
|
+
|
|
121
336
|
// src/types.ts
|
|
122
337
|
import { existsSync, readFileSync } from "fs";
|
|
123
|
-
var BOOK_PROFILES = ["academic", "tools", "minimal"];
|
|
124
338
|
var BookConfigError = class extends Error {
|
|
125
339
|
constructor(message) {
|
|
126
340
|
super(message);
|
|
@@ -171,48 +385,92 @@ function resolveProfile(explicit) {
|
|
|
171
385
|
|
|
172
386
|
// src/integration.ts
|
|
173
387
|
import { fileURLToPath } from "url";
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
];
|
|
183
|
-
var TOOLS_ONLY_STYLES = ["convergence.css", "tool-filter.css"];
|
|
184
|
-
var DEFAULT_ROUTES_ALL = [
|
|
185
|
-
{ pattern: "/references", file: "references.astro" },
|
|
186
|
-
{ pattern: "/search", file: "search.astro" },
|
|
187
|
-
{ pattern: "/print", file: "print.astro" }
|
|
188
|
-
];
|
|
189
|
-
var DEFAULT_ROUTES_TOOLS = [
|
|
190
|
-
{ pattern: "/chapters", file: "chapters.astro" },
|
|
191
|
-
{ pattern: "/convergence", file: "convergence.astro" }
|
|
388
|
+
|
|
389
|
+
// src/mdx-components-resolver.ts
|
|
390
|
+
import { existsSync as existsSync2 } from "fs";
|
|
391
|
+
import { resolve } from "path";
|
|
392
|
+
var CANDIDATE_FILES = [
|
|
393
|
+
"src/mdx-components.ts",
|
|
394
|
+
"src/mdx-components.js",
|
|
395
|
+
"src/mdx-components.mjs"
|
|
192
396
|
];
|
|
397
|
+
var VIRTUAL_ID = "virtual:book-scaffold/mdx-components";
|
|
398
|
+
var RESOLVED_ID = "\0" + VIRTUAL_ID;
|
|
399
|
+
function resolveMdxComponentsPath(consumerRoot, explicit) {
|
|
400
|
+
if (explicit) {
|
|
401
|
+
const p = resolve(consumerRoot, explicit);
|
|
402
|
+
return existsSync2(p) ? p : null;
|
|
403
|
+
}
|
|
404
|
+
for (const candidate of CANDIDATE_FILES) {
|
|
405
|
+
const p = resolve(consumerRoot, candidate);
|
|
406
|
+
if (existsSync2(p)) return p;
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
function makeMdxComponentsVitePlugin(consumerPath) {
|
|
411
|
+
return {
|
|
412
|
+
name: "book-scaffold:mdx-components",
|
|
413
|
+
enforce: "pre",
|
|
414
|
+
resolveId(id) {
|
|
415
|
+
if (id === VIRTUAL_ID) return RESOLVED_ID;
|
|
416
|
+
return null;
|
|
417
|
+
},
|
|
418
|
+
load(id) {
|
|
419
|
+
if (id !== RESOLVED_ID) return null;
|
|
420
|
+
if (consumerPath === null) {
|
|
421
|
+
return "export default {};";
|
|
422
|
+
}
|
|
423
|
+
return `export { default } from ${JSON.stringify(consumerPath)};`;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function defineMdxComponents(components) {
|
|
428
|
+
return components;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/integration.ts
|
|
432
|
+
var PACKAGE_NAME = "@brandon_m_behring/book-scaffold-astro";
|
|
433
|
+
var ROUTE_REGISTRY = {
|
|
434
|
+
references: { pattern: "/references", file: "references.astro" },
|
|
435
|
+
search: { pattern: "/search", file: "search.astro" },
|
|
436
|
+
print: { pattern: "/print", file: "print.astro" },
|
|
437
|
+
chapters: { pattern: "/chapters", file: "chapters.astro" },
|
|
438
|
+
convergence: { pattern: "/convergence", file: "convergence.astro" }
|
|
439
|
+
};
|
|
193
440
|
function resolvePage(file) {
|
|
194
441
|
return fileURLToPath(new URL(`../pages/${file}`, import.meta.url));
|
|
195
442
|
}
|
|
196
443
|
function bookScaffoldIntegration(opts) {
|
|
197
|
-
const { profile, extraStyles = [] } = opts;
|
|
444
|
+
const { profile, routes: userOverrides = {}, extraStyles = [], mdxComponentsModule } = opts;
|
|
445
|
+
const def = PROFILES[profile];
|
|
446
|
+
const enabledRoutes = { ...def.routes, ...userOverrides };
|
|
198
447
|
return {
|
|
199
448
|
name: "book-scaffold-astro",
|
|
200
449
|
hooks: {
|
|
201
|
-
"astro:config:setup": ({ injectScript, injectRoute }) => {
|
|
202
|
-
const styles =
|
|
450
|
+
"astro:config:setup": ({ injectScript, injectRoute, updateConfig, config }) => {
|
|
451
|
+
const styles = [...def.styles, ...extraStyles];
|
|
203
452
|
for (const sheet of styles) {
|
|
204
453
|
injectScript("page-ssr", `import '${PACKAGE_NAME}/styles/${sheet}';`);
|
|
205
454
|
}
|
|
206
|
-
if (
|
|
455
|
+
if (def.katex) {
|
|
207
456
|
injectScript("page-ssr", "import 'katex/dist/katex.min.css';");
|
|
208
457
|
}
|
|
209
|
-
const
|
|
210
|
-
|
|
458
|
+
for (const [name, on] of Object.entries(enabledRoutes)) {
|
|
459
|
+
if (!on) continue;
|
|
460
|
+
const route = ROUTE_REGISTRY[name];
|
|
461
|
+
if (!route) continue;
|
|
211
462
|
injectRoute({
|
|
212
463
|
pattern: route.pattern,
|
|
213
464
|
entrypoint: resolvePage(route.file)
|
|
214
465
|
});
|
|
215
466
|
}
|
|
467
|
+
const consumerRoot = fileURLToPath(config.root);
|
|
468
|
+
const resolvedMdxPath = resolveMdxComponentsPath(consumerRoot, mdxComponentsModule);
|
|
469
|
+
updateConfig({
|
|
470
|
+
vite: {
|
|
471
|
+
plugins: [makeMdxComponentsVitePlugin(resolvedMdxPath)]
|
|
472
|
+
}
|
|
473
|
+
});
|
|
216
474
|
}
|
|
217
475
|
}
|
|
218
476
|
};
|
|
@@ -249,7 +507,14 @@ async function defineBookConfig(opts) {
|
|
|
249
507
|
const integrations = [
|
|
250
508
|
mdx(),
|
|
251
509
|
preact(),
|
|
252
|
-
bookScaffoldIntegration({
|
|
510
|
+
bookScaffoldIntegration({
|
|
511
|
+
profile,
|
|
512
|
+
routes: opts.routes,
|
|
513
|
+
// v3.3.0 — per-route override (issue #3)
|
|
514
|
+
mdxComponentsModule: opts.mdxComponentsModule,
|
|
515
|
+
// v3.3.0 — explicit mdx-components path (issue #2)
|
|
516
|
+
extraStyles: opts.extraStyles
|
|
517
|
+
}),
|
|
253
518
|
...opts.extraIntegrations ?? []
|
|
254
519
|
];
|
|
255
520
|
const userMarkdown = opts.markdown ?? {};
|
|
@@ -267,12 +532,18 @@ async function defineBookConfig(opts) {
|
|
|
267
532
|
};
|
|
268
533
|
const {
|
|
269
534
|
profile: _profile,
|
|
535
|
+
routes: _routes,
|
|
536
|
+
// v3.3.0
|
|
537
|
+
mdxComponentsModule: _mdxComponentsModule,
|
|
538
|
+
// v3.3.0
|
|
270
539
|
extraIntegrations: _extraIntegrations,
|
|
271
540
|
extraStyles: _extraStyles,
|
|
272
541
|
markdown: _markdown,
|
|
273
542
|
...rest
|
|
274
543
|
} = opts;
|
|
275
544
|
void _profile;
|
|
545
|
+
void _routes;
|
|
546
|
+
void _mdxComponentsModule;
|
|
276
547
|
void _extraIntegrations;
|
|
277
548
|
void _extraStyles;
|
|
278
549
|
void _markdown;
|
|
@@ -293,109 +564,39 @@ async function defineBookConfig(opts) {
|
|
|
293
564
|
return config;
|
|
294
565
|
}
|
|
295
566
|
|
|
296
|
-
// src/
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
"
|
|
300
|
-
"
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
"
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
"synthesis"
|
|
330
|
-
];
|
|
331
|
-
var chapterStatus = [
|
|
332
|
-
"implemented",
|
|
333
|
-
"chapter_only",
|
|
334
|
-
"reading_only",
|
|
335
|
-
"prose_only",
|
|
336
|
-
"code_only",
|
|
337
|
-
"scaffolded",
|
|
338
|
-
"planned"
|
|
339
|
-
];
|
|
340
|
-
var academicChapterSchema = z.object({
|
|
341
|
-
week: z.number().int().min(1).max(99),
|
|
342
|
-
part: z.enum(academicParts),
|
|
343
|
-
title: z.string().min(1),
|
|
344
|
-
status: z.enum(chapterStatus),
|
|
345
|
-
roadmap_lines: z.tuple([z.number().int(), z.number().int()]).optional(),
|
|
346
|
-
code_path: z.string().optional(),
|
|
347
|
-
tests_path: z.string().optional(),
|
|
348
|
-
notebook_path: z.string().optional(),
|
|
349
|
-
description: z.string().optional(),
|
|
350
|
-
draft: z.boolean().default(false)
|
|
351
|
-
});
|
|
352
|
-
var toolsChapterSchema = z.object({
|
|
353
|
-
title: z.string().min(1),
|
|
354
|
-
part: z.number().int().min(0).max(10),
|
|
355
|
-
chapter: z.number().int().min(0).max(99),
|
|
356
|
-
volatility: z.enum(volatilityLevels),
|
|
357
|
-
tools_compared: z.array(z.enum(toolSlugs)).min(1),
|
|
358
|
-
last_verified: z.date(),
|
|
359
|
-
sources: z.array(z.string()).default([]),
|
|
360
|
-
description: z.string().optional(),
|
|
361
|
-
draft: z.boolean().default(false),
|
|
362
|
-
updated: z.date().optional()
|
|
363
|
-
});
|
|
364
|
-
var sourcesSchema = z.object({
|
|
365
|
-
url: z.string().url(),
|
|
366
|
-
title: z.string().min(1),
|
|
367
|
-
author: z.string().optional(),
|
|
368
|
-
publish_date: z.date().optional(),
|
|
369
|
-
captured_at: z.date(),
|
|
370
|
-
content_hash: z.string().regex(/^sha256:[a-f0-9]+$/).optional(),
|
|
371
|
-
tier: z.enum(sourceTiers),
|
|
372
|
-
tool: z.enum(toolSlugs),
|
|
373
|
-
perma_cc: z.string().url().nullable().optional(),
|
|
374
|
-
local_cache: z.string().nullable().optional()
|
|
375
|
-
});
|
|
376
|
-
var changelogSchema = z.object({
|
|
377
|
-
tool: z.enum(toolSlugs),
|
|
378
|
-
versions: z.array(
|
|
379
|
-
z.object({
|
|
380
|
-
version: z.string().min(1),
|
|
381
|
-
date: z.date(),
|
|
382
|
-
changes: z.array(
|
|
383
|
-
z.object({
|
|
384
|
-
pattern: z.string(),
|
|
385
|
-
kind: z.enum(changeKinds),
|
|
386
|
-
note: z.string().min(1),
|
|
387
|
-
source_key: z.string().optional()
|
|
388
|
-
})
|
|
389
|
-
).default([])
|
|
390
|
-
})
|
|
391
|
-
).default([])
|
|
392
|
-
});
|
|
393
|
-
var patternsSchema = z.object({
|
|
394
|
-
name: z.string().min(1),
|
|
395
|
-
description: z.string().optional(),
|
|
396
|
-
category: z.enum(patternCategories).optional(),
|
|
397
|
-
convergence_date: z.date().nullable().optional()
|
|
398
|
-
});
|
|
567
|
+
// src/lib/freshness.ts
|
|
568
|
+
var THRESHOLDS = {
|
|
569
|
+
"stable-principle": 365,
|
|
570
|
+
"architectural-pattern": 180,
|
|
571
|
+
"feature-surface": 90
|
|
572
|
+
};
|
|
573
|
+
var MS_PER_DAY = 1e3 * 60 * 60 * 24;
|
|
574
|
+
function getFreshness(lastVerified, volatility, now = /* @__PURE__ */ new Date()) {
|
|
575
|
+
if (!(lastVerified instanceof Date)) return null;
|
|
576
|
+
const thresholdDays = THRESHOLDS[volatility];
|
|
577
|
+
const daysOld = Math.floor((now.getTime() - lastVerified.getTime()) / MS_PER_DAY);
|
|
578
|
+
const daysUntil = thresholdDays - daysOld;
|
|
579
|
+
let status;
|
|
580
|
+
if (daysOld < thresholdDays * 0.75) {
|
|
581
|
+
status = "fresh";
|
|
582
|
+
} else if (daysOld < thresholdDays) {
|
|
583
|
+
status = "verify-soon";
|
|
584
|
+
} else {
|
|
585
|
+
status = "stale";
|
|
586
|
+
}
|
|
587
|
+
return { status, daysOld, thresholdDays, daysUntil };
|
|
588
|
+
}
|
|
589
|
+
function freshnessLabel(f) {
|
|
590
|
+
if (f === null) return "Verification status unknown";
|
|
591
|
+
switch (f.status) {
|
|
592
|
+
case "fresh":
|
|
593
|
+
return `Fresh (${f.daysOld}d old; verify within ${f.daysUntil}d)`;
|
|
594
|
+
case "verify-soon":
|
|
595
|
+
return `Verify soon (${f.daysOld}d old; ${f.daysUntil}d until stale)`;
|
|
596
|
+
case "stale":
|
|
597
|
+
return `Stale (${f.daysOld}d old; ${Math.abs(f.daysUntil)}d past threshold)`;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
399
600
|
export {
|
|
400
601
|
BOOK_PROFILES,
|
|
401
602
|
BookConfigError,
|
|
@@ -405,7 +606,13 @@ export {
|
|
|
405
606
|
changeKinds,
|
|
406
607
|
changelogSchema,
|
|
407
608
|
chapterStatus,
|
|
609
|
+
courseNotesChapterSchema,
|
|
408
610
|
defineBookConfig,
|
|
611
|
+
defineMdxComponents,
|
|
612
|
+
defineProfile,
|
|
613
|
+
freshnessLabel,
|
|
614
|
+
getFreshness,
|
|
615
|
+
minimalChapterSchema,
|
|
409
616
|
patternCategories,
|
|
410
617
|
patternsSchema,
|
|
411
618
|
resolveProfile,
|
package/dist/schemas.d.ts
CHANGED
package/dist/schemas.mjs
CHANGED
|
@@ -3,55 +3,9 @@ import { existsSync as existsSync2 } from "fs";
|
|
|
3
3
|
import { defineCollection } from "astro:content";
|
|
4
4
|
import { glob, file } from "astro/loaders";
|
|
5
5
|
|
|
6
|
-
// src/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var BookConfigError = class extends Error {
|
|
10
|
-
constructor(message) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = "BookConfigError";
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
function readEnvFile(path = ".env") {
|
|
16
|
-
try {
|
|
17
|
-
if (!existsSync(path)) return {};
|
|
18
|
-
const out = {};
|
|
19
|
-
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
20
|
-
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
21
|
-
if (!m) continue;
|
|
22
|
-
let val = m[2] ?? "";
|
|
23
|
-
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
24
|
-
val = val.slice(1, -1);
|
|
25
|
-
}
|
|
26
|
-
out[m[1]] = val;
|
|
27
|
-
}
|
|
28
|
-
return out;
|
|
29
|
-
} catch {
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function resolveProfile(explicit) {
|
|
34
|
-
let candidate = explicit ?? process.env.BOOK_PROFILE;
|
|
35
|
-
let source = "default";
|
|
36
|
-
if (explicit) source = "param";
|
|
37
|
-
else if (process.env.BOOK_PROFILE) source = "env";
|
|
38
|
-
if (!candidate) {
|
|
39
|
-
const fromFile = readEnvFile().BOOK_PROFILE;
|
|
40
|
-
if (fromFile) {
|
|
41
|
-
candidate = fromFile;
|
|
42
|
-
source = "dotenv";
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
candidate = candidate ?? "minimal";
|
|
46
|
-
if (!BOOK_PROFILES.includes(candidate)) {
|
|
47
|
-
throw new BookConfigError(
|
|
48
|
-
`profile must be one of ${BOOK_PROFILES.join(" | ")} (got ${JSON.stringify(candidate)})`
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
if (source === "default") {
|
|
52
|
-
console.warn("book-scaffold-astro: BOOK_PROFILE not set; falling back to 'minimal'.");
|
|
53
|
-
}
|
|
54
|
-
return candidate;
|
|
6
|
+
// src/profile-kit.ts
|
|
7
|
+
function defineProfile(p) {
|
|
8
|
+
return p;
|
|
55
9
|
}
|
|
56
10
|
|
|
57
11
|
// src/schemas.ts
|
|
@@ -122,6 +76,32 @@ var toolsChapterSchema = z.object({
|
|
|
122
76
|
draft: z.boolean().default(false),
|
|
123
77
|
updated: z.date().optional()
|
|
124
78
|
});
|
|
79
|
+
var minimalChapterSchema = toolsChapterSchema;
|
|
80
|
+
var courseNotesChapterSchema = z.object({
|
|
81
|
+
// Identity
|
|
82
|
+
title: z.string().min(1),
|
|
83
|
+
chapter: z.number().int().min(0).max(99),
|
|
84
|
+
part: z.number().int().min(0).max(20).default(1),
|
|
85
|
+
description: z.string().optional(),
|
|
86
|
+
// Source attribution
|
|
87
|
+
course: z.string().optional(),
|
|
88
|
+
instructor: z.string().optional(),
|
|
89
|
+
source_url: z.string().url().optional(),
|
|
90
|
+
// Pedagogy
|
|
91
|
+
learning_outcomes: z.array(
|
|
92
|
+
z.object({
|
|
93
|
+
id: z.string(),
|
|
94
|
+
verb: z.string(),
|
|
95
|
+
text: z.string()
|
|
96
|
+
})
|
|
97
|
+
).default([]),
|
|
98
|
+
tags: z.array(z.string()).default([]),
|
|
99
|
+
// Provenance + status (shared shape with tools profile)
|
|
100
|
+
last_verified: z.date(),
|
|
101
|
+
volatility: z.enum(volatilityLevels).default("architectural-pattern"),
|
|
102
|
+
sources: z.array(z.string()).default([]),
|
|
103
|
+
draft: z.boolean().default(false)
|
|
104
|
+
});
|
|
125
105
|
var sourcesSchema = z.object({
|
|
126
106
|
url: z.string().url(),
|
|
127
107
|
title: z.string().min(1),
|
|
@@ -158,17 +138,148 @@ var patternsSchema = z.object({
|
|
|
158
138
|
convergence_date: z.date().nullable().optional()
|
|
159
139
|
});
|
|
160
140
|
|
|
141
|
+
// src/profiles/academic.ts
|
|
142
|
+
var academicProfile = defineProfile({
|
|
143
|
+
name: "academic",
|
|
144
|
+
schema: academicChapterSchema,
|
|
145
|
+
routes: {
|
|
146
|
+
references: true,
|
|
147
|
+
search: true,
|
|
148
|
+
print: true,
|
|
149
|
+
chapters: false,
|
|
150
|
+
// academic consumers ship their own week-based /chapters listing
|
|
151
|
+
convergence: false
|
|
152
|
+
// tools-profile-specific
|
|
153
|
+
},
|
|
154
|
+
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
|
|
155
|
+
katex: true
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// src/profiles/tools.ts
|
|
159
|
+
var toolsProfile = defineProfile({
|
|
160
|
+
name: "tools",
|
|
161
|
+
schema: toolsChapterSchema,
|
|
162
|
+
routes: {
|
|
163
|
+
references: true,
|
|
164
|
+
search: true,
|
|
165
|
+
print: true,
|
|
166
|
+
chapters: true,
|
|
167
|
+
// tools profile ships a flat chapter index
|
|
168
|
+
convergence: true
|
|
169
|
+
// tools profile ships convergence dashboard
|
|
170
|
+
},
|
|
171
|
+
styles: [
|
|
172
|
+
"tokens.css",
|
|
173
|
+
"layout.css",
|
|
174
|
+
"callouts.css",
|
|
175
|
+
"chapter.css",
|
|
176
|
+
"typography.css",
|
|
177
|
+
"print.css",
|
|
178
|
+
"convergence.css",
|
|
179
|
+
"tool-filter.css"
|
|
180
|
+
]
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// src/profiles/minimal.ts
|
|
184
|
+
var minimalProfile = defineProfile({
|
|
185
|
+
name: "minimal",
|
|
186
|
+
schema: minimalChapterSchema,
|
|
187
|
+
routes: {
|
|
188
|
+
references: true,
|
|
189
|
+
search: true,
|
|
190
|
+
print: true,
|
|
191
|
+
chapters: false,
|
|
192
|
+
convergence: false
|
|
193
|
+
},
|
|
194
|
+
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// src/profiles/course-notes.ts
|
|
198
|
+
var courseNotesProfile = defineProfile({
|
|
199
|
+
name: "course-notes",
|
|
200
|
+
schema: courseNotesChapterSchema,
|
|
201
|
+
routes: {
|
|
202
|
+
references: true,
|
|
203
|
+
search: true,
|
|
204
|
+
print: true,
|
|
205
|
+
chapters: false,
|
|
206
|
+
// multi-book consumers route via [book]/[slug] themselves
|
|
207
|
+
convergence: false
|
|
208
|
+
},
|
|
209
|
+
styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// src/profiles/index.ts
|
|
213
|
+
var PROFILES = {
|
|
214
|
+
academic: academicProfile,
|
|
215
|
+
tools: toolsProfile,
|
|
216
|
+
minimal: minimalProfile,
|
|
217
|
+
"course-notes": courseNotesProfile
|
|
218
|
+
};
|
|
219
|
+
var BOOK_PROFILES = Object.keys(PROFILES);
|
|
220
|
+
|
|
221
|
+
// src/types.ts
|
|
222
|
+
import { existsSync, readFileSync } from "fs";
|
|
223
|
+
var BookConfigError = class extends Error {
|
|
224
|
+
constructor(message) {
|
|
225
|
+
super(message);
|
|
226
|
+
this.name = "BookConfigError";
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
function readEnvFile(path = ".env") {
|
|
230
|
+
try {
|
|
231
|
+
if (!existsSync(path)) return {};
|
|
232
|
+
const out = {};
|
|
233
|
+
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
234
|
+
const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
235
|
+
if (!m) continue;
|
|
236
|
+
let val = m[2] ?? "";
|
|
237
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
238
|
+
val = val.slice(1, -1);
|
|
239
|
+
}
|
|
240
|
+
out[m[1]] = val;
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
} catch {
|
|
244
|
+
return {};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function resolveProfile(explicit) {
|
|
248
|
+
let candidate = explicit ?? process.env.BOOK_PROFILE;
|
|
249
|
+
let source = "default";
|
|
250
|
+
if (explicit) source = "param";
|
|
251
|
+
else if (process.env.BOOK_PROFILE) source = "env";
|
|
252
|
+
if (!candidate) {
|
|
253
|
+
const fromFile = readEnvFile().BOOK_PROFILE;
|
|
254
|
+
if (fromFile) {
|
|
255
|
+
candidate = fromFile;
|
|
256
|
+
source = "dotenv";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
candidate = candidate ?? "minimal";
|
|
260
|
+
if (!BOOK_PROFILES.includes(candidate)) {
|
|
261
|
+
throw new BookConfigError(
|
|
262
|
+
`profile must be one of ${BOOK_PROFILES.join(" | ")} (got ${JSON.stringify(candidate)})`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (source === "default") {
|
|
266
|
+
console.warn("book-scaffold-astro: BOOK_PROFILE not set; falling back to 'minimal'.");
|
|
267
|
+
}
|
|
268
|
+
return candidate;
|
|
269
|
+
}
|
|
270
|
+
|
|
161
271
|
// src/schemas-entry.ts
|
|
162
272
|
function defineBookSchemas(opts = {}) {
|
|
163
273
|
const profile = resolveProfile(opts.profile);
|
|
164
274
|
const chaptersBase = opts.chaptersBase ?? "./src/content/chapters";
|
|
275
|
+
const schemaForProfile = profile === "academic" ? academicChapterSchema : profile === "course-notes" ? courseNotesChapterSchema : profile === "minimal" ? minimalChapterSchema : toolsChapterSchema;
|
|
165
276
|
const chapters = defineCollection({
|
|
166
277
|
loader: glob({
|
|
167
278
|
// Exclude underscore-prefixed files (standard "hidden" convention).
|
|
168
279
|
pattern: ["**/*.{md,mdx}", "!**/_*"],
|
|
169
280
|
base: chaptersBase
|
|
170
281
|
}),
|
|
171
|
-
schema:
|
|
282
|
+
schema: schemaForProfile
|
|
172
283
|
});
|
|
173
284
|
const collections = {
|
|
174
285
|
chapters
|
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.
|
|
4
|
+
"version": "3.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Brandon Behring",
|
|
@@ -113,7 +113,8 @@
|
|
|
113
113
|
"pedagogy",
|
|
114
114
|
"examples",
|
|
115
115
|
"CLAUDE.md",
|
|
116
|
-
"README.md"
|
|
116
|
+
"README.md",
|
|
117
|
+
"LATEX_TO_MDX_MAPPING.md"
|
|
117
118
|
],
|
|
118
119
|
"scripts": {
|
|
119
120
|
"build": "tsup && rm -f dist/types-*.d.ts",
|
package/pages/print.astro
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
* body, and wraps in a <section.chapter-print> so print.css can force
|
|
8
8
|
* page breaks between chapters.
|
|
9
9
|
*
|
|
10
|
+
* v3.3.0 (closes #2): renders chapters with the consumer's MDX-components
|
|
11
|
+
* map. Consumer creates src/mdx-components.{ts,js,mjs} that default-exports
|
|
12
|
+
* a defineMdxComponents({...}) call; this route imports the map via the
|
|
13
|
+
* virtual:book-scaffold/mdx-components module exposed by the toolkit's
|
|
14
|
+
* Vite plugin. If the consumer has no mdx-components file, the virtual
|
|
15
|
+
* module exports {} so the import is harmless.
|
|
16
|
+
*
|
|
10
17
|
* Build pipeline:
|
|
11
18
|
* npm run build → Astro emits dist/print/index.html
|
|
12
19
|
* npm run pdf → pagedjs-cli fetches dist/print/ via preview
|
|
@@ -17,6 +24,7 @@ import Base from '../layouts/Base.astro';
|
|
|
17
24
|
import { render } from 'astro:content';
|
|
18
25
|
import { getAllChapters } from '../src/lib/chapters';
|
|
19
26
|
import ChapterHeader from '../components/ChapterHeader.astro';
|
|
27
|
+
import mdxComponents from 'virtual:book-scaffold/mdx-components';
|
|
20
28
|
|
|
21
29
|
const chapters = await getAllChapters();
|
|
22
30
|
const rendered = await Promise.all(
|
|
@@ -32,7 +40,7 @@ const rendered = await Promise.all(
|
|
|
32
40
|
{rendered.map(({ entry, Content }) => (
|
|
33
41
|
<section class="chapter-print">
|
|
34
42
|
<ChapterHeader data={entry.data} />
|
|
35
|
-
<Content />
|
|
43
|
+
<Content components={mdxComponents} />
|
|
36
44
|
</section>
|
|
37
45
|
))}
|
|
38
46
|
</main>
|
package/src/lib/freshness.ts
CHANGED
|
@@ -49,12 +49,22 @@ const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
|
49
49
|
* Compute freshness for a chapter given its last_verified date + volatility.
|
|
50
50
|
*
|
|
51
51
|
* Pure function; caller supplies `now` only in tests. Production callers omit.
|
|
52
|
+
*
|
|
53
|
+
* v3.3.0 (closes issue #1): tolerant of `lastVerified === undefined`. Returns
|
|
54
|
+
* `null` instead of crashing when the chapter schema omits the field (e.g.,
|
|
55
|
+
* academic profile chapters that don't track verification dates, or consumer
|
|
56
|
+
* schemas that don't declare last_verified).
|
|
57
|
+
*
|
|
58
|
+
* Callers compose with optional chaining:
|
|
59
|
+
* const status = getFreshness(d.last_verified, d.volatility)?.status;
|
|
52
60
|
*/
|
|
53
61
|
export function getFreshness(
|
|
54
|
-
lastVerified: Date,
|
|
62
|
+
lastVerified: Date | undefined,
|
|
55
63
|
volatility: VolatilityLevel,
|
|
56
64
|
now: Date = new Date(),
|
|
57
|
-
): Freshness {
|
|
65
|
+
): Freshness | null {
|
|
66
|
+
if (!(lastVerified instanceof Date)) return null;
|
|
67
|
+
|
|
58
68
|
const thresholdDays = THRESHOLDS[volatility];
|
|
59
69
|
const daysOld = Math.floor((now.getTime() - lastVerified.getTime()) / MS_PER_DAY);
|
|
60
70
|
const daysUntil = thresholdDays - daysOld;
|
|
@@ -71,8 +81,13 @@ export function getFreshness(
|
|
|
71
81
|
return { status, daysOld, thresholdDays, daysUntil };
|
|
72
82
|
}
|
|
73
83
|
|
|
74
|
-
/** Human-readable label for each status; used for ARIA + tooltips.
|
|
75
|
-
|
|
84
|
+
/** Human-readable label for each status; used for ARIA + tooltips.
|
|
85
|
+
*
|
|
86
|
+
* v3.3.0: accepts `null` (the new return shape of getFreshness for undefined
|
|
87
|
+
* inputs). Returns a sentinel "unknown" label so callers can render a neutral
|
|
88
|
+
* affordance without a separate branch. */
|
|
89
|
+
export function freshnessLabel(f: Freshness | null): string {
|
|
90
|
+
if (f === null) return 'Verification status unknown';
|
|
76
91
|
switch (f.status) {
|
|
77
92
|
case 'fresh':
|
|
78
93
|
return `Fresh (${f.daysOld}d old; verify within ${f.daysUntil}d)`;
|
package/styles/chapter.css
CHANGED
|
@@ -38,6 +38,29 @@
|
|
|
38
38
|
font-size: 0.75em;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/* Inline companion-artifact chips inside .chapter-meta.
|
|
42
|
+
* v3.2.0: structural inline rendering. Previous v3.1.0 emitted a sibling
|
|
43
|
+
* <aside class="chapter-companions"><ul>...</ul></aside> with no CSS
|
|
44
|
+
* coverage; UA-default block layout added ~100px height at <=1280px,
|
|
45
|
+
* producing a uniform vertical pixel shift on academic chapter pages
|
|
46
|
+
* vs the v2.0 baseline. The inline span here adds zero block height by
|
|
47
|
+
* construction; styling matches the surrounding .chapter-meta spans. */
|
|
48
|
+
.chapter-companion {
|
|
49
|
+
font-family: var(--font-code);
|
|
50
|
+
font-size: var(--text-sm);
|
|
51
|
+
color: var(--color-text-muted);
|
|
52
|
+
}
|
|
53
|
+
.chapter-companion a,
|
|
54
|
+
.chapter-companion code {
|
|
55
|
+
color: inherit;
|
|
56
|
+
text-decoration: none;
|
|
57
|
+
border-bottom: 1px dotted var(--color-border);
|
|
58
|
+
}
|
|
59
|
+
.chapter-companion a:hover {
|
|
60
|
+
color: var(--color-link);
|
|
61
|
+
border-bottom-style: solid;
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
/* Volatility + tool badges: reuse .tool-badge from callouts.css */
|
|
42
65
|
.volatility-badge {
|
|
43
66
|
display: inline-block;
|