@brandon_m_behring/book-scaffold-astro 3.6.5 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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, E as volatilityLevels } from './types-DVbzso8w.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, h as CourseNotesChapter, M as MinimalChapter, P as ProfileDefinition, R as ResearchPortfolioChapter, i as RouteToggles, T as ToolsChapter, j as academicChapterSchema, k as academicParts, l as changeKinds, m as changelogSchema, n as chapterStatus, o as courseNotesChapterSchema, p as defineProfile, q as minimalChapterSchema, r as patternCategories, s as patternsSchema, t as researchPortfolioChapterSchema, u as resolvePreset, v as resolveProfile, w as sourceTiers, x as sourceTiersResearch, y as sourcesSchema, z as toolSlugs, D as toolsChapterSchema } from './types-DVbzso8w.js';
2
+ import { c as BookConfigOptions, f as BookScaffoldIntegrationOptions, H as volatilityLevels, h as ChaptersRenderer } from './types-C5CamFM0.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, M as MinimalChapter, P as PartKey, j as ProfileDefinition, R as ResearchPortfolioChapter, k as RouteToggles, S as StatusBadge, T as ToolsChapter, V as VolatilityBadge, l as academicChapterSchema, m as academicParts, n as changeKinds, o as changelogSchema, p as chapterStatus, q as courseNotesChapterSchema, r as defineProfile, s as minimalChapterSchema, t as patternCategories, u as patternsSchema, v as researchPortfolioChapterSchema, w as resolvePreset, x as resolveProfile, y as sourceTiers, z as sourceTiersResearch, D as sourcesSchema, E as toolSlugs, G as toolsChapterSchema } from './types-C5CamFM0.js';
4
4
  import 'astro/zod';
5
5
 
6
6
  declare function defineBookConfig(opts: BookConfigOptions): Promise<AstroUserConfig>;
@@ -103,4 +103,44 @@ declare function freshnessLabel(f: Freshness | null): string;
103
103
  */
104
104
  declare function chapterSortKey(data: Record<string, unknown>): number;
105
105
 
106
- export { BookConfigOptions, BookScaffoldIntegrationOptions, type Freshness, type FreshnessStatus, type VolatilityLevel, bookScaffoldIntegration, chapterSortKey, defineBookConfig, defineMdxComponents, freshnessLabel, getFreshness, volatilityLevels };
106
+ /**
107
+ * src/profiles/renderers/tools-chapters.ts — ChaptersRenderer implementation
108
+ * for the tools profile. Owns: numeric part grouping (with Appendix splitting
109
+ * at part >= 6), Chapter N numbering, volatility badge, freshness affordance
110
+ * from last_verified + volatility class, tools_compared tags.
111
+ *
112
+ * Mirrors the pre-v3.7.0 logic in pages/chapters.astro for tools-shape
113
+ * chapters — DOM output is intended to be byte-equivalent so the existing
114
+ * visual-regression baselines (package/tests/visual/fixture/) pass without
115
+ * recapture.
116
+ */
117
+
118
+ declare const toolsChaptersRenderer: ChaptersRenderer;
119
+
120
+ /**
121
+ * src/profiles/renderers/academic-chapters.ts — ChaptersRenderer implementation
122
+ * for the academic profile. Owns: string-enum part grouping (foundations,
123
+ * ssm-core, beyond-ssm, integration, synthesis), Week N numbering, status
124
+ * badge (7-state). No freshness/last_verified, no tools_compared.
125
+ *
126
+ * Mirrors the v3.5.2 academic branch of pages/chapters.astro — DOM output
127
+ * intended to match exactly so academic visual baselines stay stable.
128
+ */
129
+
130
+ declare const academicChaptersRenderer: ChaptersRenderer;
131
+
132
+ /**
133
+ * src/profiles/renderers/fallback-chapters.ts — ChaptersRenderer used by
134
+ * profiles that don't ship a dedicated renderer (minimal, course-notes,
135
+ * research-portfolio). Dispatches by field presence — exactly the v3.5.2
136
+ * logic that lived inline in pages/chapters.astro before #35.
137
+ *
138
+ * Safety net for shapes we haven't designed for explicitly. If a consumer
139
+ * opts a course-notes or research-portfolio book into `routes.chapters: true`,
140
+ * the fallback renders reasonably without crashing. Custom output for those
141
+ * profiles is a v4+ extension point (consumer-overridable renderer).
142
+ */
143
+
144
+ declare const fallbackChaptersRenderer: ChaptersRenderer;
145
+
146
+ export { BookConfigOptions, BookScaffoldIntegrationOptions, ChaptersRenderer, type Freshness, type FreshnessStatus, type VolatilityLevel, academicChaptersRenderer, bookScaffoldIntegration, chapterSortKey, defineBookConfig, defineMdxComponents, fallbackChaptersRenderer, freshnessLabel, getFreshness, toolsChaptersRenderer, volatilityLevels };
package/dist/index.mjs CHANGED
@@ -307,6 +307,63 @@ var patternsSchema = z.object({
307
307
  convergence_date: z.date().nullable().optional()
308
308
  });
