@pithyjs/codex 0.1.0-beta.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 (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4401 -0
  3. package/dist/annotations.d.ts +106 -0
  4. package/dist/annotations.js +306 -0
  5. package/dist/apply.d.ts +2 -0
  6. package/dist/apply.js +80 -0
  7. package/dist/changed-scope.d.ts +23 -0
  8. package/dist/changed-scope.js +31 -0
  9. package/dist/check.d.ts +2 -0
  10. package/dist/check.js +117 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +67 -0
  13. package/dist/env.d.ts +5 -0
  14. package/dist/env.js +35 -0
  15. package/dist/extract-cli.d.ts +23 -0
  16. package/dist/extract-cli.js +192 -0
  17. package/dist/extraction/breaking-changes.d.ts +66 -0
  18. package/dist/extraction/breaking-changes.js +352 -0
  19. package/dist/extraction/example-extractor.d.ts +55 -0
  20. package/dist/extraction/example-extractor.js +272 -0
  21. package/dist/extraction/index.d.ts +27 -0
  22. package/dist/extraction/index.js +31 -0
  23. package/dist/extraction/jsdoc-parser.d.ts +43 -0
  24. package/dist/extraction/jsdoc-parser.js +274 -0
  25. package/dist/extraction/pipeline.d.ts +54 -0
  26. package/dist/extraction/pipeline.js +526 -0
  27. package/dist/extraction/readme-sync.d.ts +108 -0
  28. package/dist/extraction/readme-sync.js +592 -0
  29. package/dist/extraction/snapshot-store.d.ts +40 -0
  30. package/dist/extraction/snapshot-store.js +153 -0
  31. package/dist/extraction/source-linker.d.ts +80 -0
  32. package/dist/extraction/source-linker.js +316 -0
  33. package/dist/extraction/test-example-extractor.d.ts +69 -0
  34. package/dist/extraction/test-example-extractor.js +400 -0
  35. package/dist/extraction/test-pattern-extractor.d.ts +68 -0
  36. package/dist/extraction/test-pattern-extractor.js +261 -0
  37. package/dist/extraction/testing-pyramid.d.ts +44 -0
  38. package/dist/extraction/testing-pyramid.js +163 -0
  39. package/dist/extraction/type-extractor.d.ts +34 -0
  40. package/dist/extraction/type-extractor.js +494 -0
  41. package/dist/extraction/types.d.ts +401 -0
  42. package/dist/extraction/types.js +34 -0
  43. package/dist/indexer.d.ts +1 -0
  44. package/dist/indexer.js +107 -0
  45. package/dist/llm.d.ts +8 -0
  46. package/dist/llm.js +75 -0
  47. package/dist/readme-sync-cli.d.ts +20 -0
  48. package/dist/readme-sync-cli.js +167 -0
  49. package/dist/review.d.ts +2 -0
  50. package/dist/review.js +93 -0
  51. package/dist/scan.d.ts +29 -0
  52. package/dist/scan.js +221 -0
  53. package/dist/schema.d.ts +169 -0
  54. package/dist/schema.js +70 -0
  55. package/dist/snapshot-cli.d.ts +45 -0
  56. package/dist/snapshot-cli.js +217 -0
  57. package/dist/sync-cli.d.ts +22 -0
  58. package/dist/sync-cli.js +154 -0
  59. package/dist/sync-pipeline.d.ts +58 -0
  60. package/dist/sync-pipeline.js +104 -0
  61. package/dist/sync.d.ts +2 -0
  62. package/dist/sync.js +318 -0
  63. package/dist/validate-cli.d.ts +20 -0
  64. package/dist/validate-cli.js +144 -0
  65. package/dist/validate.d.ts +76 -0
  66. package/dist/validate.js +183 -0
  67. package/dist/watch-cli.d.ts +21 -0
  68. package/dist/watch-cli.js +220 -0
  69. package/package.json +62 -0
@@ -0,0 +1,400 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.extraction.test-example-extractor",
5
+ * "title": "Test Example Extractor",
6
+ * "category": "compiler"
7
+ * }
8
+ *
9
+ * Extracts examples from test files marked with @codex:example
10
+ * and tracks test status from vitest/jest output.
11
+ */
12
+ import { normalize } from 'node:path';
13
+ /**
14
+ * Marker pattern for identifying codex examples in test files
15
+ *
16
+ * Supports formats:
17
+ * - @codex:example pithy.signals.effect
18
+ * - @codex:example("pithy.signals.effect")
19
+ * - @codex:example { entry: "pithy.signals.effect" }
20
+ */
21
+ const CODEX_EXAMPLE_MARKER = /@codex:example\s*(?:\(\s*["']([^"']+)["']\s*\)|{[^}]*entry:\s*["']([^"']+)["'][^}]*}|([\w-]+(?:\.[\w-]+)+))/g;
22
+ /**
23
+ * @codexApi {"parent":"pithy.codex.extraction.test-example-extractor","name":"extractTestExamples","stability":"stable","signature":"(content: string, filePath: string) => TestExample[]"}
24
+ *
25
+ * Extracts examples marked with @codex:example from a test file
26
+ * @param content - Test file content
27
+ * @param filePath - Path to the test file
28
+ * @returns Array of extracted test examples
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * // In a test file:
33
+ * // @codex:example pithy.signals.effect
34
+ * it("creates an effect that tracks dependencies", () => {
35
+ * const count = signal(0);
36
+ * let runCount = 0;
37
+ * effect(() => { count(); runCount++; });
38
+ * expect(runCount).toBe(1);
39
+ * });
40
+ * ```
41
+ */
42
+ export function extractTestExamples(content, filePath) {
43
+ const examples = [];
44
+ const lines = content.split('\n');
45
+ // Find all @codex:example markers
46
+ for (let i = 0; i < lines.length; i++) {
47
+ const line = lines[i];
48
+ CODEX_EXAMPLE_MARKER.lastIndex = 0;
49
+ const match = CODEX_EXAMPLE_MARKER.exec(line);
50
+ if (match) {
51
+ // Extract entry ID from any of the capture groups
52
+ const entryId = match[1] || match[2] || match[3];
53
+ if (!entryId)
54
+ continue;
55
+ // Look for the next it/test/describe block
56
+ const testBlock = findNextTestBlock(lines, i + 1);
57
+ if (!testBlock)
58
+ continue;
59
+ examples.push({
60
+ entryId: entryId.trim(),
61
+ name: testBlock.name,
62
+ code: testBlock.code,
63
+ location: {
64
+ file: filePath,
65
+ line: testBlock.startLine + 1, // 1-indexed
66
+ endLine: testBlock.endLine + 1,
67
+ },
68
+ testFile: filePath,
69
+ status: 'unknown',
70
+ });
71
+ }
72
+ }
73
+ return examples;
74
+ }
75
+ /**
76
+ * Counts brace depth change for a line, skipping braces inside strings,
77
+ * template literals, and comments (including multi-line block comments).
78
+ *
79
+ * @param line - The line to scan
80
+ * @param inBlockComment - Whether we are currently inside a multi-line block comment
81
+ * @returns Brace counts and updated block comment state
82
+ */
83
+ function countBraces(line, inBlockComment = false) {
84
+ let open = 0;
85
+ let close = 0;
86
+ let i = 0;
87
+ while (i < line.length) {
88
+ // If inside a multi-line block comment, scan for closing */
89
+ if (inBlockComment) {
90
+ const end = line.indexOf('*/', i);
91
+ if (end === -1) {
92
+ // Entire remaining line is still inside the comment
93
+ return { open, close, inBlockComment: true };
94
+ }
95
+ inBlockComment = false;
96
+ i = end + 2;
97
+ continue;
98
+ }
99
+ const ch = line[i];
100
+ // Skip single-line comments
101
+ if (ch === '/' && line[i + 1] === '/')
102
+ break;
103
+ // Block comment opening
104
+ if (ch === '/' && line[i + 1] === '*') {
105
+ const end = line.indexOf('*/', i + 2);
106
+ if (end === -1) {
107
+ // Block comment spans to subsequent lines
108
+ return { open, close, inBlockComment: true };
109
+ }
110
+ i = end + 2;
111
+ continue;
112
+ }
113
+ // Skip string/template literals
114
+ if (ch === '"' || ch === "'" || ch === '`') {
115
+ const quote = ch;
116
+ i++;
117
+ while (i < line.length) {
118
+ if (line[i] === '\\') {
119
+ i += 2; // skip escaped character
120
+ continue;
121
+ }
122
+ if (line[i] === quote) {
123
+ i++;
124
+ break;
125
+ }
126
+ i++;
127
+ }
128
+ continue;
129
+ }
130
+ if (ch === '{')
131
+ open++;
132
+ else if (ch === '}')
133
+ close++;
134
+ i++;
135
+ }
136
+ return { open, close, inBlockComment };
137
+ }
138
+ /**
139
+ * Finds the next test block (it/test/describe) starting from a given line
140
+ */
141
+ function findNextTestBlock(lines, startLine) {
142
+ for (let i = startLine; i < lines.length; i++) {
143
+ const line = lines[i];
144
+ // Match it/test/describe patterns (handles escaped quotes in test names)
145
+ const testMatch = line.match(/(?:it|test|describe)\s*\(\s*(['"`])((?:\\.|(?!\1).)+)\1/);
146
+ if (testMatch) {
147
+ const name = testMatch[2];
148
+ const blockStart = i;
149
+ // Find the closing of this block using context-aware brace counting
150
+ let depth = 0;
151
+ let foundFirstBrace = false;
152
+ let blockEnd = i;
153
+ let commentState = false;
154
+ for (let j = i; j < lines.length; j++) {
155
+ const result = countBraces(lines[j], commentState);
156
+ commentState = result.inBlockComment;
157
+ if (result.open > 0)
158
+ foundFirstBrace = true;
159
+ depth += result.open - result.close;
160
+ if (foundFirstBrace && depth <= 0) {
161
+ blockEnd = j;
162
+ break;
163
+ }
164
+ }
165
+ const code = lines.slice(blockStart, blockEnd + 1).join('\n');
166
+ return {
167
+ name,
168
+ code,
169
+ startLine: blockStart,
170
+ endLine: blockEnd,
171
+ };
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+ /**
177
+ * @codexApi {"parent":"pithy.codex.extraction.test-example-extractor","name":"parseVitestOutput","stability":"stable","signature":"(output: string) => TestStatus[]"}
178
+ *
179
+ * Parses vitest JSON output to extract test statuses
180
+ * @param output - JSON string from vitest --reporter=json
181
+ * @returns Array of test statuses
182
+ */
183
+ export function parseVitestOutput(output) {
184
+ const statuses = [];
185
+ try {
186
+ const json = JSON.parse(output);
187
+ // Handle vitest JSON output format
188
+ if (json.testResults) {
189
+ for (const fileResult of json.testResults) {
190
+ const file = fileResult.name || fileResult.file;
191
+ if (!file)
192
+ continue;
193
+ for (const testResult of fileResult.assertionResults || []) {
194
+ const testName = testResult.fullName || testResult.title;
195
+ if (!testName)
196
+ continue;
197
+ statuses.push({
198
+ file,
199
+ testName,
200
+ status: mapVitestStatus(testResult.status),
201
+ duration: testResult.duration,
202
+ error: testResult.failureMessages?.join('\n'),
203
+ });
204
+ }
205
+ }
206
+ }
207
+ // Also handle the top-level format
208
+ if (json.success !== undefined && json.results) {
209
+ for (const result of json.results) {
210
+ const file = result.file;
211
+ if (!file)
212
+ continue;
213
+ for (const task of flattenTasks(result.tasks || [])) {
214
+ if (!task?.name)
215
+ continue;
216
+ statuses.push({
217
+ file,
218
+ testName: task.name,
219
+ status: mapTaskResult(task.result?.state),
220
+ duration: task.result?.duration,
221
+ error: task.result?.error?.message,
222
+ });
223
+ }
224
+ }
225
+ }
226
+ }
227
+ catch (err) {
228
+ if (output.trim().length > 0 && process.env.NODE_ENV === 'development') {
229
+ console.warn('[codex] Failed to parse vitest output:', err instanceof Error ? err.message : String(err));
230
+ }
231
+ }
232
+ return statuses;
233
+ }
234
+ /**
235
+ * Flattens nested task structure from vitest
236
+ */
237
+ function flattenTasks(tasks, parentName = '') {
238
+ const flat = [];
239
+ for (const task of tasks) {
240
+ const fullName = parentName ? `${parentName} > ${task.name}` : task.name;
241
+ if (task.type === 'test') {
242
+ flat.push({
243
+ name: fullName,
244
+ result: task.result,
245
+ });
246
+ }
247
+ if (task.tasks && Array.isArray(task.tasks)) {
248
+ flat.push(...flattenTasks(task.tasks, fullName));
249
+ }
250
+ }
251
+ return flat;
252
+ }
253
+ /**
254
+ * Maps vitest status to our status format
255
+ */
256
+ function mapVitestStatus(status) {
257
+ switch (status?.toLowerCase()) {
258
+ case 'passed':
259
+ return 'passed';
260
+ case 'failed':
261
+ return 'failed';
262
+ case 'pending':
263
+ case 'skipped':
264
+ case 'todo':
265
+ return 'skipped';
266
+ default:
267
+ return 'skipped';
268
+ }
269
+ }
270
+ /**
271
+ * Maps vitest task result state to our status format
272
+ */
273
+ function mapTaskResult(state) {
274
+ switch (state?.toLowerCase()) {
275
+ case 'pass':
276
+ return 'passed';
277
+ case 'fail':
278
+ return 'failed';
279
+ case 'skip':
280
+ case 'todo':
281
+ return 'skipped';
282
+ default:
283
+ return 'skipped';
284
+ }
285
+ }
286
+ /**
287
+ * @codexApi {"parent":"pithy.codex.extraction.test-example-extractor","name":"matchExamplesToStatuses","stability":"stable","signature":"(examples: TestExample[], statuses: TestStatus[]) => TestExample[]"}
288
+ *
289
+ * Matches extracted examples to test statuses by test name
290
+ * @param examples - Extracted test examples
291
+ * @param statuses - Test statuses from vitest output
292
+ * @returns Examples with updated status information
293
+ */
294
+ export function matchExamplesToStatuses(examples, statuses) {
295
+ return examples.map(example => {
296
+ // Find matching status by normalized file path and test name
297
+ const normalizedExample = normalize(example.testFile);
298
+ const matchingStatus = statuses.find(status => {
299
+ const normalizedStatus = normalize(status.file);
300
+ // Prefer normalized path suffix matching to avoid cross-package collisions.
301
+ // Both paths are normalized so separator differences are handled.
302
+ const fileMatch = normalizedStatus.endsWith(normalizedExample) ||
303
+ normalizedExample.endsWith(normalizedStatus);
304
+ // Prefer exact name match; fall back to substring only if one
305
+ // is a full "describe > test" path containing the other.
306
+ const nameMatch = status.testName === example.name ||
307
+ status.testName.endsWith(` > ${example.name}`) ||
308
+ status.testName.startsWith(`${example.name} > `);
309
+ return fileMatch && nameMatch;
310
+ });
311
+ if (matchingStatus) {
312
+ return {
313
+ ...example,
314
+ status: matchingStatus.status === 'passed'
315
+ ? 'passing'
316
+ : matchingStatus.status === 'failed'
317
+ ? 'failing'
318
+ : 'skipped',
319
+ error: matchingStatus.error,
320
+ };
321
+ }
322
+ return example;
323
+ });
324
+ }
325
+ /**
326
+ * @codexApi {"parent":"pithy.codex.extraction.test-example-extractor","name":"generateExampleReport","stability":"stable","signature":"(examples: TestExample[]) => string"}
327
+ *
328
+ * Generates a markdown report of test examples with their statuses
329
+ * @param examples - Test examples to report
330
+ * @returns Markdown formatted report
331
+ */
332
+ export function generateExampleReport(examples) {
333
+ const byEntry = new Map();
334
+ // Group by entry ID
335
+ for (const example of examples) {
336
+ const existing = byEntry.get(example.entryId) || [];
337
+ existing.push(example);
338
+ byEntry.set(example.entryId, existing);
339
+ }
340
+ const lines = ['# Test Examples Report', ''];
341
+ for (const [entryId, entryExamples] of byEntry) {
342
+ lines.push(`## ${entryId}`);
343
+ lines.push('');
344
+ for (const example of entryExamples) {
345
+ const statusIcon = example.status === 'passing'
346
+ ? '✅'
347
+ : example.status === 'failing'
348
+ ? '❌'
349
+ : example.status === 'skipped'
350
+ ? '⏭️'
351
+ : '❓';
352
+ lines.push(`### ${statusIcon} ${example.name}`);
353
+ lines.push('');
354
+ lines.push(`Location: \`${example.testFile}:${example.location.line}\``);
355
+ lines.push('');
356
+ lines.push('```typescript');
357
+ lines.push(example.code);
358
+ lines.push('```');
359
+ lines.push('');
360
+ if (example.error) {
361
+ lines.push('**Error:**');
362
+ lines.push('```');
363
+ lines.push(example.error);
364
+ lines.push('```');
365
+ lines.push('');
366
+ }
367
+ }
368
+ }
369
+ // Summary
370
+ const passing = examples.filter(e => e.status === 'passing').length;
371
+ const failing = examples.filter(e => e.status === 'failing').length;
372
+ const unknown = examples.filter(e => e.status === 'unknown' || e.status === 'skipped').length;
373
+ lines.push('## Summary');
374
+ lines.push('');
375
+ lines.push(`- ✅ Passing: ${passing}`);
376
+ lines.push(`- ❌ Failing: ${failing}`);
377
+ lines.push(`- ❓ Unknown/Skipped: ${unknown}`);
378
+ return lines.join('\n');
379
+ }
380
+ /**
381
+ * @codexApi {"parent":"pithy.codex.extraction.test-example-extractor","name":"validateTestExamples","stability":"experimental","signature":"(examples: TestExample[]) => { valid: TestExample[]; invalid: TestExample[] }"}
382
+ *
383
+ * Validates that test examples are working by checking their status
384
+ * @param examples - Test examples to validate
385
+ * @returns Object with valid and invalid examples
386
+ */
387
+ export function validateTestExamples(examples) {
388
+ const valid = [];
389
+ const invalid = [];
390
+ for (const example of examples) {
391
+ if (example.status === 'passing') {
392
+ valid.push(example);
393
+ }
394
+ else if (example.status === 'failing') {
395
+ invalid.push(example);
396
+ }
397
+ // Unknown/skipped are neither valid nor invalid
398
+ }
399
+ return { valid, invalid };
400
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.extraction.test-pattern-extractor",
5
+ * "title": "Test Pattern Extractor",
6
+ * "category": "compiler"
7
+ * }
8
+ */
9
+ import type { ExtractedTestCase, TestFileAnalysis, TestReference } from './types.js';
10
+ /**
11
+ * Extracts test cases from a single test file
12
+ * @param content - Test file content
13
+ * @param filePath - Path to the test file
14
+ * @returns Array of extracted test cases
15
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"extractTestCases","stability":"stable","signature":"(content: string, filePath: string) => ExtractedTestCase[]"}
16
+ */
17
+ export declare function extractTestCases(content: string, filePath: string): ExtractedTestCase[];
18
+ /**
19
+ * Extracts imports from a test file to identify tested modules
20
+ * @param content - Test file content
21
+ * @returns Array of import paths
22
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"extractTestImports","stability":"stable","signature":"(content: string) => string[]"}
23
+ */
24
+ export declare function extractTestImports(content: string): string[];
25
+ /**
26
+ * Identifies which APIs are likely covered by a test file
27
+ * @param content - Test file content
28
+ * @param knownApis - List of known API names to look for
29
+ * @returns Array of covered API identifiers
30
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"identifyCoveredApis","stability":"stable","signature":"(content: string, knownApis: string[]) => string[]"}
31
+ */
32
+ export declare function identifyCoveredApis(content: string, knownApis: string[]): string[];
33
+ /**
34
+ * Analyzes a test file and returns structured analysis
35
+ * @param content - Test file content
36
+ * @param filePath - Path to the test file
37
+ * @param knownApis - Optional list of known API names
38
+ * @returns Test file analysis
39
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"analyzeTestFile","stability":"stable","signature":"(content: string, filePath: string, knownApis?: string[]) => TestFileAnalysis"}
40
+ */
41
+ export declare function analyzeTestFile(content: string, filePath: string, knownApis?: string[]): TestFileAnalysis;
42
+ /**
43
+ * Converts test cases to test references for linking to codex entries
44
+ * @param testCases - Extracted test cases
45
+ * @param filePath - Path to the test file
46
+ * @returns Array of test references
47
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"testCasesToReferences","stability":"stable","signature":"(testCases: ExtractedTestCase[], filePath: string) => TestReference[]"}
48
+ */
49
+ export declare function testCasesToReferences(testCases: ExtractedTestCase[], filePath: string): TestReference[];
50
+ /**
51
+ * Matches test files to codex entry IDs based on naming conventions
52
+ * @param testFilePath - Path to the test file
53
+ * @param entryIds - List of codex entry IDs
54
+ * @returns Matched entry ID or undefined
55
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"matchTestToEntry","stability":"stable","signature":"(testFilePath: string, entryIds: string[]) => string | undefined"}
56
+ */
57
+ export declare function matchTestToEntry(testFilePath: string, entryIds: string[]): string | undefined;
58
+ /**
59
+ * Generates a test coverage summary for display
60
+ * @param analyses - Array of test file analyses
61
+ * @returns Formatted coverage summary
62
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"generateCoverageSummary","stability":"stable","signature":"(analyses: TestFileAnalysis[]) => { totalTests: number; byType: Record<string, number>; coveredApis: string[] }"}
63
+ */
64
+ export declare function generateCoverageSummary(analyses: TestFileAnalysis[]): {
65
+ totalTests: number;
66
+ byType: Record<string, number>;
67
+ coveredApis: string[];
68
+ };