explico 0.1.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.
Files changed (113) hide show
  1. package/README.md +108 -0
  2. package/dist/analytics/AnalyticsEmitter.d.ts +22 -0
  3. package/dist/analytics/AnalyticsEmitter.js +5 -0
  4. package/dist/compiler/cli.d.ts +189 -0
  5. package/dist/compiler/cli.js +552 -0
  6. package/dist/compiler/collections.d.ts +70 -0
  7. package/dist/compiler/collections.js +383 -0
  8. package/dist/compiler/course-config.d.ts +31 -0
  9. package/dist/compiler/course-config.js +0 -0
  10. package/dist/compiler/lesson-derivation.d.ts +89 -0
  11. package/dist/compiler/lesson-derivation.js +91 -0
  12. package/dist/compiler/manifest.d.ts +178 -0
  13. package/dist/compiler/manifest.js +345 -0
  14. package/dist/compiler/resolution-report.d.ts +45 -0
  15. package/dist/compiler/resolution-report.js +97 -0
  16. package/dist/curriculum/validation.d.ts +76 -0
  17. package/dist/curriculum/validation.js +320 -0
  18. package/dist/progress/ProgressRepository.d.ts +53 -0
  19. package/dist/progress/ProgressRepository.js +12 -0
  20. package/dist/reference/base-library.mjs +27 -0
  21. package/dist/reference/citation-format.mjs +133 -0
  22. package/dist/reference/consistency.d.ts +64 -0
  23. package/dist/reference/consistency.js +234 -0
  24. package/dist/reference/curriculum-alignment.d.ts +7 -0
  25. package/dist/reference/curriculum-alignment.js +166 -0
  26. package/dist/reference/equations-validate.d.ts +50 -0
  27. package/dist/reference/equations-validate.js +90 -0
  28. package/dist/reference/equations.mjs +144 -0
  29. package/dist/reference/gate-math.d.ts +41 -0
  30. package/dist/reference/gate-math.js +151 -0
  31. package/dist/reference/katex-options.mjs +91 -0
  32. package/dist/reference/lab-math-scope.d.ts +21 -0
  33. package/dist/reference/lab-math-scope.js +28 -0
  34. package/dist/reference/label.d.ts +11 -0
  35. package/dist/reference/label.js +38 -0
  36. package/dist/reference/lint-ignore.d.ts +32 -0
  37. package/dist/reference/lint-ignore.js +39 -0
  38. package/dist/reference/math-glyphs.mjs +830 -0
  39. package/dist/reference/prose.d.ts +15 -0
  40. package/dist/reference/prose.js +12 -0
  41. package/dist/reference/references.d.ts +17 -0
  42. package/dist/reference/references.js +42 -0
  43. package/dist/reference/registry.d.ts +6 -0
  44. package/dist/reference/registry.js +548 -0
  45. package/dist/reference/rehype-fail-katex-errors.mjs +55 -0
  46. package/dist/reference/remark-citation.mjs +0 -0
  47. package/dist/reference/remark-notation.mjs +753 -0
  48. package/dist/reference/render-lab-math.d.ts +21 -0
  49. package/dist/reference/render-lab-math.js +58 -0
  50. package/dist/reference/types.d.ts +151 -0
  51. package/dist/reference/types.js +0 -0
  52. package/dist/reference/units-vocab.d.ts +26 -0
  53. package/dist/reference/units-vocab.js +119 -0
  54. package/dist/session/PreferenceStore.d.ts +28 -0
  55. package/dist/session/PreferenceStore.js +30 -0
  56. package/dist/session/user-context.d.ts +7 -0
  57. package/dist/session/user-context.js +10 -0
  58. package/dist/session/user.d.ts +21 -0
  59. package/dist/session/user.js +8 -0
  60. package/package.json +53 -0
  61. package/src/analytics/AnalyticsEmitter.ts +23 -0
  62. package/src/compiler/cli.ts +866 -0
  63. package/src/compiler/collections.ts +554 -0
  64. package/src/compiler/course-config.ts +32 -0
  65. package/src/compiler/lesson-derivation.ts +186 -0
  66. package/src/compiler/manifest.ts +601 -0
  67. package/src/compiler/resolution-report.ts +150 -0
  68. package/src/components/CurriculumMap.astro +208 -0
  69. package/src/components/HoverPanel.astro +67 -0
  70. package/src/components/assessments/AssessmentRunner.tsx +320 -0
  71. package/src/components/assessments/AssessmentSet.astro +54 -0
  72. package/src/components/citation/CitationLayer.astro +173 -0
  73. package/src/components/examples/CompactExample.astro +34 -0
  74. package/src/components/examples/CompactExamples.astro +253 -0
  75. package/src/components/hover-panel.ts +273 -0
  76. package/src/components/notation/NotationGlossary.astro +412 -0
  77. package/src/components/notation/NotationLayer.astro +594 -0
  78. package/src/components/notation/NotationMath.astro +24 -0
  79. package/src/components/notation/SafeMathNode.astro +23 -0
  80. package/src/components/notation/render-notation-math.ts +223 -0
  81. package/src/components/starlight/EquationEnhancer.astro +72 -0
  82. package/src/components/starlight/LayoutHeader.astro +102 -0
  83. package/src/components/starlight/LayoutPageSidebar.astro +33 -0
  84. package/src/components/starlight/LayoutSidebar.astro +29 -0
  85. package/src/components/starlight/LessonFooter.astro +58 -0
  86. package/src/content-config.ts +259 -0
  87. package/src/curriculum/validation.ts +481 -0
  88. package/src/progress/ProgressRepository.ts +64 -0
  89. package/src/reference/base-library.mjs +27 -0
  90. package/src/reference/citation-format.mjs +133 -0
  91. package/src/reference/consistency.ts +375 -0
  92. package/src/reference/curriculum-alignment.ts +228 -0
  93. package/src/reference/equations-validate.ts +167 -0
  94. package/src/reference/equations.mjs +144 -0
  95. package/src/reference/gate-math.ts +250 -0
  96. package/src/reference/katex-options.mjs +91 -0
  97. package/src/reference/lab-math-scope.ts +54 -0
  98. package/src/reference/label.ts +47 -0
  99. package/src/reference/lint-ignore.ts +75 -0
  100. package/src/reference/math-glyphs.mjs +830 -0
  101. package/src/reference/prose.ts +21 -0
  102. package/src/reference/references.ts +82 -0
  103. package/src/reference/registry.ts +723 -0
  104. package/src/reference/rehype-fail-katex-errors.mjs +55 -0
  105. package/src/reference/remark-citation.mjs +0 -0
  106. package/src/reference/remark-notation.mjs +753 -0
  107. package/src/reference/render-lab-math.ts +99 -0
  108. package/src/reference/types.ts +182 -0
  109. package/src/reference/units-vocab.ts +143 -0
  110. package/src/session/PreferenceStore.ts +54 -0
  111. package/src/session/user-context.ts +18 -0
  112. package/src/session/user.ts +24 -0
  113. package/src/styles/global.css +763 -0