309
309
 
310
+ // src/profiles/renderers/academic-chapters.ts
311
+ var ACADEMIC_PART_ORDINAL = {
312
+ foundations: 1,
313
+ "ssm-core": 2,
314
+ "beyond-ssm": 3,
315
+ integration: 4,
316
+ synthesis: 5
317
+ };
318
+ var UNKNOWN_PART_ORDINAL = 99;
319
+ function titleCase(part) {
320
+ return part.split("-").map((w) => w.length > 0 ? w.charAt(0).toUpperCase() + w.slice(1) : "").join(" ");
321
+ }
322
+ var academicChaptersRenderer = {
323
+ partKey(data) {
324
+ return data.part ?? "";
325
+ },
326
+ formatPartLabel(part) {
327
+ if (typeof part === "string" && part.length > 0) {
328
+ return titleCase(part);
329
+ }
330
+ return String(part);
331
+ },
332
+ isAppendix(_part) {
333
+ return false;
334
+ },
335
+ formatChapterNumber(data, _appendix) {
336
+ const week = data.week ?? 0;
337
+ return `Week ${week}`;
338
+ },
339
+ getToolsAttr(_data) {
340
+ return "cross-tool";
341
+ },
342
+ getVolatilityData(_data) {
343
+ return null;
344
+ },
345
+ getStatusData(data) {
346
+ const status = data.status;
347
+ if (!status) return null;
348
+ return { status, label: status };
349
+ },
350
+ getFreshnessData(_data, _now) {
351
+ return null;
352
+ },
353
+ getVerifiedDateLabel(_data) {
354
+ return null;
355
+ },
356
+ getToolsCompared(_data) {
357
+ return [];
358
+ },
359
+ sortKey(data) {
360
+ const partRaw = data.part;
361
+ const partOrdinal = typeof partRaw === "string" ? ACADEMIC_PART_ORDINAL[partRaw] ?? UNKNOWN_PART_ORDINAL : typeof partRaw === "number" ? partRaw : UNKNOWN_PART_ORDINAL;
362
+ const week = typeof data.week === "number" ? data.week : 0;
363
+ return partOrdinal * 1e3 + week;
364
+ }
365
+ };
366
+
310
367
  // src/profiles/academic.ts
