@brandon_m_behring/book-scaffold-astro 4.15.0 → 4.16.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/CLAUDE.md CHANGED
@@ -100,7 +100,7 @@ Two callout families coexist. Authors import what they need.
100
100
 
101
101
  **Pedagogy family** (v4.1.0+, any profile, 3 components): `Pitfall` (rose; "common mistake" — distinct from `WarnBox`'s preemptive warning), `WorkedExample` (plum; collapsible `<details>` block with `#worked-example-{id}` anchor for deep links), `YouWillLearn` (gold; chapter-opener with optional `prerequisites` prop). Slot bullets/code freely; render at any preset.
102
102
 
103
- **Utility components** (`src/components/`, any profile): `Cite`, `XRef`, `Figure`, `MarginNote`, `Sidenote`, `WeekRef`, `CodeRef`, `CodeBlock`, `Tag`, `StatusBadge`, `PocLayout` (v4.1.0+; wraps slot in a per-`kind` layout shell — 5 closed-union kinds; see `recipes/15-defining-styles.md`).
103
+ **Utility components** (`src/components/`, any profile): `Cite`, `XRef`, `Figure`, `MarginNote`, `Sidenote`, `WeekRef`, `CodeRef`, `CodeBlock`, `Tag`, `StatusBadge`, `BookLink` (v4.16.0+; cross-book link — `<BookLink book="design" to="…"/>` resolves `book` against `defineBookConfig({ siblingBooks })` and throws on an unknown book; `<XRef>` is in-book only — #96), `PocLayout` (v4.1.0+; wraps slot in a per-`kind` layout shell — 5 closed-union kinds; see `recipes/15-defining-styles.md`).
104
104
 
105
105
  **Provenance** (v4.8.0, any profile, **auto-injected by the chapter route — not author-imported**): per-chapter "How this was made" audit-trail block, rendered from the optional `provenance` frontmatter (`ai_tools`, `prompts_archive`, `decisions_log`, `audit_history`, `citation_backstop`). **Opt-out**: a chapter with no `provenance` shows a fallback ("Audit history not yet recorded"). Distinct from `AICollaborationDisclosure` (book-level, manual model+role disclosure). Repo-relative path fields render as `<code>`; only `http(s)` values link.
106
106
 
@@ -0,0 +1,32 @@
1
+ ---
2
+ /**
3
+ * BookLink — cross-book link to a sibling scaffold book (#96).
4
+ *
5
+ * Each scaffold book is a separate Astro app with its own `labels.json` and
6
+ * deploy origin, so `<XRef>` can't reach a sibling — a cross-book ref resolves
7
+ * against the wrong labels and dies. `<BookLink book="design" to="…">` resolves
8
+ * `book` against the consumer's `siblingBooks` registry
9
+ * (`defineBookConfig({ siblingBooks })`) — the single place to update when a
10
+ * sibling redeploys or extracts to its own repo. An unknown `book` throws at
11
+ * build (fail-loud, see `src/lib/book-link`), never a dead cross-origin link.
12
+ *
13
+ * `to` is a path within the sibling book (its router shape), e.g.
14
+ * `chapters/<slug>/#<id>`. Phase 1 builds the href; Phase 2 (deferred) will
15
+ * validate `to` against a vendored sibling `labels.json`.
16
+ *
17
+ * Usage:
18
+ * For design depth see
19
+ * <BookLink book="design" to="chapters/patterns/#layered">the layered pattern</BookLink>.
20
+ */
21
+ import bookConfig from 'virtual:book-scaffold/book-config';
22
+ import { resolveBookHref } from '../src/lib/book-link';
23
+
24
+ interface Props {
25
+ book: string;
26
+ to: string;
27
+ }
28
+
29
+ const { book, to } = Astro.props;
30
+ const href = resolveBookHref(bookConfig.siblingBooks, book, to);
31
+ ---
32
+ <a href={href} rel="external noopener" class="book-link"><slot /></a>
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AstroUserConfig, AstroIntegration } from 'astro';
2
- import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Y as volatilityLevels, h as ChaptersRenderer, r as academicParts, o as Style } from './types-CjOseOSG.js';
3
- export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, m as Provenance, R as ResearchPortfolioChapter, n as RouteToggles, S as StatusBadge, p as StyleInput, T as ToolsChapter, V as VolatilityBadge, q as academicChapterSchema, s as changeKinds, t as changelogSchema, u as chapterStatus, v as citationBackstops, w as composeStyles, x as courseNotesChapterSchema, y as defineProfile, z as defineStyle, D as minimalChapterSchema, E as normalizeFrontmatterConfig, G as patternCategories, H as patternsSchema, I as provenanceObject, J as provenanceSchema, K as researchPortfolioChapterSchema, L as resolvePreset, N as resolveProfile, O as sourceTiers, Q as sourceTiersResearch, U as sourcesSchema, W as toolSlugs, X as toolsChapterSchema } from './types-CjOseOSG.js';
2
+ import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, Y as volatilityLevels, h as ChaptersRenderer, r as academicParts, o as Style } from './types-BmBIV5qD.js';
3
+ export { A as AcademicChapter, B as BOOK_PRESETS, a as BOOK_PROFILES, b as BookConfigError, d as BookPreset, e as BookProfile, g as BookSchemasOptions, C as ChapterFor, i as CourseNotesChapter, F as FreshnessAffordance, j as FrontmatterRouteConfig, M as MinimalChapter, P as PartKey, k as PartialRouteToggles, l as ProfileDefinition, m as Provenance, R as ResearchPortfolioChapter, n as RouteToggles, S as StatusBadge, p as StyleInput, T as ToolsChapter, V as VolatilityBadge, q as academicChapterSchema, s as changeKinds, t as changelogSchema, u as chapterStatus, v as citationBackstops, w as composeStyles, x as courseNotesChapterSchema, y as defineProfile, z as defineStyle, D as minimalChapterSchema, E as normalizeFrontmatterConfig, G as patternCategories, H as patternsSchema, I as provenanceObject, J as provenanceSchema, K as researchPortfolioChapterSchema, L as resolvePreset, N as resolveProfile, O as sourceTiers, Q as sourceTiersResearch, U as sourcesSchema, W as toolSlugs, X as toolsChapterSchema } from './types-BmBIV5qD.js';
4
4
  import 'astro/zod';
