@fragno-dev/corpus 0.0.2 → 0.0.4

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/README.md CHANGED
@@ -21,9 +21,9 @@ import { getSubjects, getSubject, getAllSubjects } from "@fragno-dev/corpus";
21
21
  const subjects = getSubjects();
22
22
  // [{ id: "defining-routes", title: "Defining Routes" }, ...]
23
23
 
24
- // Get one or more subjects
24
+ // Get one or more subjects (deterministically ordered by subject tree)
25
25
  const [routes] = getSubject("defining-routes");
26
- // { id, title, description, imports, init, examples }
26
+ // { id, title, description, imports, prelude, testInit, examples, sections }
27
27
 
28
28
  // Get multiple subjects for combined context
29
29
  const [adapters, kysely] = getSubject("database-adapters", "kysely-adapter");
@@ -40,26 +40,34 @@ Required block at the top with all imports:
40
40
  \`\`\`typescript @fragno-imports import { defineRoute } from "@fragno-dev/core"; import { z } from
41
41
  "zod"; \`\`\`
42
42
 
43
- ### @fragno-init (optional)
43
+ ### @fragno-prelude (optional)
44
44
 
45
- Initialization code needed for examples:
45
+ Setup code shown to users reading the corpus (e.g., schema definitions, configuration):
46
46
 
47
- \`\`\`typescript @fragno-init const config = { apiKey: "test-key" }; \`\`\`
47
+ \`\`\`typescript @fragno-prelude:schema const userSchema = schema((s) => { return
48
+ s.addTable("users", (t) => { return t.addColumn("id", idColumn()); }); }); \`\`\`
48
49
 
49
- ### @fragno-test:route or @fragno-test:database
50
+ Optional ID syntax (`:schema`) helps identify code blocks for agent references.
50
51
 
51
- Runnable test code with type annotations:
52
+ ### @fragno-test-init (optional)
52
53
 
53
- \`\`\`typescript @fragno-test:route // test name from first comment line const route = defineRoute({
54
- method: "GET", path: "/hello", outputSchema: z.string(), handler: async (\_, { json }) =>
55
- json("Hello"), });
54
+ Test-only initialization code (not shown to users, only used in generated tests):
56
55
 
57
- expect(route.method).toBe("GET"); \`\`\`
56
+ \`\`\`typescript @fragno-test-init const { fragment } = await
57
+ createDatabaseFragmentForTest(testFragmentDef, []); const orm = fragment.services.orm; \`\`\`
58
58
 
59
- - Use `@fragno-test:route` for route tests (uses `@fragno-dev/core/test`)
60
- - Use `@fragno-test:database` for database tests (uses `@fragno-dev/test`)
61
- - Omit `:route` or `:database` for documentation-only examples (no test generation)
59
+ ### @fragno-test
60
+
61
+ Runnable test code with optional IDs:
62
+
63
+ \`\`\`typescript @fragno-test:create-user // should create a single user const userId = await
64
+ orm.create("users", { id: "user-123", email: "john@example.com", });
65
+
66
+ expect(userId).toBeDefined(); \`\`\`
67
+
68
+ - Optional ID syntax (`:create-user`) helps agents reference specific examples
62
69
  - First comment line becomes the test name
70
+ - IDs are optional - tests generate properly without them
63
71
 
64
72
  Between code blocks, add markdown explanations.
65
73
 
@@ -67,26 +75,53 @@ Between code blocks, add markdown explanations.
67
75
 
68
76
  1. Create `src/subjects/your-subject.md`
69
77
  2. Add `@fragno-imports` block at top
70
- 3. Add optional `@fragno-init` if needed
71
- 4. Add multiple `@fragno-test` blocks with examples
72
- 5. Write explanations between code blocks
73
- 6. Run `pnpm test` to validate
78
+ 3. Add optional `@fragno-prelude` for user-visible setup code
79
+ 4. Add optional `@fragno-test-init` for test-only initialization
80
+ 5. Add multiple `@fragno-test` blocks with examples (optional IDs)
81
+ 6. Write explanations between code blocks
82
+ 7. Update `src/subject-tree.ts` to add subject to the tree structure
83
+ 8. Run `pnpm test` to validate
74
84
 
75
85
  ## Testing
76
86
 
77
87
  Tests use vitest `globalSetup` to:
78
88
 
79
89
  1. Parse all markdown files
80
- 2. Extract `@fragno-test:route` and `@fragno-test:database` blocks
90
+ 2. Extract `@fragno-prelude` (user-visible setup), `@fragno-test-init` (test-only), and
91
+ `@fragno-test` blocks
81
92
  3. Generate temporary `.test.ts` files with actual vitest test cases
82
93
  4. Type-check and execute them
83
94
 
84
- Each test block becomes a `describe()` suite with `it()` test cases. Test names come from the first
85
- comment line in each block.
95
+ Each subject's test file includes:
96
+
97
+ - Imports from `@fragno-imports`
98
+ - Setup from `@fragno-prelude` (shown to users)
99
+ - Initialization from `@fragno-test-init` (test-only)
100
+ - Test cases from `@fragno-test` blocks
101
+
102
+ Test names come from the first comment line in each `@fragno-test` block.
86
103
 
87
104
  Run tests: `pnpm test`
88
105
 
89
- Generated tests are in `.corpus-tests/` (temporary directory).
106
+ Generated tests are in `corpus-tests/` (temporary directory).
107
+
108
+ ## CLI Integration
109
+
110
+ The corpus can be viewed via the Fragno CLI with various options:
111
+
112
+ ```bash
113
+ # View a subject
114
+ fragno-cli corpus database-querying
115
+
116
+ # Show only headings and code block IDs with line numbers
117
+ fragno-cli corpus --headings database-querying
118
+
119
+ # Show specific line range with line numbers
120
+ fragno-cli corpus --line-numbers --start 1 --end 50 database-querying
121
+
122
+ # Multiple subjects (automatically ordered by subject tree)
123
+ fragno-cli corpus database-adapters kysely-adapter
124
+ ```
90
125
 
91
126
  ## TODO: Future Subjects
92
127
 
@@ -0,0 +1,98 @@
1
+ //#region src/parser.d.ts
2
+ /**
3
+ * Basic information about a subject
4
+ */
5
+ interface SubjectInfo {
6
+ id: string;
7
+ title: string;
8
+ }
9
+ /**
10
+ * A single example within a subject
11
+ */
12
+ interface Example {
13
+ code: string;
14
+ explanation: string;
15
+ testName?: string;
16
+ id?: string;
17
+ }
18
+ /**
19
+ * A code block with optional ID
20
+ */
21
+ interface CodeBlock {
22
+ code: string;
23
+ id?: string;
24
+ }
25
+ /**
26
+ * A markdown section with heading and content
27
+ */
28
+ interface Section {
29
+ heading: string;
30
+ content: string;
31
+ lineNumber?: number;
32
+ }
33
+ /**
34
+ * Complete subject with all examples and metadata
35
+ */
36
+ interface Subject {
37
+ id: string;
38
+ title: string;
39
+ description: string;
40
+ imports: string;
41
+ prelude: CodeBlock[];
42
+ testInit: CodeBlock[];
43
+ examples: Example[];
44
+ sections: Section[];
45
+ }
46
+ //#endregion
47
+ //#region src/subject-tree.d.ts
48
+ /**
49
+ * Gets the parent of a subject, or null if it's a root subject
50
+ */
51
+ declare function getSubjectParent(subjectId: string): string | null;
52
+ /**
53
+ * Gets the children of a subject
54
+ */
55
+ declare function getSubjectChildren(subjectId: string): string[];
56
+ /**
57
+ * Orders an array of subject IDs according to the tree structure
58
+ * This ensures deterministic ordering regardless of input order
59
+ */
60
+ declare function orderSubjects(subjectIds: string[]): string[];
61
+ /**
62
+ * Expands a subject ID to include its children if it has any
63
+ * Useful for when a user requests a parent topic and wants to see all related content
64
+ */
65
+ declare function expandSubjectWithChildren(subjectId: string): string[];
66
+ /**
67
+ * Gets all subject IDs in tree order
68
+ */
69
+ declare function getAllSubjectIdsInOrder(): string[];
70
+ //#endregion
71
+ //#region src/index.d.ts
72
+ /**
73
+ * Get basic information about all available subjects
74
+ * @returns Array of subject info (id and title)
75
+ */
76
+ declare function getSubjects(): SubjectInfo[];
77
+ /**
78
+ * Get one or more subjects by their IDs
79
+ * @param ids Subject IDs to load
80
+ * @returns Array of complete subject data ordered by the subject tree
81
+ * @example
82
+ * ```ts
83
+ * // Get single subject
84
+ * const [routes] = getSubject("defining-routes");
85
+ *
86
+ * // Get multiple subjects for combined context
87
+ * const [adapters, kysely] = getSubject("database-adapters", "kysely-adapter");
88
+ * ```
89
+ */
90
+ declare function getSubject(...ids: string[]): Subject[];
91
+ /**
92
+ * Get all available subjects
93
+ * @returns Array of all subjects with complete data
94
+ */
95
+ declare function getAllSubjects(): Subject[];
96
+ //#endregion
97
+ export { type CodeBlock, type Example, type Section, type Subject, type SubjectInfo, expandSubjectWithChildren, getAllSubjectIdsInOrder, getAllSubjects, getSubject, getSubjectChildren, getSubjectParent, getSubjects, orderSubjects };
98
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/parser.ts","../src/subject-tree.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;AAUA;AAQA;AAUiB,UAlBA,WAAA,CAkBS;EAQT,EAAA,EAAA,MAAO;EASP,KAAA,EAAA,MAAO;;;;;AAQL,UAnCF,OAAA,CAmCE;;;;ECLH,EAAA,CAAA,EAAA,MAAA;AAOhB;AASA;AAYA;AAWA;UD3DiB,SAAA;;;AEdjB;AAwBA;AAUA;;UFZiB,OAAA;;;;;;;;UASA,OAAA;;;;;WAKN;YACC;YACA;YACA;;;;;;;AAAO,iBCLH,gBAAA,CDKG,SAAA,EAAA,MAAA,CAAA,EAAA,MAAA,GAAA,IAAA;;;;ACLH,iBAOA,kBAAA,CAPgB,SAAA,EAAA,MAAA,CAAA,EAAA,MAAA,EAAA;AAOhC;AASA;AAYA;AAWA;iBAvBgB,aAAA;;;AClDhB;AAwBA;AAUgB,iBD4BA,yBAAA,CC5ByB,SAAA,EAAA,MAAA,CAAA,EAAA,MAAA,EAAA;;;;iBDuCzB,uBAAA,CAAA;;;AD7EhB;AAQA;AAUA;AAQA;AASiB,iBE/BD,WAAA,CAAA,CF+BQ,EE/BO,WF+BP,EAAA;;;;;;;;;ACGxB;AAOA;AASA;AAYA;AAWA;iBCjDgB,UAAA,oBAA8B;;;AAxB9C;AAwBA;AAUgB,iBAAA,cAAA,CAAA,CAAyB,EAAP,OAAO,EAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,247 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ //#region src/parser.ts
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const SUBJECTS_DIR = (() => {
8
+ const distRelative = join(__dirname, "..", "src", "subjects");
9
+ try {
10
+ readdirSync(distRelative);
11
+ return distRelative;
12
+ } catch {
13
+ return join(__dirname, "subjects");
14
+ }
15
+ })();
16
+ /**
17
+ * Helper function to extract code blocks with optional IDs from a directive
18
+ */
19
+ function extractCodeBlocks(content, directive) {
20
+ const regex = new RegExp(`\`\`\`typescript @fragno-${directive}(?::(\\w+(?:-\\w+)*))?\\n([\\s\\S]*?)\`\`\``, "g");
21
+ const blocks = [];
22
+ let match;
23
+ while ((match = regex.exec(content)) !== null) {
24
+ const id = match[1] || void 0;
25
+ const code = match[2].trim();
26
+ blocks.push({
27
+ code,
28
+ id
29
+ });
30
+ }
31
+ return blocks;
32
+ }
33
+ /**
34
+ * Parses a markdown file and extracts structured content
35
+ */
36
+ function parseMarkdownFile(content) {
37
+ const titleMatch = content.match(/^#\s+(.+)$/m);
38
+ const title = titleMatch ? titleMatch[1].trim() : "Untitled";
39
+ const importsMatch = content.match(/```typescript @fragno-imports\n([\s\S]*?)```/);
40
+ const imports = importsMatch ? importsMatch[1].trim() : "";
41
+ const prelude = extractCodeBlocks(content, "prelude");
42
+ const testInit = extractCodeBlocks(content, "test-init");
43
+ const testBlockRegex = /```typescript @fragno-test(?::(\w+(?:-\w+)*))?\n([\s\S]*?)```([\s\S]*?)(?=```typescript @fragno-test|$)/g;
44
+ const testBlocks = [];
45
+ let match;
46
+ while ((match = testBlockRegex.exec(content)) !== null) {
47
+ const id = match[1] || void 0;
48
+ const code = match[2].trim();
49
+ const lines = code.split("\n");
50
+ let testName;
51
+ if (lines[0]?.trim().startsWith("//")) testName = lines[0].replace(/^\/\/\s*/, "").trim();
52
+ const explanation = match[3].split(/```/)[0].trim();
53
+ testBlocks.push({
54
+ code,
55
+ explanation,
56
+ testName,
57
+ id
58
+ });
59
+ }
60
+ const descriptionMatch = content.substring(content.indexOf(title) + title.length).match(/\n\n([\s\S]*?)(?=```|##|$)/);
61
+ const description = descriptionMatch ? descriptionMatch[1].trim() : "";
62
+ const sections = [];
63
+ const matches = [...content.matchAll(/^##\s+(.+)$/gm)];
64
+ for (let i = 0; i < matches.length; i++) {
65
+ const match$1 = matches[i];
66
+ const heading = match$1[1].trim();
67
+ const sectionStart = match$1.index + match$1[0].length;
68
+ const nextSectionStart = matches[i + 1]?.index ?? content.length;
69
+ let sectionContent = content.substring(sectionStart, nextSectionStart).trim();
70
+ sectionContent = sectionContent.replace(/```typescript @fragno-\w+(?::\w+(?:-\w+)*)?/g, "```typescript");
71
+ sectionContent = sectionContent.trim();
72
+ if (sectionContent) sections.push({
73
+ heading,
74
+ content: sectionContent
75
+ });
76
+ }
77
+ return {
78
+ title,
79
+ description,
80
+ imports,
81
+ prelude,
82
+ testInit,
83
+ testBlocks,
84
+ sections
85
+ };
86
+ }
87
+ /**
88
+ * Converts parsed markdown to a Subject
89
+ */
90
+ function markdownToSubject(id, parsed) {
91
+ const examples = parsed.testBlocks.map((block) => ({
92
+ code: block.code,
93
+ explanation: block.explanation,
94
+ testName: block.testName,
95
+ id: block.id
96
+ }));
97
+ return {
98
+ id,
99
+ title: parsed.title,
100
+ description: parsed.description,
101
+ imports: parsed.imports,
102
+ prelude: parsed.prelude,
103
+ testInit: parsed.testInit,
104
+ examples,
105
+ sections: parsed.sections
106
+ };
107
+ }
108
+ /**
109
+ * Loads and parses a subject file by ID
110
+ */
111
+ function loadSubject(id) {
112
+ return markdownToSubject(id, parseMarkdownFile(readFileSync(join(SUBJECTS_DIR, `${id}.md`), "utf-8")));
113
+ }
114
+ /**
115
+ * Gets all available subject IDs from the subjects directory
116
+ */
117
+ function getAvailableSubjectIds() {
118
+ return readdirSync(SUBJECTS_DIR).filter((file) => file.endsWith(".md")).map((file) => basename(file, ".md"));
119
+ }
120
+ /**
121
+ * Loads multiple subjects by their IDs
122
+ */
123
+ function loadSubjects(ids) {
124
+ return ids.map((id) => loadSubject(id));
125
+ }
126
+ /**
127
+ * Loads all available subjects
128
+ */
129
+ function loadAllSubjects() {
130
+ return loadSubjects(getAvailableSubjectIds());
131
+ }
132
+
133
+ //#endregion
134
+ //#region src/subject-tree.ts
135
+ /**
136
+ * Tree structure defining subject hierarchy and ordering
137
+ * - Root-level subjects are listed in order
138
+ * - Children are indented under their parents
139
+ */
140
+ const SUBJECT_TREE = [
141
+ { id: "defining-routes" },
142
+ { id: "fragment-services" },
143
+ { id: "fragment-instantiation" },
144
+ { id: "database-querying" },
145
+ {
146
+ id: "database-adapters",
147
+ children: ["kysely-adapter", "drizzle-adapter"]
148
+ }
149
+ ];
150
+ /**
151
+ * Flattened map of all subjects and their parent relationships
152
+ */
153
+ const SUBJECT_PARENT_MAP = /* @__PURE__ */ new Map();
154
+ const SUBJECT_ORDER_MAP = /* @__PURE__ */ new Map();
155
+ let orderIndex = 0;
156
+ for (const node of SUBJECT_TREE) {
157
+ SUBJECT_PARENT_MAP.set(node.id, null);
158
+ SUBJECT_ORDER_MAP.set(node.id, orderIndex++);
159
+ if (node.children) for (const childId of node.children) {
160
+ SUBJECT_PARENT_MAP.set(childId, node.id);
161
+ SUBJECT_ORDER_MAP.set(childId, orderIndex++);
162
+ }
163
+ }
164
+ /**
165
+ * Gets the parent of a subject, or null if it's a root subject
166
+ */
167
+ function getSubjectParent(subjectId) {
168
+ return SUBJECT_PARENT_MAP.get(subjectId) ?? null;
169
+ }
170
+ /**
171
+ * Gets the children of a subject
172
+ */
173
+ function getSubjectChildren(subjectId) {
174
+ return SUBJECT_TREE.find((n) => n.id === subjectId)?.children ?? [];
175
+ }
176
+ /**
177
+ * Orders an array of subject IDs according to the tree structure
178
+ * This ensures deterministic ordering regardless of input order
179
+ */
180
+ function orderSubjects(subjectIds) {
181
+ return [...subjectIds].sort((a, b) => {
182
+ return (SUBJECT_ORDER_MAP.get(a) ?? Number.MAX_SAFE_INTEGER) - (SUBJECT_ORDER_MAP.get(b) ?? Number.MAX_SAFE_INTEGER);
183
+ });
184
+ }
185
+ /**
186
+ * Expands a subject ID to include its children if it has any
187
+ * Useful for when a user requests a parent topic and wants to see all related content
188
+ */
189
+ function expandSubjectWithChildren(subjectId) {
190
+ const children = getSubjectChildren(subjectId);
191
+ if (children.length > 0) return [subjectId, ...children];
192
+ return [subjectId];
193
+ }
194
+ /**
195
+ * Gets all subject IDs in tree order
196
+ */
197
+ function getAllSubjectIdsInOrder() {
198
+ const ids = [];
199
+ for (const node of SUBJECT_TREE) {
200
+ ids.push(node.id);
201
+ if (node.children) ids.push(...node.children);
202
+ }
203
+ return ids;
204
+ }
205
+
206
+ //#endregion
207
+ //#region src/index.ts
208
+ /**
209
+ * Get basic information about all available subjects
210
+ * @returns Array of subject info (id and title)
211
+ */
212
+ function getSubjects() {
213
+ return getAvailableSubjectIds().map((id) => {
214
+ const subject = loadSubject(id);
215
+ return {
216
+ id: subject.id,
217
+ title: subject.title
218
+ };
219
+ });
220
+ }
221
+ /**
222
+ * Get one or more subjects by their IDs
223
+ * @param ids Subject IDs to load
224
+ * @returns Array of complete subject data ordered by the subject tree
225
+ * @example
226
+ * ```ts
227
+ * // Get single subject
228
+ * const [routes] = getSubject("defining-routes");
229
+ *
230
+ * // Get multiple subjects for combined context
231
+ * const [adapters, kysely] = getSubject("database-adapters", "kysely-adapter");
232
+ * ```
233
+ */
234
+ function getSubject(...ids) {
235
+ return loadSubjects(orderSubjects(ids));
236
+ }
237
+ /**
238
+ * Get all available subjects
239
+ * @returns Array of all subjects with complete data
240
+ */
241
+ function getAllSubjects() {
242
+ return loadAllSubjects();
243
+ }
244
+
245
+ //#endregion
246
+ export { expandSubjectWithChildren, getAllSubjectIdsInOrder, getAllSubjects, getSubject, getSubjectChildren, getSubjectParent, getSubjects, orderSubjects };
247
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["blocks: CodeBlock[]","testBlocks: Array<{\n code: string;\n explanation: string;\n testName?: string;\n id?: string;\n }>","testName: string | undefined","sections: Section[]","match","examples: Example[]","SUBJECT_TREE: SubjectNode[]","ids: string[]"],"sources":["../src/parser.ts","../src/subject-tree.ts","../src/index.ts"],"sourcesContent":["import { readFileSync, readdirSync } from \"node:fs\";\nimport { join, basename, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n/**\n * Basic information about a subject\n */\nexport interface SubjectInfo {\n id: string;\n title: string;\n}\n\n/**\n * A single example within a subject\n */\nexport interface Example {\n code: string;\n explanation: string;\n testName?: string;\n id?: string;\n}\n\n/**\n * A code block with optional ID\n */\nexport interface CodeBlock {\n code: string;\n id?: string;\n}\n\n/**\n * A markdown section with heading and content\n */\nexport interface Section {\n heading: string;\n content: string;\n lineNumber?: number;\n}\n\n/**\n * Complete subject with all examples and metadata\n */\nexport interface Subject {\n id: string;\n title: string;\n description: string;\n imports: string;\n prelude: CodeBlock[];\n testInit: CodeBlock[];\n examples: Example[];\n sections: Section[];\n}\n\n/**\n * Raw parsed data from markdown before processing\n */\nexport interface ParsedMarkdown {\n title: string;\n description: string;\n imports: string;\n prelude: CodeBlock[];\n testInit: CodeBlock[];\n testBlocks: Array<{\n code: string;\n explanation: string;\n testName?: string;\n id?: string;\n }>;\n sections: Section[];\n}\n\n// Look for subjects directory in source or relative to built dist\nconst SUBJECTS_DIR = (() => {\n // Try dist/../src/subjects (when running from built code)\n const distRelative = join(__dirname, \"..\", \"src\", \"subjects\");\n try {\n readdirSync(distRelative);\n return distRelative;\n } catch {\n // Fall back to ./subjects (when running from source)\n return join(__dirname, \"subjects\");\n }\n})();\n\n/**\n * Helper function to extract code blocks with optional IDs from a directive\n */\nfunction extractCodeBlocks(content: string, directive: string): CodeBlock[] {\n const regex = new RegExp(\n `\\`\\`\\`typescript @fragno-${directive}(?::(\\\\w+(?:-\\\\w+)*))?\\\\n([\\\\s\\\\S]*?)\\`\\`\\``,\n \"g\",\n );\n const blocks: CodeBlock[] = [];\n\n let match;\n while ((match = regex.exec(content)) !== null) {\n const id = match[1] || undefined;\n const code = match[2].trim();\n blocks.push({ code, id });\n }\n\n return blocks;\n}\n\n/**\n * Parses a markdown file and extracts structured content\n */\nexport function parseMarkdownFile(content: string): ParsedMarkdown {\n // Extract title (first # heading)\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n const title = titleMatch ? titleMatch[1].trim() : \"Untitled\";\n\n // Extract imports block\n const importsMatch = content.match(/```typescript @fragno-imports\\n([\\s\\S]*?)```/);\n const imports = importsMatch ? importsMatch[1].trim() : \"\";\n\n // Extract prelude blocks\n const prelude = extractCodeBlocks(content, \"prelude\");\n\n // Extract test-init blocks\n const testInit = extractCodeBlocks(content, \"test-init\");\n\n // Extract all test blocks with their explanations and optional IDs\n const testBlockRegex =\n /```typescript @fragno-test(?::(\\w+(?:-\\w+)*))?\\n([\\s\\S]*?)```([\\s\\S]*?)(?=```typescript @fragno-test|$)/g;\n const testBlocks: Array<{\n code: string;\n explanation: string;\n testName?: string;\n id?: string;\n }> = [];\n\n let match;\n while ((match = testBlockRegex.exec(content)) !== null) {\n const id = match[1] || undefined;\n const code = match[2].trim();\n\n // Extract test name from first line if it's a comment\n const lines = code.split(\"\\n\");\n let testName: string | undefined;\n if (lines[0]?.trim().startsWith(\"//\")) {\n testName = lines[0].replace(/^\\/\\/\\s*/, \"\").trim();\n }\n\n // Get explanation text after the code block until next code block or end\n const afterBlock = match[3];\n const explanation = afterBlock\n .split(/```/)[0] // Stop at next code block\n .trim();\n\n testBlocks.push({ code, explanation, testName, id });\n }\n\n // Extract description (everything between title and first code block or ## heading)\n const afterTitle = content.substring(content.indexOf(title) + title.length);\n const descriptionMatch = afterTitle.match(/\\n\\n([\\s\\S]*?)(?=```|##|$)/);\n const description = descriptionMatch ? descriptionMatch[1].trim() : \"\";\n\n // Extract all sections (## headings and their content)\n const sections: Section[] = [];\n const sectionRegex = /^##\\s+(.+)$/gm;\n const matches = [...content.matchAll(sectionRegex)];\n\n for (let i = 0; i < matches.length; i++) {\n const match = matches[i];\n const heading = match[1].trim();\n const sectionStart = match.index! + match[0].length;\n const nextSectionStart = matches[i + 1]?.index ?? content.length;\n let sectionContent = content.substring(sectionStart, nextSectionStart).trim();\n\n // Convert @fragno directive code blocks to regular typescript blocks for display\n sectionContent = sectionContent.replace(\n /```typescript @fragno-\\w+(?::\\w+(?:-\\w+)*)?/g,\n \"```typescript\",\n );\n sectionContent = sectionContent.trim();\n\n if (sectionContent) {\n sections.push({ heading, content: sectionContent });\n }\n }\n\n return {\n title,\n description,\n imports,\n prelude,\n testInit,\n testBlocks,\n sections,\n };\n}\n\n/**\n * Converts parsed markdown to a Subject\n */\nexport function markdownToSubject(id: string, parsed: ParsedMarkdown): Subject {\n const examples: Example[] = parsed.testBlocks.map((block) => ({\n code: block.code,\n explanation: block.explanation,\n testName: block.testName,\n id: block.id,\n }));\n\n return {\n id,\n title: parsed.title,\n description: parsed.description,\n imports: parsed.imports,\n prelude: parsed.prelude,\n testInit: parsed.testInit,\n examples,\n sections: parsed.sections,\n };\n}\n\n/**\n * Loads and parses a subject file by ID\n */\nexport function loadSubject(id: string): Subject {\n const filePath = join(SUBJECTS_DIR, `${id}.md`);\n const content = readFileSync(filePath, \"utf-8\");\n const parsed = parseMarkdownFile(content);\n return markdownToSubject(id, parsed);\n}\n\n/**\n * Gets all available subject IDs from the subjects directory\n */\nexport function getAvailableSubjectIds(): string[] {\n const files = readdirSync(SUBJECTS_DIR);\n return files.filter((file) => file.endsWith(\".md\")).map((file) => basename(file, \".md\"));\n}\n\n/**\n * Loads multiple subjects by their IDs\n */\nexport function loadSubjects(ids: string[]): Subject[] {\n return ids.map((id) => loadSubject(id));\n}\n\n/**\n * Loads all available subjects\n */\nexport function loadAllSubjects(): Subject[] {\n const ids = getAvailableSubjectIds();\n return loadSubjects(ids);\n}\n","/**\n * Subject tree structure defining relationships and ordering\n */\n\nexport interface SubjectNode {\n id: string;\n children?: string[];\n}\n\n/**\n * Tree structure defining subject hierarchy and ordering\n * - Root-level subjects are listed in order\n * - Children are indented under their parents\n */\nconst SUBJECT_TREE: SubjectNode[] = [\n { id: \"defining-routes\" },\n { id: \"fragment-services\" },\n { id: \"fragment-instantiation\" },\n { id: \"database-querying\" },\n {\n id: \"database-adapters\",\n children: [\"kysely-adapter\", \"drizzle-adapter\"],\n },\n];\n\n/**\n * Flattened map of all subjects and their parent relationships\n */\nconst SUBJECT_PARENT_MAP = new Map<string, string | null>();\nconst SUBJECT_ORDER_MAP = new Map<string, number>();\n\n// Build the parent and order maps\nlet orderIndex = 0;\nfor (const node of SUBJECT_TREE) {\n SUBJECT_PARENT_MAP.set(node.id, null);\n SUBJECT_ORDER_MAP.set(node.id, orderIndex++);\n\n if (node.children) {\n for (const childId of node.children) {\n SUBJECT_PARENT_MAP.set(childId, node.id);\n SUBJECT_ORDER_MAP.set(childId, orderIndex++);\n }\n }\n}\n\n/**\n * Gets the parent of a subject, or null if it's a root subject\n */\nexport function getSubjectParent(subjectId: string): string | null {\n return SUBJECT_PARENT_MAP.get(subjectId) ?? null;\n}\n\n/**\n * Gets the children of a subject\n */\nexport function getSubjectChildren(subjectId: string): string[] {\n const node = SUBJECT_TREE.find((n) => n.id === subjectId);\n return node?.children ?? [];\n}\n\n/**\n * Orders an array of subject IDs according to the tree structure\n * This ensures deterministic ordering regardless of input order\n */\nexport function orderSubjects(subjectIds: string[]): string[] {\n return [...subjectIds].sort((a, b) => {\n const orderA = SUBJECT_ORDER_MAP.get(a) ?? Number.MAX_SAFE_INTEGER;\n const orderB = SUBJECT_ORDER_MAP.get(b) ?? Number.MAX_SAFE_INTEGER;\n return orderA - orderB;\n });\n}\n\n/**\n * Expands a subject ID to include its children if it has any\n * Useful for when a user requests a parent topic and wants to see all related content\n */\nexport function expandSubjectWithChildren(subjectId: string): string[] {\n const children = getSubjectChildren(subjectId);\n if (children.length > 0) {\n return [subjectId, ...children];\n }\n return [subjectId];\n}\n\n/**\n * Gets all subject IDs in tree order\n */\nexport function getAllSubjectIdsInOrder(): string[] {\n const ids: string[] = [];\n for (const node of SUBJECT_TREE) {\n ids.push(node.id);\n if (node.children) {\n ids.push(...node.children);\n }\n }\n return ids;\n}\n","import {\n getAvailableSubjectIds,\n loadSubject,\n loadSubjects,\n loadAllSubjects,\n type SubjectInfo,\n type Subject,\n} from \"./parser\";\nimport { orderSubjects } from \"./subject-tree\";\n\n/**\n * Get basic information about all available subjects\n * @returns Array of subject info (id and title)\n */\nexport function getSubjects(): SubjectInfo[] {\n const ids = getAvailableSubjectIds();\n return ids.map((id) => {\n const subject = loadSubject(id);\n return {\n id: subject.id,\n title: subject.title,\n };\n });\n}\n\n/**\n * Get one or more subjects by their IDs\n * @param ids Subject IDs to load\n * @returns Array of complete subject data ordered by the subject tree\n * @example\n * ```ts\n * // Get single subject\n * const [routes] = getSubject(\"defining-routes\");\n *\n * // Get multiple subjects for combined context\n * const [adapters, kysely] = getSubject(\"database-adapters\", \"kysely-adapter\");\n * ```\n */\nexport function getSubject(...ids: string[]): Subject[] {\n // Order subjects deterministically according to the tree structure\n const orderedIds = orderSubjects(ids);\n return loadSubjects(orderedIds);\n}\n\n/**\n * Get all available subjects\n * @returns Array of all subjects with complete data\n */\nexport function getAllSubjects(): Subject[] {\n return loadAllSubjects();\n}\n\n// Re-export types\nexport type { Subject, SubjectInfo, Example, Section, CodeBlock } from \"./parser.js\";\n\n// Re-export subject tree utilities\nexport {\n orderSubjects,\n getSubjectParent,\n getSubjectChildren,\n expandSubjectWithChildren,\n getAllSubjectIdsInOrder,\n} from \"./subject-tree.js\";\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragno-dev/corpus",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -12,15 +12,20 @@
12
12
  "default": "./dist/index.js"
13
13
  }