311
368
  var academicProfile = defineProfile({
312
369
  name: "academic",
@@ -323,9 +380,104 @@ var academicProfile = defineProfile({
323
380
  // opt-in per book; see #7
324
381
  },
325
382
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
326
- katex: true
383
+ katex: true,
384
+ chaptersRenderer: academicChaptersRenderer
385
+ // v3.7.0 (#35) — owns /chapters semantics if consumer opts in via routes.chapters
327
386
  });
328
387
 
388
+ // src/lib/freshness.ts
389
+ var THRESHOLDS = {
390
+ "stable-principle": 365,
391
+ "architectural-pattern": 180,
392
+ "feature-surface": 90
393
+ };
394
+ var MS_PER_DAY = 1e3 * 60 * 60 * 24;
395
+ function getFreshness(lastVerified, volatility, now = /* @__PURE__ */ new Date()) {
396
+ if (!(lastVerified instanceof Date)) return null;
397
+ const thresholdDays = THRESHOLDS[volatility];
398
+ const daysOld = Math.floor((now.getTime() - lastVerified.getTime()) / MS_PER_DAY);
399
+ const daysUntil = thresholdDays - daysOld;
400
+ let status;
401
+ if (daysOld < thresholdDays * 0.75) {
402
+ status = "fresh";
403
+ } else if (daysOld < thresholdDays) {
404
+ status = "verify-soon";
405
+ } else {
406
+ status = "stale";
407
+ }
408
+ return { status, daysOld, thresholdDays, daysUntil };
409
+ }
410
+ function freshnessLabel(f) {
411
+ if (f === null) return "Verification status unknown";
412
+ switch (f.status) {
413
+ case "fresh":
414
+ return `Fresh (${f.daysOld}d old; verify within ${f.daysUntil}d)`;
415
+ case "verify-soon":
416
+ return `Verify soon (${f.daysOld}d old; ${f.daysUntil}d until stale)`;
417
+ case "stale":
418
+ return `Stale (${f.daysOld}d old; ${Math.abs(f.daysUntil)}d past threshold)`;
419
+ }
420
+ }
421
+
422
+ // src/profiles/renderers/tools-chapters.ts
423
+ function formatDate(d) {
424
+ return d.toISOString().slice(0, 10);
425
+ }
426
+ var toolsChaptersRenderer = {
427
+ partKey(data) {
428
+ return data.part ?? 0;
429
+ },
430
+ formatPartLabel(part) {
431
+ if (typeof part === "number") {
432
+ return part >= 6 ? "Appendices" : `Part ${part}`;
433
+ }
434
+ return String(part);
435
+ },
436
+ isAppendix(part) {
437
+ return typeof part === "number" && part >= 6;
438
+ },
439
+ formatChapterNumber(data, appendix) {
440
+ const chapter = data.chapter ?? 0;
441
+ if (appendix) {
442
+ return `Appendix ${String.fromCharCode(64 + chapter).toLowerCase()}`;
443
+ }
444
+ return `Chapter ${chapter}`;
445
+ },
446
+ getToolsAttr(data) {
447
+ const tools = data.tools_compared ?? [];
448
+ return tools.join(" ");
449
+ },
450
+ getVolatilityData(data) {
451
+ const level = data.volatility;
452
+ if (!level) return null;
453
+ return { level, label: level };
454
+ },
455
+ getStatusData(_data) {
456
+ return null;
457
+ },
458
+ getFreshnessData(data, now) {
459
+ const lastVerified = data.last_verified;
460
+ const volatility = data.volatility;
461
+ if (!lastVerified || !volatility) return null;
462
+ const f = getFreshness(lastVerified, volatility, now);
463
+ if (!f) return null;
464
+ return { status: f.status, label: freshnessLabel(f) };
465
+ },
466
+ getVerifiedDateLabel(data) {
467
+ const lastVerified = data.last_verified;
468
+ if (!(lastVerified instanceof Date)) return null;
469
+ return `verified ${formatDate(lastVerified)}`;
470
+ },
471
+ getToolsCompared(data) {
472
+ return (data.tools_compared ?? []).slice();
473
+ },
474
+ sortKey(data) {
475
+ const part = data.part ?? 0;
476
+ const chapter = data.chapter ?? 0;
477
+ return part * 1e3 + chapter;
478
+ }
479
+ };
480
+
329
481
  // src/profiles/tools.ts
330
482
  var toolsProfile = defineProfile({
331
483
  name: "tools",
@@ -350,9 +502,62 @@ var toolsProfile = defineProfile({
350
502
  "print.css",
351
503
  "convergence.css",
352
504
  "tool-filter.css"
353
- ]
505
+ ],
506
+ chaptersRenderer: toolsChaptersRenderer
507
+ // v3.7.0 (#35) — owns /chapters semantics for tools shape
354
508
  });
355
509
 
510
+ // src/profiles/renderers/fallback-chapters.ts
511
+ function isToolsShape(data) {
512
+ return "volatility" in data && "chapter" in data;
513
+ }
514
+ function isAcademicShape(data) {
515
+ return "week" in data && "status" in data;
516
+ }
517
+ function dispatch(data) {
518
+ if (isToolsShape(data)) return toolsChaptersRenderer;
519
+ if (isAcademicShape(data)) return academicChaptersRenderer;
520
+ return toolsChaptersRenderer;
521
+ }
522
+ var fallbackChaptersRenderer = {
523
+ partKey(data) {
524
+ return data.part ?? 0;
525
+ },
526
+ formatPartLabel(part) {
527
+ if (typeof part === "number") {
528
+ return part >= 6 ? "Appendices" : `Part ${part}`;
529
+ }
530
+ return dispatch({ part }).formatPartLabel(part);
531
+ },
532
+ isAppendix(part) {
533
+ return typeof part === "number" && part >= 6;
534
+ },
535
+ formatChapterNumber(data, appendix) {
536
+ return dispatch(data).formatChapterNumber(data, appendix);
537
+ },
538
+ getToolsAttr(data) {
539
+ return dispatch(data).getToolsAttr(data);
540
+ },
541
+ getVolatilityData(data) {
542
+ return dispatch(data).getVolatilityData(data);
543
+ },
544
+ getStatusData(data) {
545
+ return dispatch(data).getStatusData(data);
546
+ },
547
+ getFreshnessData(data, now) {
548
+ return dispatch(data).getFreshnessData(data, now);
549
+ },
550
+ getVerifiedDateLabel(data) {
551
+ return dispatch(data).getVerifiedDateLabel(data);
552
+ },
553
+ getToolsCompared(data) {
554
+ return dispatch(data).getToolsCompared(data);
555
+ },
556
+ sortKey(data) {
557
+ return dispatch(data).sortKey(data);
558
+ }
559
+ };
560
+
356
561
  // src/profiles/minimal.ts
357
562
  var minimalProfile = defineProfile({
358
563
  name: "minimal",
@@ -366,7 +571,9 @@ var minimalProfile = defineProfile({
366
571
  frontmatter: false
367
572
  // opt-in per book; see #7
368
573
  },
369
- styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
574
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
575
+ // v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
576
+ chaptersRenderer: fallbackChaptersRenderer
370
577
  });
371
578
 
372
579
  // src/profiles/course-notes.ts
@@ -383,7 +590,9 @@ var courseNotesProfile = defineProfile({
383
590
  frontmatter: false
384
591
  // opt-in per book; see #7
385
592
  },
386
- styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
593
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
594
+ // v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
595
+ chaptersRenderer: fallbackChaptersRenderer
387
596
  });
388
597
 
389
598
  // src/profiles/research-portfolio.ts
@@ -402,8 +611,10 @@ var researchPortfolioProfile = defineProfile({
402
611
  // portfolios universally need title/disclosure/banner pages
403
612
  },
404
613
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
405
- katex: true
614
+ katex: true,
406
615
  // math is common in research content
616
+ // v3.7.0 (#35): portfolio schema is a union of academic + tools shapes — fallback renderer dispatches per chapter via field presence
617
+ chaptersRenderer: fallbackChaptersRenderer
407
618
  });
