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,21 @@
1
+ interface LabMathScopeDefinition {
2
+ readonly key: string;
3
+ readonly notation: string;
4
+ }
5
+ interface RenderedLabMath {
6
+ /** KaTeX HTML+MathML with `data-notation-key` and `data-lab-slot` spans. */
7
+ readonly html: string;
8
+ /** Slot names, in template order, the island must supply at runtime. */
9
+ readonly slots: readonly string[];
10
+ /** Notation keys the template resolved to, for diagnostics. */
11
+ readonly keys: readonly string[];
12
+ }
13
+ /**
14
+ * Compile a developer-authored lab math template the same way lesson `$$`
15
+ * math is compiled: every identifier must resolve to a notation key in the
16
+ * page's scope or this throws. Slots are set aside during binding and restored
17
+ * as inert `data-lab-slot` spans the interactive island rewrites at runtime.
18
+ */
19
+ declare function renderLabMath(template: string, scope: readonly LabMathScopeDefinition[]): RenderedLabMath;
20
+
21
+ export { type LabMathScopeDefinition, type RenderedLabMath, renderLabMath };
@@ -0,0 +1,58 @@
1
+ import katex from "katex";
2
+ import { resolveMathGlyphs } from "./math-glyphs.mjs";
3
+ import { createNotationKatexOptions } from "./katex-options.mjs";
4
+ const SLOT_PATTERN = /\\slot\{([a-z][a-z0-9]*)\}\{((?:[^{}]|\{[^{}]*\})*)\}/g;
5
+ const sentinelFor = (index) => `909909${index}.909909`;
6
+ function renderLabMath(template, scope) {
7
+ const slots = [];
8
+ const forBinding = template.replace(
9
+ SLOT_PATTERN,
10
+ (_match, name, fallback) => {
11
+ const sentinel = sentinelFor(slots.length);
12
+ slots.push({ name, fallback });
13
+ return ` ${sentinel} `;
14
+ }
15
+ );
16
+ let result;
17
+ try {
18
+ result = resolveMathGlyphs(forBinding, [...scope]);
19
+ } catch (error) {
20
+ throw new Error(
21
+ `Lab math failed to compile: ${error instanceof Error ? error.message : String(error)}
22
+ template: ${template.trim()}`
23
+ );
24
+ }
25
+ if (result.unresolved.length > 0) {
26
+ const list = result.unresolved.map(({ token }) => `"${token}"`).join(", ");
27
+ throw new Error(
28
+ `Lab math uses undefined notation ${list}. Every symbol must resolve to a notation entry in this page's glyph scope (a [[key]] reference or notation.local).
29
+ template: ${template.trim()}`
30
+ );
31
+ }
32
+ let bound = result.latex;
33
+ slots.forEach(({ name, fallback }, index) => {
34
+ const sentinel = sentinelFor(index);
35
+ if (!bound.includes(sentinel)) {
36
+ throw new Error(
37
+ `Lab math slot "${name}" was lost during notation binding; keep slots out of subscripts and other reindexed positions.
38
+ template: ${template.trim()}`
39
+ );
40
+ }
41
+ bound = bound.replace(
42
+ sentinel,
43
+ `\\htmlData{lab-slot=${name}}{${fallback}}`
44
+ );
45
+ });
46
+ const html = katex.renderToString(
47
+ bound,
48
+ createNotationKatexOptions({ displayMode: true })
49
+ );
50
+ return {
51
+ html,
52
+ slots: slots.map(({ name }) => name),
53
+ keys: [...new Set(result.bindings.map(({ key }) => key))]
54
+ };
55
+ }
56
+ export {
57
+ renderLabMath
58
+ };
@@ -0,0 +1,151 @@
1
+ type EditorialStatus = 'draft' | 'in-review' | 'reviewed';
2
+ interface SourceSpan {
3
+ readonly file: string;
4
+ readonly line?: number;
5
+ readonly column?: number;
6
+ readonly endLine?: number;
7
+ readonly endColumn?: number;
8
+ }
9
+ type NotationAlignment = {
10
+ readonly kind: 'competency';
11
+ readonly introducedByCompetency: string;
12
+ readonly introducedInLesson: string;
13
+ } | {
14
+ readonly kind: 'general';
15
+ readonly rationale: string;
16
+ };
17
+ type NotationReferenceKind = 'prose' | 'math' | 'definition';
18
+ interface NotationReferenceInput {
19
+ readonly key: string;
20
+ readonly kind: NotationReferenceKind;
21
+ readonly source: SourceSpan;
22
+ }
23
+ interface SharedNotationDefinitionInput {
24
+ readonly key: string;
25
+ readonly notation: string;
26
+ readonly label: string;
27
+ readonly summary: string;
28
+ readonly aliases: readonly string[];
29
+ readonly domain: string;
30
+ readonly formula?: string;
31
+ readonly units?: string;
32
+ readonly sources: readonly string[];
33
+ readonly seeAlso: readonly string[];
34
+ readonly alignment: NotationAlignment;
35
+ readonly status: EditorialStatus;
36
+ readonly aiAssisted: boolean;
37
+ readonly body: string;
38
+ /** References harvested from the Markdown body. */
39
+ readonly references: readonly NotationReferenceInput[];
40
+ readonly source: SourceSpan;
41
+ }
42
+ interface LocalNotationDefinitionInput {
43
+ readonly key: string;
44
+ readonly notation: string;
45
+ readonly label: string;
46
+ readonly summary: string;
47
+ readonly formula?: string;
48
+ readonly units?: string;
49
+ readonly sources: readonly string[];
50
+ readonly seeAlso: readonly string[];
51
+ readonly alignment: NotationAlignment;
52
+ /** References harvested from summary content. */
53
+ readonly references: readonly NotationReferenceInput[];
54
+ readonly source: SourceSpan;
55
+ }
56
+ interface NotationLessonInput {
57
+ readonly lessonId: string;
58
+ readonly status: EditorialStatus;
59
+ readonly localDefinitions: readonly LocalNotationDefinitionInput[];
60
+ /** References harvested from lesson prose and equations, excluding definitions. */
61
+ readonly references: readonly NotationReferenceInput[];
62
+ /** Original lesson body, retained for lossless loader adapters. */
63
+ readonly body: string;
64
+ readonly source: SourceSpan;
65
+ }
66
+ interface NotationRegistryInput {
67
+ readonly sharedDefinitions: readonly SharedNotationDefinitionInput[];
68
+ readonly lessons: readonly NotationLessonInput[];
69
+ }
70
+ type NotationDiagnosticCode = 'invalid-key' | 'duplicate-page' | 'duplicate-definition' | 'conflicting-definition' | 'undefined-reference' | 'reference-cycle' | 'unused-definition' | 'alignment-unknown-competency' | 'alignment-unknown-lesson' | 'alignment-introduction' | 'alignment-unavailable' | 'alignment-review-state';
71
+ type DiagnosticSeverity = 'error' | 'warning';
72
+ interface NotationDiagnostic {
73
+ readonly code: NotationDiagnosticCode;
74
+ readonly severity: DiagnosticSeverity;
75
+ readonly message: string;
76
+ readonly key?: string;
77
+ readonly lessonId?: string;
78
+ readonly definitionId?: string;
79
+ readonly source?: SourceSpan;
80
+ readonly relatedSources?: readonly SourceSpan[];
81
+ readonly path?: readonly string[];
82
+ }
83
+ type NotationDefinitionScope = {
84
+ readonly kind: 'shared';
85
+ } | {
86
+ readonly kind: 'page';
87
+ readonly lessonId: string;
88
+ };
89
+ interface ResolvedNotationReference {
90
+ readonly key: string;
91
+ readonly kind: NotationReferenceKind;
92
+ readonly definitionId: string;
93
+ readonly source: SourceSpan;
94
+ }
95
+ interface NotationDefinitionRecordBase {
96
+ readonly id: string;
97
+ readonly key: string;
98
+ readonly scope: NotationDefinitionScope;
99
+ readonly notation: string;
100
+ readonly label: string;
101
+ readonly seeAlso: readonly string[];
102
+ readonly alignment: NotationAlignment;
103
+ readonly status: EditorialStatus;
104
+ readonly source: SourceSpan;
105
+ readonly resolvedReferences: readonly ResolvedNotationReference[];
106
+ }
107
+ interface SharedNotationDefinitionRecord extends NotationDefinitionRecordBase {
108
+ readonly kind: 'shared';
109
+ readonly summary: string;
110
+ readonly aliases: readonly string[];
111
+ readonly domain: string;
112
+ readonly formula?: string;
113
+ readonly units?: string;
114
+ readonly sources: readonly string[];
115
+ readonly aiAssisted: boolean;
116
+ readonly body: string;
117
+ }
118
+ interface LocalNotationDefinitionRecord extends NotationDefinitionRecordBase {
119
+ readonly kind: 'local';
120
+ readonly lessonId: string;
121
+ readonly summary: string;
122
+ readonly formula?: string;
123
+ readonly units?: string;
124
+ readonly sources: readonly string[];
125
+ }
126
+ type NotationDefinitionRecord = SharedNotationDefinitionRecord | LocalNotationDefinitionRecord;
127
+ interface NotationBinding {
128
+ readonly key: string;
129
+ readonly definitionId: string;
130
+ }
131
+ interface NotationPageBundle {
132
+ readonly lessonId: string;
133
+ readonly status: EditorialStatus;
134
+ readonly source: SourceSpan;
135
+ readonly bindings: readonly NotationBinding[];
136
+ readonly definitionIds: readonly string[];
137
+ }
138
+ interface NotationBacklink {
139
+ readonly definitionId: string;
140
+ readonly key: string;
141
+ readonly lessonId: string;
142
+ readonly direct: boolean;
143
+ }
144
+ interface NotationRegistry {
145
+ readonly definitions: readonly NotationDefinitionRecord[];
146
+ readonly bundles: readonly NotationPageBundle[];
147
+ readonly backlinks: readonly NotationBacklink[];
148
+ readonly diagnostics: readonly NotationDiagnostic[];
149
+ }
150
+
151
+ export type { DiagnosticSeverity, EditorialStatus, LocalNotationDefinitionInput, LocalNotationDefinitionRecord, NotationAlignment, NotationBacklink, NotationBinding, NotationDefinitionRecord, NotationDefinitionScope, NotationDiagnostic, NotationDiagnosticCode, NotationLessonInput, NotationPageBundle, NotationReferenceInput, NotationReferenceKind, NotationRegistry, NotationRegistryInput, ResolvedNotationReference, SharedNotationDefinitionInput, SharedNotationDefinitionRecord, SourceSpan };
File without changes
@@ -0,0 +1,26 @@
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
+ declare const UNITS_VOCAB: ReadonlySet<string>;
21
+ /** Lowercase word tokens of a `units` string (split on whitespace and hyphens). */
22
+ declare function unitsTokens(units: string): string[];
23
+ /** Tokens of `units` that are not in the controlled vocabulary, in order, deduped. */
24
+ declare function offVocabularyTokens(units: string): string[];
25
+
26
+ export { UNITS_VOCAB, offVocabularyTokens, unitsTokens };
@@ -0,0 +1,119 @@
1
+ const UNITS_VOCAB = /* @__PURE__ */ new Set([
2
+ // structure / function words
3
+ "a",
4
+ "and",
5
+ "as",
6
+ "at",
7
+ "between",
8
+ "by",
9
+ "for",
10
+ "from",
11
+ "in",
12
+ "of",
13
+ "or",
14
+ "per",
15
+ "the",
16
+ "this",
17
+ "to",
18
+ "under",
19
+ "when",
20
+ "after",
21
+ // currency / value
22
+ "currency",
23
+ "currencies",
24
+ "currency-unit",
25
+ "currency-units",
26
+ "unit",
27
+ "units",
28
+ "stated",
29
+ "value",
30
+ "present",
31
+ "notional",
32
+ "cash",
33
+ // time
34
+ "time",
35
+ "times",
36
+ "year",
37
+ "years",
38
+ "model",
39
+ "model-year",
40
+ "model-years",
41
+ "valuation",
42
+ "date",
43
+ "dates",
44
+ "day",
45
+ "days",
46
+ "horizon",
47
+ "current",
48
+ "future",
49
+ "next",
50
+ "node",
51
+ "successor",
52
+ "settlement",
53
+ "expiry",
54
+ "expiration",
55
+ "delivery",
56
+ "forward",
57
+ "maturity",
58
+ "payment",
59
+ "payments",
60
+ "coupon",
61
+ "premium",
62
+ "accrual",
63
+ "scheduled",
64
+ "schedule",
65
+ "period",
66
+ "periods",
67
+ // rates / dimensionless
68
+ "decimal",
69
+ "rate",
70
+ "nominal",
71
+ "annual",
72
+ "compounded",
73
+ "compounding",
74
+ "frequency",
75
+ "intensity",
76
+ "basis",
77
+ "point",
78
+ "points",
79
+ "percent",
80
+ "fraction",
81
+ "probability",
82
+ "probabilities",
83
+ "weight",
84
+ "weights",
85
+ "dimensionless",
86
+ "integer",
87
+ "index",
88
+ "count",
89
+ "zero",
90
+ "one",
91
+ // instruments / quantities
92
+ "bond",
93
+ "option",
94
+ "claim",
95
+ "security",
96
+ "underlying",
97
+ "quantity",
98
+ "quoted",
99
+ "explicitly",
100
+ "calculations"
101
+ ]);
102
+ function unitsTokens(units) {
103
+ return units.toLowerCase().split(/[\s]+/).flatMap((word) => word.split(/(?<=[a-z0-9])-(?=[a-z])/)).map((word) => word.replace(/^[^a-z0-9_]+|[^a-z0-9_]+$/g, "")).filter((word) => word.length > 0 && !/^\d+$/.test(word));
104
+ }
105
+ function offVocabularyTokens(units) {
106
+ const seen = /* @__PURE__ */ new Set();
107
+ const out = [];
108
+ for (const token of unitsTokens(units)) {
109
+ if (UNITS_VOCAB.has(token) || seen.has(token)) continue;
110
+ seen.add(token);
111
+ out.push(token);
112
+ }
113
+ return out;
114
+ }
115
+ export {
116
+ UNITS_VOCAB,
117
+ offVocabularyTokens,
118
+ unitsTokens
119
+ };
@@ -0,0 +1,28 @@
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
+ interface PreferenceStore {
16
+ get(key: string): string | null;
17
+ set(key: string, value: string): void;
18
+ }
19
+ /**
20
+ * A `localStorage`-backed store. Every call is wrapped: storage can be absent
21
+ * (SSR), disabled, or full, and the caller must degrade gracefully — the UI
22
+ * controls still work, they just do not persist.
23
+ */
24
+ declare function createBrowserPreferenceStore(): PreferenceStore;
25
+ /** An in-memory store — the no-op-equivalent seam for SSR or tests. */
26
+ declare function createMemoryPreferenceStore(): PreferenceStore;
27
+
28
+ export { type PreferenceStore, createBrowserPreferenceStore, createMemoryPreferenceStore };
@@ -0,0 +1,30 @@
1
+ function createBrowserPreferenceStore() {
2
+ return {
3
+ get(key) {
4
+ try {
5
+ return window.localStorage.getItem(key);
6
+ } catch {
7
+ return null;
8
+ }
9
+ },
10
+ set(key, value) {
11
+ try {
12
+ window.localStorage.setItem(key, value);
13
+ } catch {
14
+ }
15
+ }
16
+ };
17
+ }
18
+ function createMemoryPreferenceStore() {
19
+ const map = /* @__PURE__ */ new Map();
20
+ return {
21
+ get: (key) => map.get(key) ?? null,
22
+ set: (key, value) => {
23
+ map.set(key, value);
24
+ }
25
+ };
26
+ }
27
+ export {
28
+ createBrowserPreferenceStore,
29
+ createMemoryPreferenceStore
30
+ };
@@ -0,0 +1,7 @@
1
+ import * as react from 'react';
2
+ import { SessionUser } from './user.js';
3
+
4
+ declare const SessionUserContext: react.Context<SessionUser>;
5
+ declare function useSessionUser(): SessionUser;
6
+
7
+ export { SessionUserContext, useSessionUser };
@@ -0,0 +1,10 @@
1
+ import { createContext, useContext } from "react";
2
+ import { anonymousUser } from "./user";
3
+ const SessionUserContext = createContext(anonymousUser);
4
+ function useSessionUser() {
5
+ return useContext(SessionUserContext);
6
+ }
7
+ export {
8
+ SessionUserContext,
9
+ useSessionUser
10
+ };
@@ -0,0 +1,21 @@
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
+ interface SessionUser {
12
+ /** Always `'anonymous'` in the static build. */
13
+ readonly kind: 'anonymous';
14
+ /** A stable id for a signed-in viewer; always `null` here. */
15
+ readonly id: string | null;
16
+ }
17
+ /** The only user the static site ever has. */
18
+ declare const anonymousUser: SessionUser;
19
+ declare function isAnonymous(user: SessionUser): boolean;
20
+
21
+ export { type SessionUser, anonymousUser, isAnonymous };
@@ -0,0 +1,8 @@
1
+ const anonymousUser = { kind: "anonymous", id: null };
2
+ function isAnonymous(user) {
3
+ return user.kind === "anonymous" || user.id === null;
4
+ }
5
+ export {
6
+ anonymousUser,
7
+ isAnonymous
8
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "explico",
3
+ "version": "0.1.0",
4
+ "description": "Build-time engine for prerequisite-aware, notation-rigorous course content: one validated content model, a semantic notation/citation/equation system with a completeness gate, and a deterministic JSON manifest.",
5
+ "type": "module",
6
+ "license": "UNLICENSED",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/danieltuzes/explico.git"
10
+ },
11
+ "homepage": "https://github.com/danieltuzes/explico#readme",
12
+ "bugs": "https://github.com/danieltuzes/explico/issues",
13
+ "sideEffects": [
14
+ "*.css"
15
+ ],
16
+ "exports": {
17
+ "./package.json": "./package.json",
18
+ "./content-config": "./src/content-config.ts",
19
+ "./components/*": "./src/components/*",
20
+ "./styles/*": "./src/styles/*",
21
+ "./reference/*.mjs": "./dist/reference/*.mjs",
22
+ "./*": {
23
+ "types": "./dist/*.d.ts",
24
+ "default": "./dist/*.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "src",
30
+ "README.md"
31
+ ],
32
+ "peerDependencies": {
33
+ "@astrojs/markdown-remark": "7.2.4",
34
+ "@astrojs/react": "6.0.4",
35
+ "@astrojs/starlight": "0.41.9",
36
+ "astro": "7.2.7",
37
+ "katex": "0.16.47",
38
+ "react": "19.2.8",
39
+ "react-dom": "19.2.8",
40
+ "rehype-katex": "7.0.1",
41
+ "remark-math": "6.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "24.13.3",
45
+ "@types/react": "19.2.18",
46
+ "@types/react-dom": "19.2.5",
47
+ "tsup": "8.5.1",
48
+ "typescript": "6.0.2"
49
+ },
50
+ "scripts": {
51
+ "build": "tsup && node scripts/copy-assets.mjs"
52
+ }
53
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The analytics seam (Phase H1).
3
+ *
4
+ * `noopAnalyticsEmitter` is the only sink the static site ships — `emit` does
5
+ * nothing. Stateful components call it alongside the {@link ProgressRepository}
6
+ * so a future host can route interaction events somewhere real without touching
7
+ * a component. Framework-free: no React, Astro, content, or browser API, and no
8
+ * network call.
9
+ */
10
+
11
+ export interface AnalyticsEvent {
12
+ readonly name: string;
13
+ /** Epoch milliseconds, supplied by the caller so this stays side-effect-free. */
14
+ readonly at: number;
15
+ readonly props?: Readonly<Record<string, string | number | boolean | null>>;
16
+ }
17
+
18
+ export interface AnalyticsEmitter {
19
+ emit(event: AnalyticsEvent): void;
20
+ }
21
+
22
+ /** The only sink in the static build. */
23
+ export const noopAnalyticsEmitter: AnalyticsEmitter = { emit: () => {} };