@@ -0,0 +1,99 @@
1
+ import katex from 'katex';
2
+ import { resolveMathGlyphs } from './math-glyphs.mjs';
3
+ import { createNotationKatexOptions } from './katex-options.mjs';
4
+
5
+ export interface LabMathScopeDefinition {
6
+ readonly key: string;
7
+ readonly notation: string;
8
+ }
9
+
10
+ export interface RenderedLabMath {
11
+ /** KaTeX HTML+MathML with `data-notation-key` and `data-lab-slot` spans. */
12
+ readonly html: string;
13
+ /** Slot names, in template order, the island must supply at runtime. */
14
+ readonly slots: readonly string[];
15
+ /** Notation keys the template resolved to, for diagnostics. */
16
+ readonly keys: readonly string[];
17
+ }
18
+
19
+ interface BindResult {
20
+ latex: string;
21
+ bindings: ReadonlyArray<{ key: string }>;
22
+ unresolved: ReadonlyArray<{ token: string; start: number }>;
23
+ }
24
+
25
+ // `\slot{name}{fallback}` — `name` is a lab-slot id, `fallback` may contain one
26
+ // level of nested braces (for KaTeX digit grouping such as `1{,}000`).
27
+ const SLOT_PATTERN = /\\slot\{([a-z][a-z0-9]*)\}\{((?:[^{}]|\{[^{}]*\})*)\}/g;
28
+
29
+ // A decimal literal is invisible to the notation binder (a number is never an
30
+ // identifier), survives `injectScopeBindings` verbatim, and will not collide
31
+ // with authored content.
32
+ const sentinelFor = (index: number): string => `909909${index}.909909`;
33
+
34
+ /**
35
+ * Compile a developer-authored lab math template the same way lesson `$$`
36
+ * math is compiled: every identifier must resolve to a notation key in the
37
+ * page's scope or this throws. Slots are set aside during binding and restored
38
+ * as inert `data-lab-slot` spans the interactive island rewrites at runtime.
39
+ */
40
+ export function renderLabMath(
41
+ template: string,
42
+ scope: readonly LabMathScopeDefinition[],
43
+ ): RenderedLabMath {
44
+ const slots: Array<{ name: string; fallback: string }> = [];
45
+ const forBinding = template.replace(
46
+ SLOT_PATTERN,
47
+ (_match: string, name: string, fallback: string) => {
48
+ const sentinel = sentinelFor(slots.length);
49
+ slots.push({ name, fallback });
50
+ return ` ${sentinel} `;
51
+ },
52
+ );
53
+
54
+ let result: BindResult;
55
+ try {
56
+ result = resolveMathGlyphs(forBinding, [...scope]) as BindResult;
57
+ } catch (error) {
58
+ throw new Error(
59
+ `Lab math failed to compile: ${
60
+ error instanceof Error ? error.message : String(error)
61
+ }\n template: ${template.trim()}`,
62
+ );
63
+ }
64
+
65
+ if (result.unresolved.length > 0) {
66
+ const list = result.unresolved.map(({ token }) => `"${token}"`).join(', ');
67
+ throw new Error(
68
+ `Lab math uses undefined notation ${list}. Every symbol must resolve to ` +
69
+ `a notation entry in this page's glyph scope (a [[key]] reference or ` +
70
+ `notation.local).\n template: ${template.trim()}`,
71
+ );
72
+ }
73
+
74
+ let bound = result.latex;
75
+ slots.forEach(({ name, fallback }, index) => {
76
+ const sentinel = sentinelFor(index);
77
+ if (!bound.includes(sentinel)) {
78
+ throw new Error(
79
+ `Lab math slot "${name}" was lost during notation binding; keep slots ` +
80
+ `out of subscripts and other reindexed positions.\n template: ${template.trim()}`,
81
+ );
82
+ }
83
+ bound = bound.replace(
84
+ sentinel,
85
+ `\\htmlData{lab-slot=${name}}{${fallback}}`,
86
+ );
87
+ });
88
+
89
+ const html = katex.renderToString(
90
+ bound,
91
+ createNotationKatexOptions({ displayMode: true }),
92
+ );
93
+
94
+ return {
95
+ html,
96
+ slots: slots.map(({ name }) => name),
97
+ keys: [...new Set(result.bindings.map(({ key }) => key))],
98
+ };
99
+ }
@@ -0,0 +1,182 @@
1
+ export type EditorialStatus = 'draft' | 'in-review' | 'reviewed';
2
+
3
+ export interface SourceSpan {
4
+ readonly file: string;
5
+ readonly line?: number;
6
+ readonly column?: number;
7
+ readonly endLine?: number;
8
+ readonly endColumn?: number;
9
+ }
10
+
11
+ export type NotationAlignment =
12
+ | {
13
+ readonly kind: 'competency';
14
+ readonly introducedByCompetency: string;
15
+ readonly introducedInLesson: string;
16
+ }
17
+ | {
18
+ readonly kind: 'general';
19
+ readonly rationale: string;
20
+ };
21
+
22
+ export type NotationReferenceKind = 'prose' | 'math' | 'definition';
23
+
24
+ export interface NotationReferenceInput {
25
+ readonly key: string;
26
+ readonly kind: NotationReferenceKind;
27
+ readonly source: SourceSpan;
28
+ }
29
+
30
+ export interface SharedNotationDefinitionInput {
31
+ readonly key: string;
32
+ readonly notation: string;
33
+ readonly label: string;
34
+ readonly summary: string;
35
+ readonly aliases: readonly string[];
36
+ readonly domain: string;
37
+ readonly formula?: string;
38
+ readonly units?: string;
39
+ readonly sources: readonly string[];
40
+ readonly seeAlso: readonly string[];
41
+ readonly alignment: NotationAlignment;
42
+ readonly status: EditorialStatus;
43
+ readonly aiAssisted: boolean;
44
+ readonly body: string;
45
+ /** References harvested from the Markdown body. */
46
+ readonly references: readonly NotationReferenceInput[];
47
+ readonly source: SourceSpan;
48
+ }
49
+
50
+ export interface LocalNotationDefinitionInput {
51
+ readonly key: string;
52
+ readonly notation: string;
53
+ readonly label: string;
54
+ readonly summary: string;
55
+ readonly formula?: string;
56
+ readonly units?: string;
57
+ readonly sources: readonly string[];
58
+ readonly seeAlso: readonly string[];
59
+ readonly alignment: NotationAlignment;
60
+ /** References harvested from summary content. */
61
+ readonly references: readonly NotationReferenceInput[];
62
+ readonly source: SourceSpan;
63
+ }
64
+
65
+ export interface NotationLessonInput {
66
+ readonly lessonId: string;
67
+ readonly status: EditorialStatus;
68
+ readonly localDefinitions: readonly LocalNotationDefinitionInput[];
69
+ /** References harvested from lesson prose and equations, excluding definitions. */
70
+ readonly references: readonly NotationReferenceInput[];
71
+ /** Original lesson body, retained for lossless loader adapters. */
72
+ readonly body: string;
73
+ readonly source: SourceSpan;
74
+ }
75
+
76
+ export interface NotationRegistryInput {
77
+ readonly sharedDefinitions: readonly SharedNotationDefinitionInput[];
78
+ readonly lessons: readonly NotationLessonInput[];
79
+ }
80
+
81
+ export type NotationDiagnosticCode =
82
+ | 'invalid-key'
83
+ | 'duplicate-page'
84
+ | 'duplicate-definition'
85
+ | 'conflicting-definition'
86
+ | 'undefined-reference'
87
+ | 'reference-cycle'
88
+ | 'unused-definition'
89
+ | 'alignment-unknown-competency'
90
+ | 'alignment-unknown-lesson'
91
+ | 'alignment-introduction'
92
+ | 'alignment-unavailable'
93
+ | 'alignment-review-state';
94
+
95
+ export type DiagnosticSeverity = 'error' | 'warning';
96
+
97
+ export interface NotationDiagnostic {
98
+ readonly code: NotationDiagnosticCode;
99
+ readonly severity: DiagnosticSeverity;
100
+ readonly message: string;
101
+ readonly key?: string;
102
+ readonly lessonId?: string;
103
+ readonly definitionId?: string;
104
+ readonly source?: SourceSpan;
105
+ readonly relatedSources?: readonly SourceSpan[];
106
+ readonly path?: readonly string[];
107
+ }
108
+
109
+ export type NotationDefinitionScope =
110
+ | { readonly kind: 'shared' }
111
+ | { readonly kind: 'page'; readonly lessonId: string };
112
+
113
+ export interface ResolvedNotationReference {
114
+ readonly key: string;
115
+ readonly kind: NotationReferenceKind;
116
+ readonly definitionId: string;
117
+ readonly source: SourceSpan;
118
+ }
119
+
120
+ interface NotationDefinitionRecordBase {
121
+ readonly id: string;
122
+ readonly key: string;
123
+ readonly scope: NotationDefinitionScope;
124
+ readonly notation: string;
125
+ readonly label: string;
126
+ readonly seeAlso: readonly string[];
127
+ readonly alignment: NotationAlignment;
128
+ readonly status: EditorialStatus;
129
+ readonly source: SourceSpan;
130
+ readonly resolvedReferences: readonly ResolvedNotationReference[];
131
+ }
132
+
133
+ export interface SharedNotationDefinitionRecord extends NotationDefinitionRecordBase {
134
+ readonly kind: 'shared';
135
+ readonly summary: string;
136
+ readonly aliases: readonly string[];
137
+ readonly domain: string;
138
+ readonly formula?: string;
139
+ readonly units?: string;
140
+ readonly sources: readonly string[];
141
+ readonly aiAssisted: boolean;
142
+ readonly body: string;
143
+ }
144
+
145
+ export interface LocalNotationDefinitionRecord extends NotationDefinitionRecordBase {
146
+ readonly kind: 'local';
147
+ readonly lessonId: string;
148
+ readonly summary: string;
149
+ readonly formula?: string;
150
+ readonly units?: string;
151
+ readonly sources: readonly string[];
152
+ }
153
+
154
+ export type NotationDefinitionRecord =
155
+ SharedNotationDefinitionRecord | LocalNotationDefinitionRecord;
156
+
157
+ export interface NotationBinding {
158
+ readonly key: string;
159
+ readonly definitionId: string;
160
+ }
161
+
162
+ export interface NotationPageBundle {
163
+ readonly lessonId: string;
164
+ readonly status: EditorialStatus;
165
+ readonly source: SourceSpan;
166
+ readonly bindings: readonly NotationBinding[];
167
+ readonly definitionIds: readonly string[];
168
+ }
169
+
170
+ export interface NotationBacklink {
171
+ readonly definitionId: string;
172
+ readonly key: string;
173
+ readonly lessonId: string;
174
+ readonly direct: boolean;
175
+ }
176
+
177
+ export interface NotationRegistry {
178
+ readonly definitions: readonly NotationDefinitionRecord[];
179
+ readonly bundles: readonly NotationPageBundle[];
180
+ readonly backlinks: readonly NotationBacklink[];
181
+ readonly diagnostics: readonly NotationDiagnostic[];
182
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Controlled vocabulary for notation `units` strings (Step D6, check `units-vocab`).
3
+ *
4
+ * A `units` string is a short English phrase (`stated currency at valuation
5
+ * time`, `model-years from valuation time`, `probability between zero and one`).
6
+ * There is no fixed enum of whole phrases — the corpus composes them — so the
7
+ * vocabulary is a **word list**: every whitespace/hyphen-separated token of a
8
+ * `units` string must already be established in the corpus.
9
+ *
10
+ * The list below is the set of tokens that appear in two or more distinct
11
+ * `units` strings today, plus a small set of function words and morphological
12
+ * variants. A token outside it is either a typo or unreviewed divergence
13
+ * (`USD` vs `stated currency`, `valuation date` vs `valuation time`,
14
+ * `synthetic` / `simplified` framing, multi-clause `code; … prose and UI`
15
+ * strings) — surfaced as a Tier-3 warning, never auto-corrected.
16
+ *
17
+ * Extend deliberately: add a token here only when a reviewer has accepted the
18
+ * phrasing that introduces it.
19
+ */
20
+
21
+ export const UNITS_VOCAB: ReadonlySet<string> = new Set([
22
+ // structure / function words
23
+ 'a',
24
+ 'and',
25
+ 'as',
26
+ 'at',
27
+ 'between',
28
+ 'by',
29
+ 'for',
30
+ 'from',
31
+ 'in',
32
+ 'of',
33
+ 'or',
34
+ 'per',
35
+ 'the',
36
+ 'this',
37
+ 'to',
38
+ 'under',
39
+ 'when',
40
+ 'after',
41
+ // currency / value
42
+ 'currency',
43
+ 'currencies',
44
+ 'currency-unit',
45
+ 'currency-units',
46
+ 'unit',
47
+ 'units',
48
+ 'stated',
49
+ 'value',
50
+ 'present',
51
+ 'notional',
52
+ 'cash',
53
+ // time
54
+ 'time',
55
+ 'times',
56
+ 'year',
57
+ 'years',
58
+ 'model',
59
+ 'model-year',
60
+ 'model-years',
61
+ 'valuation',
62
+ 'date',
63
+ 'dates',
64
+ 'day',
65
+ 'days',
66
+ 'horizon',
67
+ 'current',
68
+ 'future',
69
+ 'next',
70
+ 'node',
71
+ 'successor',
72
+ 'settlement',
73
+ 'expiry',
74
+ 'expiration',
75
+ 'delivery',
76
+ 'forward',
77
+ 'maturity',
78
+ 'payment',
79
+ 'payments',
80
+ 'coupon',
81
+ 'premium',
82
+ 'accrual',
83
+ 'scheduled',
84
+ 'schedule',
85
+ 'period',
86
+ 'periods',
87
+ // rates / dimensionless
88
+ 'decimal',
89
+ 'rate',
90
+ 'nominal',
91
+ 'annual',
92
+ 'compounded',
93
+ 'compounding',
94
+ 'frequency',
95
+ 'intensity',
96
+ 'basis',
97
+ 'point',
98
+ 'points',
99
+ 'percent',
100
+ 'fraction',
101
+ 'probability',
102
+ 'probabilities',
103
+ 'weight',
104
+ 'weights',
105
+ 'dimensionless',
106
+ 'integer',
107
+ 'index',
108
+ 'count',
109
+ 'zero',
110
+ 'one',
111
+ // instruments / quantities
112
+ 'bond',
113
+ 'option',
114
+ 'claim',
115
+ 'security',
116
+ 'underlying',
117
+ 'quantity',
118
+ 'quoted',
119
+ 'explicitly',
120
+ 'calculations',
121
+ ]);
122
+
123
+ /** Lowercase word tokens of a `units` string (split on whitespace and hyphens). */
124
+ export function unitsTokens(units: string): string[] {
125
+ return units
126
+ .toLowerCase()
127
+ .split(/[\s]+/)
128
+ .flatMap((word) => word.split(/(?<=[a-z0-9])-(?=[a-z])/))
129
+ .map((word) => word.replace(/^[^a-z0-9_]+|[^a-z0-9_]+$/g, ''))
130
+ .filter((word) => word.length > 0 && !/^\d+$/.test(word));
131
+ }
132
+
133
+ /** Tokens of `units` that are not in the controlled vocabulary, in order, deduped. */
134
+ export function offVocabularyTokens(units: string): string[] {
135
+ const seen = new Set<string>();
136
+ const out: string[] = [];
137
+ for (const token of unitsTokens(units)) {
138
+ if (UNITS_VOCAB.has(token) || seen.has(token)) continue;
139
+ seen.add(token);
140
+ out.push(token);
141
+ }
142
+ return out;
143
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The per-viewer UI-preference seam (Phase H1).
3
+ *
4
+ * A "preference" is a small, non-authoritative bit of view state — which layout
5
+ * rails a viewer collapsed, say. It is **not** learning progress (that is the
6
+ * {@link ProgressRepository} seam) and never carries content or identity.
7
+ *
8
+ * This is the single module in the codebase allowed to touch browser storage.
9
+ * Components import a `PreferenceStore`; they never call `localStorage`
10
+ * directly, so a future SSR host can pass `memoryPreferenceStore` (or a
11
+ * cookie-backed store) and nothing else changes. `createBrowserPreferenceStore`
12
+ * is the "versioned local-storage adapter" the architecture doc anticipates —
13
+ * kept deliberately tiny, and the only place `git grep localStorage` matches.
14
+ */
15
+
16
+ export interface PreferenceStore {
17
+ get(key: string): string | null;
18
+ set(key: string, value: string): void;
19
+ }
20
+
21
+ /**
22
+ * A `localStorage`-backed store. Every call is wrapped: storage can be absent
23
+ * (SSR), disabled, or full, and the caller must degrade gracefully — the UI
24
+ * controls still work, they just do not persist.
25
+ */
26
+ export function createBrowserPreferenceStore(): PreferenceStore {
27
+ return {
28
+ get(key) {
29
+ try {
30
+ return window.localStorage.getItem(key);
31
+ } catch {
32
+ return null;
33
+ }
34
+ },
35
+ set(key, value) {
36
+ try {
37
+ window.localStorage.setItem(key, value);
38
+ } catch {
39
+ // Storage unavailable or full — the control still works this session.
40
+ }
41
+ },
42
+ };
43
+ }
44
+
45
+ /** An in-memory store — the no-op-equivalent seam for SSR or tests. */
46
+ export function createMemoryPreferenceStore(): PreferenceStore {
47
+ const map = new Map<string, string>();
48
+ return {
49
+ get: (key) => map.get(key) ?? null,
50
+ set: (key, value) => {
51
+ map.set(key, value);
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * React binding for the viewer identity seam (Phase H1).
3
+ *
4
+ * `SessionUserContext` defaults to {@link anonymousUser}. The static site never
5
+ * wraps a provider around it, so `useSessionUser()` always returns the
6
+ * anonymous user; a future SSR/LMS host injects a real identity here without
7
+ * touching any consumer.
8
+ */
9
+
10
+ import { createContext, useContext } from 'react';
11
+
12
+ import { anonymousUser, type SessionUser } from './user';
13
+
14
+ export const SessionUserContext = createContext<SessionUser>(anonymousUser);
15
+
16
+ export function useSessionUser(): SessionUser {
17
+ return useContext(SessionUserContext);
18
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The viewer identity seam (Phase H1).
3
+ *
4
+ * The static site has no accounts and no sign-in: every viewer is anonymous.
5
+ * A future SSR app or LMS host is the only thing that would ever supply a real
6
+ * identity, and it would do so by wrapping the React tree in a provider over
7
+ * {@link SessionUserContext} (see `./user-context`). Nothing in this repository
8
+ * produces a non-anonymous user, and nothing here reads a cookie, a token, or
9
+ * browser storage.
10
+ */
11
+
12
+ export interface SessionUser {
13
+ /** Always `'anonymous'` in the static build. */
14
+ readonly kind: 'anonymous';
15
+ /** A stable id for a signed-in viewer; always `null` here. */
16
+ readonly id: string | null;
17
+ }
18
+
19
+ /** The only user the static site ever has. */
20
+ export const anonymousUser: SessionUser = { kind: 'anonymous', id: null };
21
+
22
+ export function isAnonymous(user: SessionUser): boolean {
23
+ return user.kind === 'anonymous' || user.id === null;
24
+ }