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,41 @@
1
+ import { NotationRegistryInput } from './types.js';
2
+
3
+ interface MathGateDiagnostic {
4
+ readonly context: 'body' | 'formula' | 'assessment';
5
+ /**
6
+ * Body / component-slot math blocks the build (the render path needs every
7
+ * glyph resolved for the KaTeX trust callback). `formula` and `assessment`
8
+ * math enter as warnings (Tier 3) and are promoted once existing content is
9
+ * brought into line — they surface pre-existing ambiguities that are a
10
+ * separate reviewed content pass (D6/D13/D14), not a D3 codemod.
11
+ */
12
+ readonly severity: 'error' | 'warning';
13
+ readonly file: string;
14
+ readonly line?: number;
15
+ readonly token?: string;
16
+ readonly message: string;
17
+ }
18
+ interface AssessmentItemLike {
19
+ readonly id?: string;
20
+ readonly prompt?: string;
21
+ readonly explanation?: string;
22
+ }
23
+ interface AssessmentLike {
24
+ readonly id: string;
25
+ readonly items?: readonly AssessmentItemLike[];
26
+ }
27
+ interface LessonAssessmentLink {
28
+ readonly lessonId: string;
29
+ readonly assessmentIds: readonly string[];
30
+ }
31
+ /**
32
+ * Resolve every rendered-math context against the page glyph table. Returns one
33
+ * diagnostic per unresolved token; an empty array means the gate passed.
34
+ */
35
+ declare function gateContentMath(input: {
36
+ readonly notationInput: NotationRegistryInput;
37
+ readonly assessments: readonly AssessmentLike[];
38
+ readonly lessonAssessments: readonly LessonAssessmentLink[];
39
+ }): MathGateDiagnostic[];
40
+
41
+ export { type MathGateDiagnostic, gateContentMath };
@@ -0,0 +1,151 @@
1
+ import { resolveMathGlyphs, GlyphResolutionError } from "./math-glyphs.mjs";
2
+ import { stripEquationLabels } from "./equations.mjs";
3
+ const SEVERITY = {
4
+ body: "error",
5
+ formula: "warning",
6
+ assessment: "warning"
7
+ };
8
+ const TERM_KEY = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
9
+ const EXPLAIN_KEY = /\\explain\s*\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}/g;
10
+ const DISPLAY_MATH = /\$\$([\s\S]+?)\$\$/g;
11
+ const INLINE_MATH = /(?<![\\$])\$(?!\s)([^$\n]+?)(?<![\\\s])\$(?!\d)/g;
12
+ function stripCode(text) {
13
+ return text.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, " ")).replace(/(^|[^`])`[^`\n]*`/g, (match) => match.replace(/[^\n]/g, " "));
14
+ }
15
+ function lineOf(text, index) {
16
+ return text.slice(0, index).split("\n").length;
17
+ }
18
+ function mathSpans(body) {
19
+ const searchable = stripCode(body);
20
+ const spans = [];
21
+ for (const pattern of [DISPLAY_MATH, INLINE_MATH]) {
22
+ pattern.lastIndex = 0;
23
+ let match;
24
+ while (match = pattern.exec(searchable)) {
25
+ spans.push({
26
+ // D7: `\label{eq:…}` is an identity marker, not math KaTeX parses.
27
+ latex: stripEquationLabels(match[1] ?? ""),
28
+ line: lineOf(body, match.index)
29
+ });
30
+ }
31
+ }
32
+ return spans;
33
+ }
34
+ function pageScope(lesson, sharedByKey) {
35
+ const local = lesson.localDefinitions.map((definition) => ({
36
+ key: definition.key,
37
+ notation: definition.notation
38
+ }));
39
+ const localKeys = new Set(local.map((entry) => entry.key));
40
+ const referenced = /* @__PURE__ */ new Set();
41
+ for (const pattern of [TERM_KEY, EXPLAIN_KEY]) {
42
+ for (const match of lesson.body.matchAll(pattern)) referenced.add(match[1]);
43
+ }
44
+ const scope = [...local];
45
+ for (const key of referenced) {
46
+ if (localKeys.has(key)) continue;
47
+ const shared = sharedByKey.get(key);
48
+ if (shared) scope.push({ key: shared.key, notation: shared.notation });
49
+ }
50
+ return scope;
51
+ }
52
+ function resolveInto(latex, scope, context, file, line, fix, out) {
53
+ let result;
54
+ try {
55
+ result = resolveMathGlyphs(latex, [...scope]);
56
+ } catch (error) {
57
+ out.push({
58
+ context,
59
+ severity: SEVERITY[context],
60
+ file,
61
+ line,
62
+ message: error instanceof GlyphResolutionError ? error.message : `Could not compile math ${JSON.stringify(latex)}: ${error instanceof Error ? error.message : String(error)}`
63
+ });
64
+ return;
65
+ }
66
+ for (const { token } of result.unresolved) {
67
+ out.push({
68
+ context,
69
+ severity: SEVERITY[context],
70
+ file,
71
+ line,
72
+ token,
73
+ message: `Unresolved notation ${JSON.stringify(token)} in ${context} math. ${fix}`
74
+ });
75
+ }
76
+ }
77
+ const BODY_FIX = "Introduce the symbol with [[key]], define it in notation.local, or wrap it in \\explain{key}{latex}.";
78
+ const FORMULA_FIX = "Every right-hand-side variable of a notation.local formula must resolve to the entry key, another page symbol, or the base library.";
79
+ const ASSESSMENT_FIX = "Introduce the symbol in the owning lesson's prose with [[key]], or wrap it in \\explain{key}{latex}.";
80
+ function gateContentMath(input) {
81
+ const { notationInput, assessments, lessonAssessments } = input;
82
+ const sharedByKey = new Map(
83
+ notationInput.sharedDefinitions.map((definition) => [
84
+ definition.key,
85
+ definition
86
+ ])
87
+ );
88
+ const lessonById = new Map(
89
+ notationInput.lessons.map((lesson) => [lesson.lessonId, lesson])
90
+ );
91
+ const assessmentById = new Map(
92
+ assessments.map((assessment) => [assessment.id, assessment])
93
+ );
94
+ const diagnostics = [];
95
+ for (const lesson of notationInput.lessons) {
96
+ const scope = pageScope(lesson, sharedByKey);
97
+ const file = lesson.source.file;
98
+ for (const span of mathSpans(lesson.body)) {
99
+ resolveInto(
100
+ span.latex,
101
+ scope,
102
+ "body",
103
+ file,
104
+ span.line,
105
+ BODY_FIX,
106
+ diagnostics
107
+ );
108
+ }
109
+ for (const definition of lesson.localDefinitions) {
110
+ if (!definition.formula) continue;
111
+ resolveInto(
112
+ definition.formula,
113
+ scope,
114
+ "formula",
115
+ file,
116
+ void 0,
117
+ FORMULA_FIX,
118
+ diagnostics
119
+ );
120
+ }
121
+ }
122
+ for (const link of lessonAssessments) {
123
+ const lesson = lessonById.get(link.lessonId);
124
+ if (!lesson) continue;
125
+ const scope = pageScope(lesson, sharedByKey);
126
+ for (const assessmentId of link.assessmentIds) {
127
+ const assessment = assessmentById.get(assessmentId);
128
+ if (!assessment?.items) continue;
129
+ for (const item of assessment.items) {
130
+ for (const field of [item.prompt, item.explanation]) {
131
+ if (typeof field !== "string") continue;
132
+ for (const span of mathSpans(field)) {
133
+ resolveInto(
134
+ span.latex,
135
+ scope,
136
+ "assessment",
137
+ `${assessmentId} (${item.id ?? "?"})`,
138
+ void 0,
139
+ ASSESSMENT_FIX,
140
+ diagnostics
141
+ );
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+ return diagnostics;
148
+ }
149
+ export {
150
+ gateContentMath
151
+ };
@@ -0,0 +1,91 @@
1
+ import { NOTATION_KEY_PATTERN } from './remark-notation.mjs';
2
+
3
+ export const EXPLAIN_MACRO = String.raw`\htmlData{notation-key=#1}{#2}`;
4
+
5
+ // Interactive labs render developer-authored math templates that carry two
6
+ // kinds of inert marker: `\explain` for a notation key (as above) and `\slot`
7
+ // for a numeric hole the island rewrites at runtime. Both compile to a single
8
+ // `data-*` attribute and nothing else.
9
+ export const SLOT_MACRO = String.raw`\htmlData{lab-slot=#1}{#2}`;
10
+ const LAB_SLOT_PATTERN = /^[a-z][a-z0-9]*$/;
11
+
12
+ /**
13
+ * KaTeX trust callback that permits exactly one inert marker attribute:
14
+ * `data-notation-key` (a registry key) or `data-lab-slot` (a lab value hole).
15
+ * Links, styles, classes, IDs, protocols, and every other data attribute stay
16
+ * untrusted. Key existence is checked by the remark adapter, the lab-math
17
+ * renderer, and the workspace validator before KaTeX runs.
18
+ */
19
+ export function trustNotationMarker(context) {
20
+ if (context?.command !== '\\htmlData') return false;
21
+ if (!context.attributes || typeof context.attributes !== 'object') {
22
+ return false;
23
+ }
24
+
25
+ const entries = Object.entries(context.attributes);
26
+ if (entries.length !== 1) return false;
27
+ const [name, value] = entries[0];
28
+ if (typeof value !== 'string') return false;
29
+
30
+ if (name === 'data-notation-key') return NOTATION_KEY_PATTERN.test(value);
31
+ if (name === 'data-lab-slot') return LAB_SLOT_PATTERN.test(value);
32
+ return false;
33
+ }
34
+
35
+ function strictNotation(errorCode) {
36
+ // The trusted macros intentionally use KaTeX's HTML extension. Keep normal
37
+ // strict-mode warnings for all other non-LaTeX constructs.
38
+ return errorCode === 'htmlExtension' ? 'ignore' : 'warn';
39
+ }
40
+
41
+ export const notationKatexOptions = Object.freeze({
42
+ output: 'htmlAndMathml',
43
+ throwOnError: true,
44
+ strict: strictNotation,
45
+ trust: trustNotationMarker,
46
+ macros: Object.freeze({
47
+ '\\explain': EXPLAIN_MACRO,
48
+ '\\slot': SLOT_MACRO,
49
+ }),
50
+ });
51
+
52
+ /**
53
+ * Add non-security KaTeX settings while keeping the notation macros and trust
54
+ * boundary non-overridable.
55
+ */
56
+ export function createNotationKatexOptions(overrides = {}) {
57
+ if ('output' in overrides) {
58
+ throw new TypeError(
59
+ 'The notation KaTeX output mode cannot be overridden; HTML and MathML are required.',
60
+ );
61
+ }
62
+ if ('throwOnError' in overrides) {
63
+ throw new TypeError(
64
+ 'The notation KaTeX error mode cannot be overridden; rendering errors must fail the build.',
65
+ );
66
+ }
67
+ if ('trust' in overrides) {
68
+ throw new TypeError(
69
+ 'The notation KaTeX trust callback cannot be overridden.',
70
+ );
71
+ }
72
+ if (overrides.macros?.['\\explain'] !== undefined) {
73
+ throw new TypeError('The \\explain KaTeX macro cannot be overridden.');
74
+ }
75
+ if (overrides.macros?.['\\slot'] !== undefined) {
76
+ throw new TypeError('The \\slot KaTeX macro cannot be overridden.');
77
+ }
78
+
79
+ return {
80
+ ...notationKatexOptions,
81
+ ...overrides,
82
+ output: notationKatexOptions.output,
83
+ throwOnError: notationKatexOptions.throwOnError,
84
+ trust: trustNotationMarker,
85
+ macros: {
86
+ ...(overrides.macros ?? {}),
87
+ '\\explain': EXPLAIN_MACRO,
88
+ '\\slot': SLOT_MACRO,
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,21 @@
1
+ import { LabMathScopeDefinition } from './render-lab-math.js';
2
+
3
+ interface LessonNotationFrontmatter {
4
+ readonly local?: ReadonlyArray<{
5
+ readonly key: string;
6
+ readonly latex: string;
7
+ }>;
8
+ }
9
+ /**
10
+ * The notation a lab's math template may reference: exactly the same page glyph
11
+ * scope the remark adapter gives lesson `$$` math — this page's
12
+ * `notation.local` plus the shared entries it names in the body with `[[key]]`
13
+ * (or `\explain{key}{…}`). No transitive closure, so island math cannot
14
+ * quietly reach beyond what the lesson introduces.
15
+ *
16
+ * Shared definitions are read from the one compiler manifest (Phase D5); this
17
+ * module never re-reads the content tree.
18
+ */
19
+ declare function labMathScope(notation: LessonNotationFrontmatter | undefined, body?: string): Promise<LabMathScopeDefinition[]>;
20
+
21
+ export { labMathScope };
@@ -0,0 +1,28 @@
1
+ import { loadManifest } from "../compiler/manifest";
2
+ const TERM_KEY = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
3
+ const EXPLAIN_KEY = /\\explain\s*\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}/g;
4
+ async function labMathScope(notation, body = "") {
5
+ const local = notation?.local ?? [];
6
+ const scope = local.map((definition) => ({
7
+ key: definition.key,
8
+ notation: definition.latex
9
+ }));
10
+ const referenced = /* @__PURE__ */ new Set();
11
+ for (const pattern of [TERM_KEY, EXPLAIN_KEY]) {
12
+ for (const match of body.matchAll(pattern)) referenced.add(match[1]);
13
+ }
14
+ const localKeys = new Set(local.map((definition) => definition.key));
15
+ const manifest = await loadManifest();
16
+ const sharedByKey = new Map(
17
+ manifest.notation.definitions.filter((definition) => definition.kind === "shared").map((definition) => [definition.key, definition])
18
+ );
19
+ for (const key of referenced) {
20
+ if (localKeys.has(key)) continue;
21
+ const shared = sharedByKey.get(key);
22
+ if (shared) scope.push({ key: shared.key, notation: shared.notation });
23
+ }
24
+ return scope;
25
+ }
26
+ export {
27
+ labMathScope
28
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Display label for a notation entry (Phase C1b, replacing the authored
3
+ * `title`). Default = the key humanized: split on `-`/`.`, upper-case known
4
+ * acronyms, capitalize the first word. An entry may still set an explicit
5
+ * `label` where the humanized key would read wrong.
6
+ */
7
+ declare function humanizeKey(key: string): string;
8
+ /** The label an entry resolves to: an explicit `label`, else the humanized key. */
9
+ declare function resolveLabel(key: string, label?: string): string;
10
+
11
+ export { humanizeKey, resolveLabel };
@@ -0,0 +1,38 @@
1
+ const ACRONYMS = /* @__PURE__ */ new Map([
2
+ ["cds", "CDS"],
3
+ ["cdx", "CDX"],
4
+ ["isda", "ISDA"],
5
+ ["pv", "PV"],
6
+ ["npv", "NPV"],
7
+ ["lgd", "LGD"],
8
+ ["ytm", "YTM"],
9
+ ["rop", "RoP"]
10
+ ]);
11
+ const COMPOUNDS = /* @__PURE__ */ new Set([
12
+ "risk-neutral",
13
+ "real-world",
14
+ "price-yield",
15
+ "market-standard",
16
+ "risk-free"
17
+ ]);
18
+ function humanizeKey(key) {
19
+ const words = key.split(/[-.]/).filter(Boolean);
20
+ const cased = words.map((word, index) => {
21
+ const acronym = ACRONYMS.get(word);
22
+ if (acronym) return acronym;
23
+ return index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word;
24
+ });
25
+ let out = cased[0] ?? "";
26
+ for (let i = 1; i < cased.length; i += 1) {
27
+ const joiner = COMPOUNDS.has(`${words[i - 1]}-${words[i]}`) ? "-" : " ";
28
+ out += joiner + cased[i];
29
+ }
30
+ return out;
31
+ }
32
+ function resolveLabel(key, label) {
33
+ return label && label.trim().length > 0 ? label : humanizeKey(key);
34
+ }
35
+ export {
36
+ humanizeKey,
37
+ resolveLabel
38
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The one reviewed suppression registry for consistency checks (Step D6).
3
+ *
4
+ * A corpus-level finding (a glyph that resolves to more than one meaning, a
5
+ * unit outside the controlled vocabulary, …) is a violation unless it is
6
+ * acknowledged here, in `lint-ignore.yml` at the repository root, with a
7
+ * reason. Silent suppression is disallowed: there is no per-file pragma and no
8
+ * inline comment that turns a check off.
9
+ *
10
+ * File grammar — one acknowledgement per line:
11
+ *
12
+ * <check-code>[:<detail>] — <reason>
13
+ *
14
+ * `<detail>` is optional and narrows the acknowledgement (a glyph for
15
+ * `glyph-unique-in-corpus`, a notation key otherwise); with no detail the line
16
+ * acknowledges every finding of that code. The separator is an em dash (`—`),
17
+ * or `--` / ` - ` in ASCII. Blank lines and `#` comments are ignored.
18
+ */
19
+ interface LintIgnoreEntry {
20
+ readonly code: string;
21
+ readonly detail?: string;
22
+ readonly reason: string;
23
+ }
24
+ interface LintIgnore {
25
+ readonly entries: readonly LintIgnoreEntry[];
26
+ /** True when a `<code>` (optionally `<code>:<detail>`) finding is acknowledged. */
27
+ matches(code: string, detail?: string): boolean;
28
+ }
29
+ declare function parseLintIgnore(text: string): LintIgnoreEntry[];
30
+ declare function loadLintIgnore(): LintIgnore;
31
+
32
+ export { type LintIgnore, type LintIgnoreEntry, loadLintIgnore, parseLintIgnore };
@@ -0,0 +1,39 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const IGNORE_PATH = join(process.cwd(), "lint-ignore.yml");
4
+ const LINE = /^-\s*([a-z0-9-]+)(?::([^\s—-][^—-]*?))?\s*(?:—|--|\s-\s)\s*(.+?)\s*$/;
5
+ function parseLintIgnore(text) {
6
+ const entries = [];
7
+ for (const rawLine of text.split("\n")) {
8
+ const line = rawLine.replace(/#.*$/, "").trim();
9
+ if (line === "" || line === "ignore:" || line === "[]") continue;
10
+ const match = LINE.exec(line);
11
+ if (!match) {
12
+ throw new Error(
13
+ `lint-ignore.yml: expected "- <code>[:<detail>] \u2014 <reason>", got ${JSON.stringify(rawLine)}`
14
+ );
15
+ }
16
+ const [, code, detail, reason] = match;
17
+ entries.push({
18
+ code,
19
+ ...detail && detail.trim() ? { detail: detail.trim() } : {},
20
+ reason: reason.trim()
21
+ });
22
+ }
23
+ return entries;
24
+ }
25
+ function loadLintIgnore() {
26
+ const entries = existsSync(IGNORE_PATH) ? parseLintIgnore(readFileSync(IGNORE_PATH, "utf8")) : [];
27
+ return {
28
+ entries,
29
+ matches(code, detail) {
30
+ return entries.some(
31
+ (entry) => entry.code === code && (entry.detail === void 0 || entry.detail === detail)
32
+ );
33
+ }
34
+ };
35
+ }
36
+ export {
37
+ loadLintIgnore,
38
+ parseLintIgnore
39
+ };