14
14
  },
15
+ "files": [
16
+ "dist"
17
+ ],
15
18
  "devDependencies": {
16
- "zod": "^4.0.5",
19
+ "@types/marked-terminal": "^6.1.1",
17
20
  "@types/node": "^22",
18
21
  "drizzle-orm": "^0.44.7",
19
22
  "kysely": "^0.28.0",
23
+ "zod": "^4.0.5",
24
+ "@fragno-dev/core": "0.1.8",
25
+ "@fragno-dev/db": "0.1.14",
26
+ "@fragno-dev/test": "0.1.12",
20
27
  "@fragno-private/typescript-config": "0.0.1",
21
- "@fragno-private/vitest-config": "0.0.0",
22
- "@fragno-dev/core": "0.1.6",
23
- "@fragno-dev/db": "0.1.12"
28
+ "@fragno-private/vitest-config": "0.0.0"
24
29
  },
25
30
  "scripts": {
26
31
  "build": "tsdown",
@@ -1,15 +0,0 @@
1
-
2
- > @fragno-dev/corpus@0.0.2 build /home/runner/work/fragno/fragno/packages/corpus
3
- > tsdown
4
-
5
- ℹ tsdown v0.15.12 powered by rolldown v1.0.0-beta.45
6
- ℹ Using tsdown config: /home/runner/work/fragno/fragno/packages/corpus/tsdown.config.ts
7
- ℹ entry: src/index.ts
8
- ℹ tsconfig: tsconfig.json
9
- ℹ Build start
10
- ℹ dist/index.js 3.84 kB │ gzip: 1.42 kB
11
- ℹ dist/index.js.map 8.62 kB │ gzip: 2.81 kB
12
- ℹ dist/index.d.ts.map 0.34 kB │ gzip: 0.22 kB
13
- ℹ dist/index.d.ts 1.39 kB │ gzip: 0.57 kB
14
- ℹ 4 files, total: 14.18 kB
15
- ✔ Build complete in 7620ms
package/CHANGELOG.md DELETED
@@ -1,7 +0,0 @@
1
- # @fragno-dev/corpus
2
-
3
- ## 0.0.2
4
-
5
- ### Patch Changes
6
-
7
- - 27cc540: fix: Corpus dependency issue
package/src/index.test.ts DELETED
@@ -1,107 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { getSubjects, getSubject, getAllSubjects } from "./index.js";
3
-
4
- describe("corpus API", () => {
5
- describe("getSubjects", () => {
6
- it("should return array of subject info", () => {
7
- const subjects = getSubjects();
8
-
9
- expect(subjects).toBeInstanceOf(Array);
10
- expect(subjects.length).toBeGreaterThan(0);
11
-
12
- // Check structure of first subject
13
- const first = subjects[0];
14
- expect(first).toHaveProperty("id");
15
- expect(first).toHaveProperty("title");
16
- expect(typeof first.id).toBe("string");
17
- expect(typeof first.title).toBe("string");
18
- });
19
-
20
- it("should include expected subjects", () => {
21
- const subjects = getSubjects();
22
- const ids = subjects.map((s) => s.id);
23
-
24
- expect(ids).toContain("defining-routes");
25
- expect(ids).toContain("database-querying");
26
- expect(ids).toContain("database-adapters");
27
- expect(ids).toContain("kysely-adapter");
28
- expect(ids).toContain("drizzle-adapter");
29
- });
30
- });
31
-
32
- describe("getSubject", () => {
33
- it("should return single subject when given one id", () => {
34
- const [subject] = getSubject("defining-routes");
35
-
36
- expect(subject).toBeDefined();
37
- expect(subject.id).toBe("defining-routes");
38
- expect(subject.title).toBeTruthy();
39
- expect(subject.description).toBeTruthy();
40
- expect(subject.imports).toBeTruthy();
41
- expect(subject.examples).toBeInstanceOf(Array);
42
- expect(subject.examples.length).toBeGreaterThan(0);
43
- });
44
-
45
- it("should return multiple subjects when given multiple ids", () => {
46
- const subjects = getSubject("database-adapters", "kysely-adapter");
47
-
48
- expect(subjects).toHaveLength(2);
49
- expect(subjects[0].id).toBe("database-adapters");
50
- expect(subjects[1].id).toBe("kysely-adapter");
51
- });
52
-
53
- it("should include examples with code and explanation", () => {
54
- const [subject] = getSubject("defining-routes");
55
-
56
- const example = subject.examples[0];
57
- expect(example).toHaveProperty("code");
58
- expect(example).toHaveProperty("explanation");
59
- expect(typeof example.code).toBe("string");
60
- expect(typeof example.explanation).toBe("string");
61
- });
62
- });
63
-
64
- describe("getAllSubjects", () => {
65
- it("should return all available subjects", () => {
66
- const allSubjects = getAllSubjects();
67
- const subjectsList = getSubjects();
68
-
69
- expect(allSubjects).toHaveLength(subjectsList.length);
70
- });
71
-
72
- it("should return complete subject data", () => {
73
- const allSubjects = getAllSubjects();
74
-
75
- for (const subject of allSubjects) {
76
- expect(subject.id).toBeTruthy();
77
- expect(subject.title).toBeTruthy();
78
- expect(subject.imports).toBeDefined(); // Can be empty string
79
- expect(subject.examples).toBeInstanceOf(Array);
80
- }
81
- });
82
- });
83
-
84
- describe("subject content validation", () => {
85
- it("defining-routes should have multiple examples", () => {
86
- const [subject] = getSubject("defining-routes");
87
-
88
- expect(subject.examples.length).toBeGreaterThan(3);
89
- expect(subject.imports).toContain("defineRoute");
90
- });
91
-
92
- it("database-querying should have database content", () => {
93
- const [subject] = getSubject("database-querying");
94
-
95
- // Database querying examples are currently documentation-only
96
- expect(subject.title).toBe("Database Querying");
97
- expect(subject.imports).toContain("defineFragnoDatabase");
98
- });
99
-
100
- it("database-adapters should have adapter overview", () => {
101
- const [subject] = getSubject("database-adapters");
102
-
103
- expect(subject.description).toContain("adapter");
104
- expect(subject.imports).toContain("DatabaseAdapter");
105
- });
106
- });
107
- });