5
5
 
6
6
  /**
@@ -320,6 +320,23 @@ declare function assertEnumProp<T extends string>(value: unknown, allowed: reado
320
320
  prop: string;
321
321
  }): T;
322
322
 
323
+ /**
324
+ * book-link — resolve a cross-book `<BookLink>` href from the consumer's
325
+ * sibling-book registry (#96).
326
+ *
327
+ * Each scaffold book is a separate Astro app with its own `labels.json` and
328
+ * deploy origin, so `<XRef>` can't reach a sibling book — a cross-book ref
329
+ * resolves against the wrong labels and dies. `<BookLink book to>` instead
330
+ * resolves `book` against a per-consumer registry of sibling base URLs
331
+ * (`defineBookConfig({ siblingBooks })`) — the single place to update when a
332
+ * sibling redeploys or extracts to its own repo. An unknown `book` THROWS
333
+ * rather than emitting a dead cross-origin link (fail-loud, like #109).
334
+ *
335
+ * Phase 1 (this): registry-backed href + fail-loud on unknown book. Phase 2
336
+ * (deferred): validate the `to` id against a vendored sibling `labels.json`.
337
+ */
338
+ declare function resolveBookHref(siblingBooks: Record<string, string> | null | undefined, book: string, to: string): string;
339
+
323
340
  /**
324
341
  * src/styles/built-in.ts — toolkit-shipped Styles, one per BookPreset (v4.0.0).
325
342
  *
@@ -421,4 +438,4 @@ type TipsConfigInput = Omit<TipsConfig, typeof TipsConfigBrand | '__tipsConfigVe
421
438
  */
422
439
  declare function defineTips(opts: TipsConfigInput): TipsConfig;
423
440
 