408
619
 
409
620
  // src/profiles/index.ts
@@ -576,9 +787,10 @@ function bookScaffoldIntegration(opts) {
576
787
  // src/config.ts
577
788
  async function defineBookConfig(opts) {
578
789
  const profile = resolvePreset(opts.preset, opts.profile);
790
+ const wantsKatex = PROFILES[profile]?.katex === true;
579
791
  const remarkPlugins = [];
580
792
  const rehypePlugins = [];
581
- if (profile === "academic") {
793
+ if (wantsKatex) {
582
794
  const { default: remarkMath } = await import(
583
795
  /* @vite-ignore */
584
796
  "remark-math"
@@ -651,7 +863,7 @@ async function defineBookConfig(opts) {
651
863
  void _extraStyles;
652
864
  void _markdown;
653
865
  void _katexMacros;
654
- const katexExternals = profile === "academic" ? [] : ["remark-math", "rehype-katex", "katex"];
866
+ const katexExternals = wantsKatex ? [] : ["remark-math", "rehype-katex", "katex"];
655
867
  const config = {
656
868
  ...rest,
657
869
  integrations,
@@ -668,52 +880,18 @@ async function defineBookConfig(opts) {
668
880
  return config;
669
881
  }
670
882
 
671
- // src/lib/freshness.ts
672
- var THRESHOLDS = {
673
- "stable-principle": 365,
674
- "architectural-pattern": 180,
675
- "feature-surface": 90
676
- };
677
- var MS_PER_DAY = 1e3 * 60 * 60 * 24;
678
- function getFreshness(lastVerified, volatility, now = /* @__PURE__ */ new Date()) {
679
- if (!(lastVerified instanceof Date)) return null;
680
- const thresholdDays = THRESHOLDS[volatility];
681
- const daysOld = Math.floor((now.getTime() - lastVerified.getTime()) / MS_PER_DAY);
682
- const daysUntil = thresholdDays - daysOld;
683
- let status;
684
- if (daysOld < thresholdDays * 0.75) {
685
- status = "fresh";
686
- } else if (daysOld < thresholdDays) {
687
- status = "verify-soon";
688
- } else {
689
- status = "stale";
690
- }
691
- return { status, daysOld, thresholdDays, daysUntil };
692
- }
693
- function freshnessLabel(f) {
694
- if (f === null) return "Verification status unknown";
695
- switch (f.status) {
696
- case "fresh":
697
- return `Fresh (${f.daysOld}d old; verify within ${f.daysUntil}d)`;
698
- case "verify-soon":
699
- return `Verify soon (${f.daysOld}d old; ${f.daysUntil}d until stale)`;
700
- case "stale":
701
- return `Stale (${f.daysOld}d old; ${Math.abs(f.daysUntil)}d past threshold)`;
702
- }
703
- }
704
-
705
883
  // src/lib/chapter-sort.ts
706
- var ACADEMIC_PART_ORDINAL = {
884
+ var ACADEMIC_PART_ORDINAL2 = {
707
885
  foundations: 1,
708
886
  "ssm-core": 2,
709
887
  "beyond-ssm": 3,
710
888
  integration: 4,
711
889
  synthesis: 5
712
890
  };
713
- var UNKNOWN_PART_ORDINAL = 99;
891
+ var UNKNOWN_PART_ORDINAL2 = 99;
714
892
  function chapterSortKey(data) {
715
893
  const partRaw = data.part;
716
- const partOrdinal = typeof partRaw === "number" ? partRaw : typeof partRaw === "string" ? ACADEMIC_PART_ORDINAL[partRaw] ?? UNKNOWN_PART_ORDINAL : UNKNOWN_PART_ORDINAL;
894
+ const partOrdinal = typeof partRaw === "number" ? partRaw : typeof partRaw === "string" ? ACADEMIC_PART_ORDINAL2[partRaw] ?? UNKNOWN_PART_ORDINAL2 : UNKNOWN_PART_ORDINAL2;
717
895
  const within = typeof data.chapter === "number" ? data.chapter : typeof data.week === "number" ? data.week : 0;
718
896
  return partOrdinal * 1e3 + within;
719
897
  }
@@ -722,6 +900,7 @@ export {
722
900
  BOOK_PROFILES,
723
901
  BookConfigError,
724
902
  academicChapterSchema,
903
+ academicChaptersRenderer,
725
904
  academicParts,
726
905
  bookScaffoldIntegration,
727
906
  changeKinds,
@@ -732,6 +911,7 @@ export {
732
911
  defineBookConfig,
733
912
  defineMdxComponents,
734
913
  defineProfile,
914
+ fallbackChaptersRenderer,
735
915
  freshnessLabel,
736
916
  getFreshness,
737
917
  minimalChapterSchema,
@@ -745,5 +925,6 @@ export {
745
925
  sourcesSchema,
746
926
  toolSlugs,
747
927
  toolsChapterSchema,
928
+ toolsChaptersRenderer,
748
929
  volatilityLevels
749
930
  };
package/dist/schemas.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineCollection } from 'astro:content';
2
- import { g as BookSchemasOptions } from './types-DVbzso8w.js';
2
+ import { g as BookSchemasOptions } from './types-C5CamFM0.js';
3
3
  import 'astro';
4
4
  import 'astro/zod';
5
5
 
package/dist/schemas.mjs CHANGED
@@ -192,6 +192,63 @@ var patternsSchema = z.object({
192
192
  convergence_date: z.date().nullable().optional()
193
193
  });
194
194
 
195
+ // src/profiles/renderers/academic-chapters.ts
196
+ var ACADEMIC_PART_ORDINAL = {
197
+ foundations: 1,
198
+ "ssm-core": 2,
199
+ "beyond-ssm": 3,
200
+ integration: 4,
201
+ synthesis: 5
202
+ };
203
+ var UNKNOWN_PART_ORDINAL = 99;
204
+ function titleCase(part) {
205
+ return part.split("-").map((w) => w.length > 0 ? w.charAt(0).toUpperCase() + w.slice(1) : "").join(" ");
206
+ }
207
+ var academicChaptersRenderer = {
208
+ partKey(data) {
209
+ return data.part ?? "";
210
+ },
211
+ formatPartLabel(part) {
212
+ if (typeof part === "string" && part.length > 0) {
213
+ return titleCase(part);
214
+ }
215
+ return String(part);
216
+ },
217
+ isAppendix(_part) {
218
+ return false;
219
+ },
220
+ formatChapterNumber(data, _appendix) {
221
+ const week = data.week ?? 0;
222
+ return `Week ${week}`;
223
+ },
224
+ getToolsAttr(_data) {
225
+ return "cross-tool";
226
+ },
227
+ getVolatilityData(_data) {
228
+ return null;
229
+ },
230
+ getStatusData(data) {
231
+ const status = data.status;
232
+ if (!status) return null;
233
+ return { status, label: status };
234
+ },
235
+ getFreshnessData(_data, _now) {
236
+ return null;
237
+ },
238
+ getVerifiedDateLabel(_data) {
239
+ return null;
240
+ },
241
+ getToolsCompared(_data) {
242
+ return [];
243
+ },
244
+ sortKey(data) {
245
+ const partRaw = data.part;
246
+ const partOrdinal = typeof partRaw === "string" ? ACADEMIC_PART_ORDINAL[partRaw] ?? UNKNOWN_PART_ORDINAL : typeof partRaw === "number" ? partRaw : UNKNOWN_PART_ORDINAL;
247
+ const week = typeof data.week === "number" ? data.week : 0;
248
+ return partOrdinal * 1e3 + week;
249
+ }
250
+ };
251
+
195
252
  // src/profiles/academic.ts
196
253
  var academicProfile = defineProfile({
197
254
  name: "academic",
@@ -208,9 +265,104 @@ var academicProfile = defineProfile({
208
265
  // opt-in per book; see #7
209
266
  },
210
267
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
211
- katex: true
268
+ katex: true,
269
+ chaptersRenderer: academicChaptersRenderer
270
+ // v3.7.0 (#35) — owns /chapters semantics if consumer opts in via routes.chapters
212
271
  });
213
272
 
273
+ // src/lib/freshness.ts
274
+ var THRESHOLDS = {
275
+ "stable-principle": 365,
276
+ "architectural-pattern": 180,
277
+ "feature-surface": 90
278
+ };
279
+ var MS_PER_DAY = 1e3 * 60 * 60 * 24;
280
+ function getFreshness(lastVerified, volatility, now = /* @__PURE__ */ new Date()) {
281
+ if (!(lastVerified instanceof Date)) return null;
282
+ const thresholdDays = THRESHOLDS[volatility];
283
+ const daysOld = Math.floor((now.getTime() - lastVerified.getTime()) / MS_PER_DAY);
284
+ const daysUntil = thresholdDays - daysOld;
285
+ let status;
286
+ if (daysOld < thresholdDays * 0.75) {
287
+ status = "fresh";
288
+ } else if (daysOld < thresholdDays) {
289
+ status = "verify-soon";
290
+ } else {
291
+ status = "stale";
292
+ }
293
+ return { status, daysOld, thresholdDays, daysUntil };
294
+ }
295
+ function freshnessLabel(f) {
296
+ if (f === null) return "Verification status unknown";
297
+ switch (f.status) {
298
+ case "fresh":
299
+ return `Fresh (${f.daysOld}d old; verify within ${f.daysUntil}d)`;
300
+ case "verify-soon":
301
+ return `Verify soon (${f.daysOld}d old; ${f.daysUntil}d until stale)`;
302
+ case "stale":
303
+ return `Stale (${f.daysOld}d old; ${Math.abs(f.daysUntil)}d past threshold)`;
304
+ }
305
+ }
306
+
307
+ // src/profiles/renderers/tools-chapters.ts
308
+ function formatDate(d) {
309
+ return d.toISOString().slice(0, 10);
310
+ }
311
+ var toolsChaptersRenderer = {
312
+ partKey(data) {
313
+ return data.part ?? 0;
314
+ },
315
+ formatPartLabel(part) {
316
+ if (typeof part === "number") {
317
+ return part >= 6 ? "Appendices" : `Part ${part}`;
318
+ }
319
+ return String(part);
320
+ },
321
+ isAppendix(part) {
322
+ return typeof part === "number" && part >= 6;
323
+ },
324
+ formatChapterNumber(data, appendix) {
325
+ const chapter = data.chapter ?? 0;
326
+ if (appendix) {
327
+ return `Appendix ${String.fromCharCode(64 + chapter).toLowerCase()}`;
328
+ }
329
+ return `Chapter ${chapter}`;
330
+ },
331
+ getToolsAttr(data) {
332
+ const tools = data.tools_compared ?? [];
333
+ return tools.join(" ");
334
+ },
335
+ getVolatilityData(data) {
336
+ const level = data.volatility;
337
+ if (!level) return null;
338
+ return { level, label: level };
339
+ },
340
+ getStatusData(_data) {
341
+ return null;
342
+ },
343
+ getFreshnessData(data, now) {
344
+ const lastVerified = data.last_verified;
345
+ const volatility = data.volatility;
346
+ if (!lastVerified || !volatility) return null;
347
+ const f = getFreshness(lastVerified, volatility, now);
348
+ if (!f) return null;
349
+ return { status: f.status, label: freshnessLabel(f) };
350
+ },
351
+ getVerifiedDateLabel(data) {
352
+ const lastVerified = data.last_verified;
353
+ if (!(lastVerified instanceof Date)) return null;
354
+ return `verified ${formatDate(lastVerified)}`;
355
+ },
356
+ getToolsCompared(data) {
357
+ return (data.tools_compared ?? []).slice();
358
+ },
359
+ sortKey(data) {
360
+ const part = data.part ?? 0;
361
+ const chapter = data.chapter ?? 0;
362
+ return part * 1e3 + chapter;
363
+ }
364
+ };
365
+
214
366
  // src/profiles/tools.ts
215
367
  var toolsProfile = defineProfile({
216
368
  name: "tools",
@@ -235,9 +387,62 @@ var toolsProfile = defineProfile({
235
387
  "print.css",
236
388
  "convergence.css",
237
389
  "tool-filter.css"
238
- ]
390
+ ],
391
+ chaptersRenderer: toolsChaptersRenderer
392
+ // v3.7.0 (#35) — owns /chapters semantics for tools shape
239
393
  });
240
394
 
395
+ // src/profiles/renderers/fallback-chapters.ts
396
+ function isToolsShape(data) {
397
+ return "volatility" in data && "chapter" in data;
398
+ }
399
+ function isAcademicShape(data) {
400
+ return "week" in data && "status" in data;
401
+ }
402
+ function dispatch(data) {
403
+ if (isToolsShape(data)) return toolsChaptersRenderer;
404
+ if (isAcademicShape(data)) return academicChaptersRenderer;
405
+ return toolsChaptersRenderer;
406
+ }
407
+ var fallbackChaptersRenderer = {
408
+ partKey(data) {
409
+ return data.part ?? 0;
410
+ },
411
+ formatPartLabel(part) {
412
+ if (typeof part === "number") {
413
+ return part >= 6 ? "Appendices" : `Part ${part}`;
414
+ }
415
+ return dispatch({ part }).formatPartLabel(part);
416
+ },
417
+ isAppendix(part) {
418
+ return typeof part === "number" && part >= 6;
419
+ },
420
+ formatChapterNumber(data, appendix) {
421
+ return dispatch(data).formatChapterNumber(data, appendix);
422
+ },
423
+ getToolsAttr(data) {
424
+ return dispatch(data).getToolsAttr(data);
425
+ },
426
+ getVolatilityData(data) {
427
+ return dispatch(data).getVolatilityData(data);
428
+ },
429
+ getStatusData(data) {
430
+ return dispatch(data).getStatusData(data);
431
+ },
432
+ getFreshnessData(data, now) {
433
+ return dispatch(data).getFreshnessData(data, now);
434
+ },
435
+ getVerifiedDateLabel(data) {
436
+ return dispatch(data).getVerifiedDateLabel(data);
437
+ },
438
+ getToolsCompared(data) {
439
+ return dispatch(data).getToolsCompared(data);
440
+ },
441
+ sortKey(data) {
442
+ return dispatch(data).sortKey(data);
443
+ }
444
+ };
445
+
241
446
  // src/profiles/minimal.ts
242
447
  var minimalProfile = defineProfile({
243
448
  name: "minimal",
@@ -251,7 +456,9 @@ var minimalProfile = defineProfile({
251
456
  frontmatter: false
252
457
  // opt-in per book; see #7
253
458
  },
254
- styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
459
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
460
+ // v3.7.0 (#35): minimal aliases tools schema; fallback renderer field-dispatches if a consumer opts into routes.chapters
461
+ chaptersRenderer: fallbackChaptersRenderer
255
462
  });
256
463
 
257
464
  // src/profiles/course-notes.ts
@@ -268,7 +475,9 @@ var courseNotesProfile = defineProfile({
268
475
  frontmatter: false
269
476
  // opt-in per book; see #7
270
477
  },
271
- styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"]
478
+ styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
479
+ // v3.7.0 (#35): course-notes schema has tools-style fields (chapter, volatility, sources) — fallback renderer dispatches via tools renderer
480
+ chaptersRenderer: fallbackChaptersRenderer
272
481
  });
273
482
 
274
483
  // src/profiles/research-portfolio.ts
@@ -287,8 +496,10 @@ var researchPortfolioProfile = defineProfile({
287
496
  // portfolios universally need title/disclosure/banner pages
288
497
  },
289
498
  styles: ["tokens.css", "layout.css", "callouts.css", "chapter.css", "typography.css", "print.css"],
290
- katex: true
499
+ katex: true,
291
500
  // math is common in research content
501
+ // v3.7.0 (#35): portfolio schema is a union of academic + tools shapes — fallback renderer dispatches per chapter via field presence
502
+ chaptersRenderer: fallbackChaptersRenderer
292
503
  });
293
504
 
294
505
  // src/profiles/index.ts