@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,494 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.extraction.type-extractor",
5
+ * "title": "TypeScript Type Extractor",
6
+ * "category": "compiler"
7
+ * }
8
+ */
9
+ import ts from 'typescript';
10
+ /**
11
+ * @codexApi {"parent":"pithy.codex.extraction.type-extractor","name":"extractTypesFromFile","stability":"stable","signature":"(filePath: string, content: string) => ExtractedTypeDefinition[]"}
12
+ *
13
+ * Extracts type definitions from a TypeScript file using the TypeScript Compiler API
14
+ * @param filePath - Path to the TypeScript file
15
+ * @param content - Content of the file
16
+ * @returns Array of extracted type definitions
17
+ */
18
+ export function extractTypesFromFile(filePath, content) {
19
+ const types = [];
20
+ // Create a source file
21
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
22
+ // Visit all nodes
23
+ function visit(node) {
24
+ if (ts.isInterfaceDeclaration(node)) {
25
+ types.push(extractInterface(node, sourceFile, filePath));
26
+ }
27
+ else if (ts.isTypeAliasDeclaration(node)) {
28
+ types.push(extractTypeAlias(node, sourceFile, filePath));
29
+ }
30
+ else if (ts.isEnumDeclaration(node)) {
31
+ types.push(extractEnum(node, sourceFile, filePath));
32
+ }
33
+ else if (ts.isClassDeclaration(node) && node.name) {
34
+ types.push(extractClass(node, sourceFile, filePath));
35
+ }
36
+ ts.forEachChild(node, visit);
37
+ }
38
+ visit(sourceFile);
39
+ return types;
40
+ }
41
+ /**
42
+ * Extracts an interface declaration
43
+ */
44
+ function extractInterface(node, sourceFile, filePath) {
45
+ const name = node.name.text;
46
+ const exported = hasExportModifier(node);
47
+ const location = getSourceLocation(node, sourceFile, filePath);
48
+ // Extract generics
49
+ const generics = node.typeParameters?.map(tp => tp.getText(sourceFile));
50
+ // Extract extends
51
+ const extendsClause = node.heritageClauses?.find(hc => hc.token === ts.SyntaxKind.ExtendsKeyword);
52
+ const extendsTypes = extendsClause?.types.map(t => t.getText(sourceFile));
53
+ // Extract members
54
+ const members = node.members.map(member => extractMember(member, sourceFile));
55
+ // Extract JSDoc
56
+ const jsdoc = extractJSDocFromNode(node, sourceFile, filePath);
57
+ return {
58
+ name,
59
+ kind: 'interface',
60
+ exported,
61
+ generics,
62
+ extends: extendsTypes,
63
+ members,
64
+ jsdoc,
65
+ location,
66
+ };
67
+ }
68
+ /**
69
+ * Extracts a type alias declaration
70
+ */
71
+ function extractTypeAlias(node, sourceFile, filePath) {
72
+ const name = node.name.text;
73
+ const exported = hasExportModifier(node);
74
+ const location = getSourceLocation(node, sourceFile, filePath);
75
+ // Extract generics
76
+ const generics = node.typeParameters?.map(tp => tp.getText(sourceFile));
77
+ // For type aliases, we represent the type as a single "member"
78
+ const members = [];
79
+ // If it's an object type literal, extract its properties
80
+ if (ts.isTypeLiteralNode(node.type)) {
81
+ for (const member of node.type.members) {
82
+ members.push(extractMember(member, sourceFile));
83
+ }
84
+ }
85
+ else {
86
+ // For other types (union, intersection, etc.), store the full type as a special member
87
+ members.push({
88
+ name: '__type',
89
+ type: node.type.getText(sourceFile),
90
+ optional: false,
91
+ readonly: false,
92
+ });
93
+ }
94
+ const jsdoc = extractJSDocFromNode(node, sourceFile, filePath);
95
+ return {
96
+ name,
97
+ kind: 'type',
98
+ exported,
99
+ generics,
100
+ members,
101
+ jsdoc,
102
+ location,
103
+ };
104
+ }
105
+ /**
106
+ * Extracts an enum declaration
107
+ */
108
+ function extractEnum(node, sourceFile, filePath) {
109
+ const name = node.name.text;
110
+ const exported = hasExportModifier(node);
111
+ const location = getSourceLocation(node, sourceFile, filePath);
112
+ // Extract enum members
113
+ const members = node.members.map(member => {
114
+ const memberName = ts.isIdentifier(member.name)
115
+ ? member.name.text
116
+ : member.name.getText(sourceFile);
117
+ const value = member.initializer?.getText(sourceFile) || '';
118
+ return {
119
+ name: memberName,
120
+ type: value || 'auto',
121
+ optional: false,
122
+ readonly: true,
123
+ };
124
+ });
125
+ const jsdoc = extractJSDocFromNode(node, sourceFile, filePath);
126
+ return {
127
+ name,
128
+ kind: 'enum',
129
+ exported,
130
+ members,
131
+ jsdoc,
132
+ location,
133
+ };
134
+ }
135
+ /**
136
+ * Extracts a class declaration
137
+ */
138
+ function extractClass(node, sourceFile, filePath) {
139
+ const name = node.name?.text || 'AnonymousClass';
140
+ const exported = hasExportModifier(node);
141
+ const location = getSourceLocation(node, sourceFile, filePath);
142
+ // Extract generics
143
+ const generics = node.typeParameters?.map(tp => tp.getText(sourceFile));
144
+ // Extract extends
145
+ const extendsClause = node.heritageClauses?.find(hc => hc.token === ts.SyntaxKind.ExtendsKeyword);
146
+ const extendsTypes = extendsClause?.types.map(t => t.getText(sourceFile));
147
+ // Extract implements
148
+ const implementsClause = node.heritageClauses?.find(hc => hc.token === ts.SyntaxKind.ImplementsKeyword);
149
+ const implementsTypes = implementsClause?.types.map(t => t.getText(sourceFile));
150
+ // Extract public members only (skip private, protected, and #private)
151
+ const members = [];
152
+ for (const member of node.members) {
153
+ if (hasPrivateModifier(member))
154
+ continue;
155
+ if (hasProtectedModifier(member))
156
+ continue;
157
+ // Skip ECMAScript #private fields/methods
158
+ if (member.name && ts.isPrivateIdentifier(member.name))
159
+ continue;
160
+ if (ts.isPropertyDeclaration(member) ||
161
+ ts.isMethodDeclaration(member) ||
162
+ ts.isGetAccessor(member) ||
163
+ ts.isSetAccessor(member)) {
164
+ members.push(extractClassMember(member, sourceFile));
165
+ }
166
+ }
167
+ const jsdoc = extractJSDocFromNode(node, sourceFile, filePath);
168
+ return {
169
+ name,
170
+ kind: 'class',
171
+ exported,
172
+ generics,
173
+ extends: extendsTypes,
174
+ implements: implementsTypes,
175
+ members,
176
+ jsdoc,
177
+ location,
178
+ };
179
+ }
180
+ /**
181
+ * Extracts a member from an interface/type literal
182
+ */
183
+ function extractMember(member, sourceFile) {
184
+ if (ts.isPropertySignature(member)) {
185
+ const name = member.name.getText(sourceFile);
186
+ const type = member.type?.getText(sourceFile) || 'unknown';
187
+ const optional = !!member.questionToken;
188
+ const readonly = hasReadonlyModifier(member);
189
+ const description = getJSDocDescription(member, sourceFile);
190
+ return { name, type, optional, readonly, description };
191
+ }
192
+ if (ts.isMethodSignature(member)) {
193
+ const name = member.name.getText(sourceFile);
194
+ const signature = member.getText(sourceFile);
195
+ const optional = !!member.questionToken;
196
+ const description = getJSDocDescription(member, sourceFile);
197
+ return {
198
+ name,
199
+ type: 'method',
200
+ optional,
201
+ readonly: false,
202
+ signature,
203
+ description,
204
+ };
205
+ }
206
+ if (ts.isIndexSignatureDeclaration(member)) {
207
+ return {
208
+ name: '[index]',
209
+ type: member.type?.getText(sourceFile) || 'unknown',
210
+ optional: false,
211
+ readonly: hasReadonlyModifier(member),
212
+ };
213
+ }
214
+ // Fallback
215
+ return {
216
+ name: member.getText(sourceFile).slice(0, 50),
217
+ type: 'unknown',
218
+ optional: false,
219
+ readonly: false,
220
+ };
221
+ }
222
+ /**
223
+ * Builds a signature string for a class method/getter/setter without the body.
224
+ */
225
+ function buildClassMemberSignature(member, sourceFile) {
226
+ const name = member.name.getText(sourceFile);
227
+ if (ts.isGetAccessor(member)) {
228
+ const returnType = member.type
229
+ ? `: ${member.type.getText(sourceFile)}`
230
+ : '';
231
+ return `get ${name}()${returnType}`;
232
+ }
233
+ if (ts.isSetAccessor(member)) {
234
+ const params = member.parameters.map(p => p.getText(sourceFile)).join(', ');
235
+ return `set ${name}(${params})`;
236
+ }
237
+ // Method declaration
238
+ const typeParams = member.typeParameters
239
+ ? `<${member.typeParameters.map(tp => tp.getText(sourceFile)).join(', ')}>`
240
+ : '';
241
+ const params = member.parameters.map(p => p.getText(sourceFile)).join(', ');
242
+ const returnType = member.type ? `: ${member.type.getText(sourceFile)}` : '';
243
+ return `${name}${typeParams}(${params})${returnType}`;
244
+ }
245
+ /**
246
+ * Extracts a class member
247
+ */
248
+ function extractClassMember(member, sourceFile) {
249
+ const name = member.name.getText(sourceFile);
250
+ const optional = ts.isPropertyDeclaration(member) && !!member.questionToken;
251
+ const readonly = hasReadonlyModifier(member);
252
+ const description = getJSDocDescription(member, sourceFile);
253
+ if (ts.isMethodDeclaration(member) ||
254
+ ts.isGetAccessor(member) ||
255
+ ts.isSetAccessor(member)) {
256
+ const signature = buildClassMemberSignature(member, sourceFile);
257
+ return {
258
+ name,
259
+ type: ts.isGetAccessor(member)
260
+ ? 'getter'
261
+ : ts.isSetAccessor(member)
262
+ ? 'setter'
263
+ : 'method',
264
+ optional,
265
+ readonly,
266
+ signature,
267
+ description,
268
+ };
269
+ }
270
+ const type = member.type?.getText(sourceFile) || 'unknown';
271
+ return { name, type, optional, readonly, description };
272
+ }
273
+ /**
274
+ * Gets source location from a node
275
+ */
276
+ function getSourceLocation(node, sourceFile, filePath) {
277
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
278
+ const endPos = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
279
+ return {
280
+ file: filePath,
281
+ line: line + 1, // 1-indexed
282
+ column: character + 1,
283
+ endLine: endPos.line + 1,
284
+ endColumn: endPos.character + 1,
285
+ };
286
+ }
287
+ /**
288
+ * Checks if a node has the export modifier
289
+ */
290
+ function hasExportModifier(node) {
291
+ const modifiers = ts.canHaveModifiers(node)
292
+ ? ts.getModifiers(node)
293
+ : undefined;
294
+ return modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) || false;
295
+ }
296
+ /**
297
+ * Checks if a node has the private modifier
298
+ */
299
+ function hasPrivateModifier(node) {
300
+ const modifiers = ts.canHaveModifiers(node)
301
+ ? ts.getModifiers(node)
302
+ : undefined;
303
+ return modifiers?.some(m => m.kind === ts.SyntaxKind.PrivateKeyword) || false;
304
+ }
305
+ /**
306
+ * Checks if a node has the protected modifier
307
+ */
308
+ function hasProtectedModifier(node) {
309
+ const modifiers = ts.canHaveModifiers(node)
310
+ ? ts.getModifiers(node)
311
+ : undefined;
312
+ return (modifiers?.some(m => m.kind === ts.SyntaxKind.ProtectedKeyword) || false);
313
+ }
314
+ /**
315
+ * Checks if a node has the readonly modifier
316
+ */
317
+ function hasReadonlyModifier(node) {
318
+ const modifiers = ts.canHaveModifiers(node)
319
+ ? ts.getModifiers(node)
320
+ : undefined;
321
+ return (modifiers?.some(m => m.kind === ts.SyntaxKind.ReadonlyKeyword) || false);
322
+ }
323
+ /**
324
+ * Gets the JSDoc description from a node
325
+ */
326
+ function getJSDocDescription(node, sourceFile) {
327
+ const jsDocs = ts.getJSDocCommentsAndTags(node);
328
+ for (const doc of jsDocs) {
329
+ if (ts.isJSDoc(doc) && doc.comment) {
330
+ if (typeof doc.comment === 'string') {
331
+ return doc.comment;
332
+ }
333
+ return doc.comment.map(c => c.getText(sourceFile)).join('');
334
+ }
335
+ }
336
+ return undefined;
337
+ }
338
+ /**
339
+ * Extracts a comment string from a JSDoc comment node
340
+ */
341
+ function extractCommentText(comment, sourceFile) {
342
+ if (!comment)
343
+ return '';
344
+ if (typeof comment === 'string')
345
+ return comment;
346
+ return comment.map(c => c.getText(sourceFile)).join('');
347
+ }
348
+ /**
349
+ * Extracts JSDoc from a node into our ParsedJSDoc format,
350
+ * including @param, @returns, @throws, @see, @since, @deprecated, and @example tags.
351
+ */
352
+ function extractJSDocFromNode(node, sourceFile, filePath) {
353
+ const jsDocs = ts.getJSDocCommentsAndTags(node);
354
+ for (const doc of jsDocs) {
355
+ if (!ts.isJSDoc(doc))
356
+ continue;
357
+ const location = getSourceLocation(doc, sourceFile, filePath);
358
+ const description = extractCommentText(doc.comment, sourceFile);
359
+ const params = [];
360
+ const examples = [];
361
+ const tags = {};
362
+ let returns;
363
+ const throws = [];
364
+ const see = [];
365
+ let since;
366
+ let deprecated;
367
+ if (doc.tags) {
368
+ for (const tag of doc.tags) {
369
+ const tagName = tag.tagName.text;
370
+ const tagComment = extractCommentText(tag.comment, sourceFile);
371
+ switch (tagName) {
372
+ case 'param': {
373
+ if (ts.isJSDocParameterTag(tag)) {
374
+ params.push({
375
+ name: tag.name.getText(sourceFile),
376
+ type: tag.typeExpression?.getText(sourceFile),
377
+ description: tagComment,
378
+ optional: tag.isBracketed,
379
+ });
380
+ }
381
+ break;
382
+ }
383
+ case 'returns':
384
+ case 'return':
385
+ returns = {
386
+ type: tag.typeExpression?.getText(sourceFile),
387
+ description: tagComment,
388
+ };
389
+ break;
390
+ case 'throws':
391
+ case 'exception':
392
+ throws.push(tagComment);
393
+ break;
394
+ case 'see':
395
+ see.push(tagComment);
396
+ break;
397
+ case 'since':
398
+ since = tagComment;
399
+ break;
400
+ case 'deprecated':
401
+ deprecated = tagComment || true;
402
+ break;
403
+ case 'example':
404
+ examples.push({
405
+ code: tagComment,
406
+ language: 'typescript',
407
+ runnable: false,
408
+ isDoctest: false,
409
+ location: getSourceLocation(tag, sourceFile, filePath),
410
+ });
411
+ break;
412
+ default:
413
+ if (!tags[tagName])
414
+ tags[tagName] = [];
415
+ tags[tagName].push(tagComment);
416
+ break;
417
+ }
418
+ }
419
+ }
420
+ return {
421
+ description,
422
+ params,
423
+ returns,
424
+ examples,
425
+ throws: throws.length > 0 ? throws : undefined,
426
+ see: see.length > 0 ? see : undefined,
427
+ since,
428
+ deprecated,
429
+ tags,
430
+ location,
431
+ };
432
+ }
433
+ return undefined;
434
+ }
435
+ /**
436
+ * @codexApi {"parent":"pithy.codex.extraction.type-extractor","name":"getTypeSignature","stability":"stable","signature":"(def: ExtractedTypeDefinition) => string"}
437
+ *
438
+ * Generates a human-readable signature for a type definition
439
+ * @param def - The extracted type definition
440
+ * @returns Formatted type signature
441
+ */
442
+ export function getTypeSignature(def) {
443
+ const exportPrefix = def.exported ? 'export ' : '';
444
+ const genericsStr = def.generics?.length
445
+ ? `<${def.generics.join(', ')}>`
446
+ : '';
447
+ const extendsStr = def.extends?.length
448
+ ? ` extends ${def.extends.join(', ')}`
449
+ : '';
450
+ const implementsStr = def.implements?.length
451
+ ? ` implements ${def.implements.join(', ')}`
452
+ : '';
453
+ switch (def.kind) {
454
+ case 'interface':
455
+ return `${exportPrefix}interface ${def.name}${genericsStr}${extendsStr}`;
456
+ case 'type':
457
+ return `${exportPrefix}type ${def.name}${genericsStr}`;
458
+ case 'enum':
459
+ return `${exportPrefix}enum ${def.name}`;
460
+ case 'class':
461
+ return `${exportPrefix}class ${def.name}${genericsStr}${extendsStr}${implementsStr}`;
462
+ default:
463
+ return def.name;
464
+ }
465
+ }
466
+ /**
467
+ * @codexApi {"parent":"pithy.codex.extraction.type-extractor","name":"generateMethodTable","stability":"stable","signature":"(def: ExtractedTypeDefinition) => string"}
468
+ *
469
+ * Generates a markdown table of members for documentation
470
+ * @param def - The extracted type definition
471
+ * @returns Markdown table string
472
+ */
473
+ export function generateMethodTable(def) {
474
+ if (def.members.length === 0)
475
+ return '';
476
+ const lines = [
477
+ '| Member | Type | Description |',
478
+ '|--------|------|-------------|',
479
+ ];
480
+ for (const member of def.members) {
481
+ let nameCol = member.name;
482
+ if (member.readonly) {
483
+ nameCol = `readonly ${nameCol}`;
484
+ }
485
+ if (member.optional) {
486
+ nameCol = `${nameCol}?`;
487
+ }
488
+ const typeCol = member.signature || member.type;
489
+ const rawDesc = member.description ?? '';
490
+ const descCol = rawDesc.replace(/\|/g, '\\|').replace(/\r?\n/g, '<br/>');
491
+ lines.push(`| \`${nameCol}\` | \`${typeCol}\` | ${descCol} |`);
492
+ }
493
+ return lines.join('\n');
494
+ }