424
- export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type Freshness, type FreshnessStatus, KIND_LABEL, type ResolvedTheoremLabel, Style, THEOREM_KINDS, type TheoremKind, type TheoremLabelProps, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, assertEnumProp, bookScaffoldIntegration, buildGithubUrl, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, originUrlFromGitConfig, parseRepoSlug, researchPortfolioStyle, resolveGithubRepo, theoremLabel, toolsChaptersRenderer, toolsStyle, volatilityLevels };
441
+ export { ACADEMIC_PART_NAMES, BRANDON_PORTFOLIO_DEFAULT, BUILTIN_STYLES, BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, DEFAULT_GITHUB_BRANCH, type Freshness, type FreshnessStatus, KIND_LABEL, type ResolvedTheoremLabel, Style, THEOREM_KINDS, type TheoremKind, type TheoremLabelProps, type TipsConfig, type TipsConfigInput, UNKNOWN_PART_ORDINAL, type VolatilityLevel, academicChaptersRenderer, academicPartHeading, academicPartName, academicPartOrdinal, academicParts, academicStyle, assertEnumProp, bookScaffoldIntegration, buildGithubUrl, chapterSortKey, courseNotesStyle, defineBookConfig, defineMdxComponents, defineTips, fallbackChaptersRenderer, freshnessLabel, getFreshness, minimalStyle, originUrlFromGitConfig, parseRepoSlug, researchPortfolioStyle, resolveBookHref, resolveGithubRepo, theoremLabel, toolsChaptersRenderer, toolsStyle, volatilityLevels };
package/dist/index.mjs CHANGED
@@ -1028,7 +1028,9 @@ function bookScaffoldIntegration(opts) {
1028
1028
  seo,
1029
1029
  // v4.15.0 (#109): optional GitHub repo/branch override for CodeRef/CodeBlock.
1030
1030
  githubRepo,
1031
- githubBranch
1031
+ githubBranch,
1032
+ // v4.16.0 (#96): sibling-book registry for cross-book <BookLink>.
1033
+ siblingBooks
1032
1034
  } = opts;
1033
1035
  const def = PROFILES[profile];
1034
1036
  const fmNormalized = normalizeFrontmatterConfig(userOverrides.frontmatter);
@@ -1088,7 +1090,8 @@ function bookScaffoldIntegration(opts) {
1088
1090
  twitterHandle: seo?.twitterHandle ?? null
1089
1091
  },
1090
1092
  githubRepo: resolvedGithubRepo,
1091
- githubBranch: resolvedGithubBranch
1093
+ githubBranch: resolvedGithubBranch,
1094
+ siblingBooks: siblingBooks ?? {}
1092
1095
  })
1093
1096
  ],
1094
1097
  define: {
@@ -1224,7 +1227,9 @@ async function defineBookConfig(opts) {
1224
1227
  seo: opts.seo ? { ogImage: opts.seo.ogImage, twitterHandle: opts.seo.twitterHandle } : void 0,
1225
1228
  // v4.15.0 (#109): repo/branch override; integration auto-detects when undefined.
1226
1229
  githubRepo: opts.githubRepo,
1227
- githubBranch: opts.githubBranch
1230
+ githubBranch: opts.githubBranch,
1231
+ // v4.16.0 (#96): cross-book link registry.
1232
+ siblingBooks: opts.siblingBooks
1228
1233
  }),
1229
1234
  ...mergedExtraIntegrations
1230
1235
  ];
@@ -1269,6 +1274,8 @@ async function defineBookConfig(opts) {
1269
1274
  // v4.15.0: strip repo opts so they don't leak into AstroUserConfig.
1270
1275
  githubRepo: _githubRepo,
1271
1276
  githubBranch: _githubBranch,
1277
+ // v4.16.0: strip cross-book registry.
1278
+ siblingBooks: _siblingBooks,
1272
1279
  ...rest
1273
1280
  } = opts;
1274
1281
  void _styles;
@@ -1287,6 +1294,7 @@ async function defineBookConfig(opts) {
1287
1294
  void _seo;
1288
1295
  void _githubRepo;
1289
1296
  void _githubBranch;
1297
+ void _siblingBooks;
1290
1298
  const katexExternals = wantsKatex ? [] : ["remark-math", "rehype-katex", "katex"];
1291
1299
  const restVite = rest.vite ?? {};
1292
1300
  const restSsr = restVite.ssr ?? {};
@@ -1377,6 +1385,18 @@ function assertEnumProp(value, allowed, ctx) {
1377
1385
  );
1378
1386
  }
1379
1387
 
1388
+ // src/lib/book-link.ts
1389
+ function resolveBookHref(siblingBooks, book, to) {
1390
+ const base = siblingBooks?.[book];
1391
+ if (!base) {
1392
+ const known = siblingBooks ? Object.keys(siblingBooks) : [];
1393
+ throw new Error(
1394
+ `<BookLink book="${book}">: unknown sibling book. Register it in defineBookConfig({ siblingBooks: { "${book}": "https://\u2026" } })` + (known.length ? ` (known: ${known.join(", ")})` : "") + "."
1395
+ );
1396
+ }
1397
+ return `${base.replace(/\/+$/, "")}/${to.replace(/^\/+/, "")}`;
1398
+ }
1399
+
1380
1400
  // src/styles/built-in.ts
1381
1401
  var academicStyle = defineStyle({
1382
1402
  name: "academic",
@@ -1464,6 +1484,7 @@ export {
1464
1484
  provenanceSchema,
1465
1485
  researchPortfolioChapterSchema,
1466
1486
  researchPortfolioStyle,
1487
+ resolveBookHref,
1467
1488
  resolveGithubRepo,
1468
1489
  resolvePreset,
1469
1490
  resolveProfile,
package/dist/schemas.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineCollection } from 'astro:content';
2
- import { g as BookSchemasOptions } from './types-CjOseOSG.js';
2
+ import { g as BookSchemasOptions } from './types-BmBIV5qD.js';
3
3
  import 'astro';
4
4
  import 'astro/zod';
5
5
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@brandon_m_behring/book-scaffold-astro",
3
3
  "description": "Astro 6 + MDX toolkit for long-form technical books. Profile-aware (academic / tools / minimal); ships Tufte typography, KaTeX, BibTeX citations, Pagefind, Cloudflare Workers deploy. See PACKAGE_DESIGN.md for the API contract.",
4
- "version": "4.15.0",
4
+ "version": "4.16.0",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Brandon Behring",
@@ -43,6 +43,7 @@
43
43
  "./package.json": "./package.json",
44
44
  "./components/AICollaborationDisclosure.astro": "./components/AICollaborationDisclosure.astro",
45
45
  "./components/BlockedByCallout.astro": "./components/BlockedByCallout.astro",
46
+ "./components/BookLink.astro": "./components/BookLink.astro",
46
47
  "./components/CaseStudy.astro": "./components/CaseStudy.astro",
47
48
  "./components/ChapterHeader.astro": "./components/ChapterHeader.astro",
48
49
  "./components/ChapterNav.astro": "./components/ChapterNav.astro",
@@ -15,6 +15,8 @@
15
15
  * path exists + line in bounds.
16
16
  * 6. <Theorem> — has a resolvable kind= (or legacy type=); else it would
17
17
  * render an empty label and throw at build (#121).
18
+ * 7. <BookLink book="…" to="…"> (#96) — both props present, and book= is a
19
+ * key in the consumer's siblingBooks registry (best-effort).
18
20
  *
19
21
  * Run from the consumer's project root. Closes #8 (was resolving paths
20
22
  * from the package's own directory inside node_modules — false negatives
@@ -214,6 +216,9 @@ const RE_MD_LINK = /\[(?:[^\]]*)\]\((\/[^)\s#]+)(?:#[^)]*)?\)/g;
214
216
  // #121: a <Theorem> opening tag — capture its attributes to assert a
215
217
  // resolvable kind= (or legacy type=) is present.
216
218
  const RE_THEOREM = /<Theorem\b([^>]*)>/g;
219
+ // #96: a <BookLink> opening tag — assert book= + to= present, and (best-effort)
220
+ // that book= is a registered sibling.
221
+ const RE_BOOKLINK = /<BookLink\b([^>]*)>/g;
217
222
 
218
223
  async function fileExists(p) {
219
224
  try {
@@ -228,6 +233,25 @@ function lineOf(content, idx) {
228
233
  return content.slice(0, idx).split('\n').length;
229
234
  }
230
235
 
236
+ // #96: best-effort siblingBooks registry keys from astro.config.mjs, so the
237
+ // <BookLink> check can flag an unknown book= earlier than the component's
238
+ // build-time throw. null = couldn't determine → membership not checked (the
239
+ // component still fails loud at build).
240
+ let siblingBookKeys = null;
241
+ {
242
+ const astroConfigPath = resolve(ROOT, 'astro.config.mjs');
243
+ if (existsSync(astroConfigPath)) {
244
+ const block = readFileSync(astroConfigPath, 'utf8').match(/siblingBooks\s*:\s*\{([^}]*)\}/);
245
+ if (block) {
246
+ // Anchor each key to an entry boundary ({ , or start) so the `https:` in
247
+ // a URL value isn't mistaken for a key.
248
+ siblingBookKeys = new Set(
249
+ [...block[1].matchAll(/(?:^|[{,])\s*['"]?([\w-]+)['"]?\s*:/g)].map((x) => x[1]),
250
+ );
251
+ }
252
+ }
253
+ }
254
+
231
255
  // ===== Run all checks on each chapter =====
232
256
  for (const rel of chapterFiles) {
233
257
  const abs = join(CHAPTERS_DIR, rel);
@@ -296,6 +320,23 @@ for (const rel of chapterFiles) {
296
320
  );
297
321
  }
298
322
  }
323
+
324
+ // 7. BookLink (#96): structural (book= + to=) + best-effort registry membership.
325
+ for (const m of content.matchAll(RE_BOOKLINK)) {
326
+ const attrs = m[1];
327
+ const bookMatch = attrs.match(/\bbook=["']([^"']+)["']/);
328
+ if (!bookMatch || !/\bto=["']/.test(attrs)) {
329
+ fail(rel, lineOf(content, m.index), `<BookLink> requires both book="…" and to="…".`);
330
+ continue;
331
+ }
332
+ if (siblingBookKeys && !siblingBookKeys.has(bookMatch[1])) {
333
+ fail(
334
+ rel,
335
+ lineOf(content, m.index),
336
+ `<BookLink book="${bookMatch[1]}"> — not in defineBookConfig siblingBooks (${[...siblingBookKeys].join(', ') || 'none'}). Register it or fix the key.`,
337
+ );
338
+ }
339
+ }
299
340
  }
300
341
 
301
342
  // ===== v4.6.0 (issue #77): missing-prereq re-framing =====
@@ -0,0 +1,32 @@
1
+ /**
2
+ * book-link — resolve a cross-book `<BookLink>` href from the consumer's
3
+ * sibling-book registry (#96).
4
+ *
5
+ * Each scaffold book is a separate Astro app with its own `labels.json` and
6
+ * deploy origin, so `<XRef>` can't reach a sibling book — a cross-book ref
7
+ * resolves against the wrong labels and dies. `<BookLink book to>` instead
8
+ * resolves `book` against a per-consumer registry of sibling base URLs
9
+ * (`defineBookConfig({ siblingBooks })`) — the single place to update when a
10
+ * sibling redeploys or extracts to its own repo. An unknown `book` THROWS
11
+ * rather than emitting a dead cross-origin link (fail-loud, like #109).
12
+ *
13
+ * Phase 1 (this): registry-backed href + fail-loud on unknown book. Phase 2
14
+ * (deferred): validate the `to` id against a vendored sibling `labels.json`.
15
+ */
16
+ export function resolveBookHref(
17
+ siblingBooks: Record<string, string> | null | undefined,
18
+ book: string,
19
+ to: string,
20
+ ): string {
21
+ const base = siblingBooks?.[book];
22
+ if (!base) {
23
+ const known = siblingBooks ? Object.keys(siblingBooks) : [];
24
+ throw new Error(
25
+ `<BookLink book="${book}">: unknown sibling book. Register it in ` +
26
+ `defineBookConfig({ siblingBooks: { "${book}": "https://…" } })` +
27
+ (known.length ? ` (known: ${known.join(', ')})` : '') +
28
+ '.',
29
+ );
30
+ }
31
+ return `${base.replace(/\/+$/, '')}/${to.replace(/^\/+/, '')}`;
32
+ }