@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,261 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.extraction.test-pattern-extractor",
5
+ * "title": "Test Pattern Extractor",
6
+ * "category": "compiler"
7
+ * }
8
+ */
9
+ import { basename } from 'node:path';
10
+ /**
11
+ * Escapes special regex metacharacters in a string.
12
+ * This prevents regex injection when user-provided strings are used in RegExp.
13
+ */
14
+ function escapeRegexMetachars(str) {
15
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16
+ }
17
+ /**
18
+ * Patterns for test framework detection
19
+ */
20
+ const DESCRIBE_PATTERN = /(?:describe|suite)\s*\(\s*(['"`])([^'"`]+)\1\s*,\s*(?:async\s*)?\(\s*\)\s*=>\s*\{/g;
21
+ const IT_PATTERN = /(?:it|test)\s*\(\s*(['"`])([^'"`]+)\1\s*,\s*(?:async\s*)?\(\s*\)\s*=>\s*\{/g;
22
+ const IMPORT_PATTERN = /import\s+(?:\{[^}]+\}|\*\s+as\s+\w+|\w+)\s+from\s+(['"`])([^'"`]+)\1/g;
23
+ const ASSERTION_PATTERN = /expect\([^)]+\)\.(?:toBe|toEqual|toMatch|toThrow|toHaveBeenCalled|toContain|toBeTruthy|toBeFalsy|toBeNull|toBeUndefined|toBeDefined|toBeGreaterThan|toBeLessThan|toHaveLength|toHaveProperty|toMatchObject|toMatchSnapshot|toMatchInlineSnapshot)\(/g;
24
+ /**
25
+ * Determines the test type based on file path and patterns
26
+ */
27
+ function determineTestType(filePath) {
28
+ const lowerPath = filePath.toLowerCase();
29
+ if (lowerPath.includes('e2e') || lowerPath.includes('end-to-end')) {
30
+ return 'e2e';
31
+ }
32
+ if (lowerPath.includes('integration') ||
33
+ lowerPath.includes('integ') ||
34
+ lowerPath.includes('.integration.')) {
35
+ return 'integration';
36
+ }
37
+ return 'unit';
38
+ }
39
+ /**
40
+ * Extracts test cases from a single test file
41
+ * @param content - Test file content
42
+ * @param filePath - Path to the test file
43
+ * @returns Array of extracted test cases
44
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"extractTestCases","stability":"stable","signature":"(content: string, filePath: string) => ExtractedTestCase[]"}
45
+ */
46
+ export function extractTestCases(content, filePath) {
47
+ const testCases = [];
48
+ // Find all describe blocks for hierarchy tracking
49
+ const describes = [];
50
+ let match;
51
+ // Reset regex state
52
+ DESCRIBE_PATTERN.lastIndex = 0;
53
+ while ((match = DESCRIBE_PATTERN.exec(content)) !== null) {
54
+ const textBefore = content.slice(0, match.index);
55
+ const lineNumber = textBefore.split('\n').length;
56
+ describes.push({
57
+ name: match[2],
58
+ startIndex: match.index,
59
+ line: lineNumber,
60
+ });
61
+ }
62
+ // Find all test cases
63
+ IT_PATTERN.lastIndex = 0;
64
+ while ((match = IT_PATTERN.exec(content)) !== null) {
65
+ const testName = match[2];
66
+ const startIndex = match.index;
67
+ const textBefore = content.slice(0, startIndex);
68
+ const lineNumber = textBefore.split('\n').length;
69
+ // Find the parent describe block
70
+ let parentDescribe;
71
+ for (const desc of describes) {
72
+ if (desc.startIndex < startIndex) {
73
+ parentDescribe = desc.name;
74
+ }
75
+ else {
76
+ break;
77
+ }
78
+ }
79
+ // Extract the test body (simplified - just gets the next few lines)
80
+ const testBodyStart = content.indexOf('{', startIndex);
81
+ let braceCount = 1;
82
+ let testBodyEnd = testBodyStart + 1;
83
+ while (braceCount > 0 && testBodyEnd < content.length) {
84
+ const char = content[testBodyEnd];
85
+ if (char === '{')
86
+ braceCount++;
87
+ if (char === '}')
88
+ braceCount--;
89
+ testBodyEnd++;
90
+ }
91
+ const testCode = content.slice(testBodyStart, testBodyEnd);
92
+ // Find assertions in the test
93
+ const assertions = [];
94
+ ASSERTION_PATTERN.lastIndex = 0;
95
+ let assertMatch;
96
+ while ((assertMatch = ASSERTION_PATTERN.exec(testCode)) !== null) {
97
+ assertions.push(assertMatch[0]);
98
+ }
99
+ testCases.push({
100
+ name: testName,
101
+ type: match[0].startsWith('it') ? 'it' : 'test',
102
+ location: {
103
+ file: filePath,
104
+ line: lineNumber,
105
+ },
106
+ parentDescribe,
107
+ code: testCode,
108
+ assertions,
109
+ });
110
+ }
111
+ return testCases;
112
+ }
113
+ /**
114
+ * Extracts imports from a test file to identify tested modules
115
+ * @param content - Test file content
116
+ * @returns Array of import paths
117
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"extractTestImports","stability":"stable","signature":"(content: string) => string[]"}
118
+ */
119
+ export function extractTestImports(content) {
120
+ const imports = [];
121
+ IMPORT_PATTERN.lastIndex = 0;
122
+ let match;
123
+ while ((match = IMPORT_PATTERN.exec(content)) !== null) {
124
+ const importPath = match[2];
125
+ // Filter out test utilities and external packages
126
+ if (!importPath.startsWith('vitest') &&
127
+ !importPath.startsWith('@testing') &&
128
+ !importPath.startsWith('jest')) {
129
+ imports.push(importPath);
130
+ }
131
+ }
132
+ return imports;
133
+ }
134
+ /**
135
+ * Identifies which APIs are likely covered by a test file
136
+ * @param content - Test file content
137
+ * @param knownApis - List of known API names to look for
138
+ * @returns Array of covered API identifiers
139
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"identifyCoveredApis","stability":"stable","signature":"(content: string, knownApis: string[]) => string[]"}
140
+ */
141
+ export function identifyCoveredApis(content, knownApis) {
142
+ const covered = [];
143
+ for (const api of knownApis) {
144
+ // Escape regex metacharacters to prevent regex injection
145
+ const escapedApi = escapeRegexMetachars(api);
146
+ // Check if the API name appears in the test file
147
+ // Look for function calls, method calls, or imports
148
+ const patterns = [
149
+ new RegExp(`\\b${escapedApi}\\s*\\(`, 'g'), // Function call: api()
150
+ new RegExp(`\\.${escapedApi}\\s*\\(`, 'g'), // Method call: .api()
151
+ new RegExp(`import[^;]*\\b${escapedApi}\\b`, 'g'), // Import (limited scope)
152
+ new RegExp(`from\\s*['"][^'"]*${escapedApi}`, 'gi'), // From path containing api
153
+ ];
154
+ for (const pattern of patterns) {
155
+ if (pattern.test(content)) {
156
+ covered.push(api);
157
+ break;
158
+ }
159
+ }
160
+ }
161
+ return [...new Set(covered)];
162
+ }
163
+ /**
164
+ * Analyzes a test file and returns structured analysis
165
+ * @param content - Test file content
166
+ * @param filePath - Path to the test file
167
+ * @param knownApis - Optional list of known API names
168
+ * @returns Test file analysis
169
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"analyzeTestFile","stability":"stable","signature":"(content: string, filePath: string, knownApis?: string[]) => TestFileAnalysis"}
170
+ */
171
+ export function analyzeTestFile(content, filePath, knownApis = []) {
172
+ const testCases = extractTestCases(content, filePath);
173
+ const imports = extractTestImports(content);
174
+ const coveredApis = identifyCoveredApis(content, knownApis);
175
+ return {
176
+ file: filePath,
177
+ testCases,
178
+ imports,
179
+ coveredApis,
180
+ };
181
+ }
182
+ /**
183
+ * Converts test cases to test references for linking to codex entries
184
+ * @param testCases - Extracted test cases
185
+ * @param filePath - Path to the test file
186
+ * @returns Array of test references
187
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"testCasesToReferences","stability":"stable","signature":"(testCases: ExtractedTestCase[], filePath: string) => TestReference[]"}
188
+ */
189
+ export function testCasesToReferences(testCases, filePath) {
190
+ const testType = determineTestType(filePath);
191
+ return testCases.map(tc => ({
192
+ file: filePath,
193
+ line: tc.location.line,
194
+ name: tc.parentDescribe ? `${tc.parentDescribe} > ${tc.name}` : tc.name,
195
+ type: testType,
196
+ }));
197
+ }
198
+ /**
199
+ * Matches test files to codex entry IDs based on naming conventions
200
+ * @param testFilePath - Path to the test file
201
+ * @param entryIds - List of codex entry IDs
202
+ * @returns Matched entry ID or undefined
203
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"matchTestToEntry","stability":"stable","signature":"(testFilePath: string, entryIds: string[]) => string | undefined"}
204
+ */
205
+ export function matchTestToEntry(testFilePath, entryIds) {
206
+ // Extract the base name from the test file (cross-platform)
207
+ const fileName = basename(testFilePath);
208
+ const baseName = fileName
209
+ .replace(/\.test\.ts$/, '')
210
+ .replace(/\.spec\.ts$/, '')
211
+ .replace(/\.test\.js$/, '')
212
+ .replace(/\.spec\.js$/, '')
213
+ .toLowerCase();
214
+ // Try to find a matching entry ID
215
+ for (const entryId of entryIds) {
216
+ const entryParts = entryId.toLowerCase().split('.');
217
+ const lastPart = entryParts[entryParts.length - 1];
218
+ if (lastPart === baseName || entryId.toLowerCase().includes(baseName)) {
219
+ return entryId;
220
+ }
221
+ }
222
+ // Try matching by path segments (cross-platform: handle both / and \)
223
+ const normalizedPath = testFilePath.toLowerCase().replace(/\\/g, '/');
224
+ const pathSegments = normalizedPath.split('/');
225
+ for (const entryId of entryIds) {
226
+ const entryParts = entryId.toLowerCase().split('.');
227
+ // Check if any entry part matches a path segment
228
+ if (entryParts.some(part => pathSegments.includes(part))) {
229
+ return entryId;
230
+ }
231
+ }
232
+ return undefined;
233
+ }
234
+ /**
235
+ * Generates a test coverage summary for display
236
+ * @param analyses - Array of test file analyses
237
+ * @returns Formatted coverage summary
238
+ * @codexApi {"parent":"pithy.codex.extraction.test-pattern-extractor","name":"generateCoverageSummary","stability":"stable","signature":"(analyses: TestFileAnalysis[]) => { totalTests: number; byType: Record<string, number>; coveredApis: string[] }"}
239
+ */
240
+ export function generateCoverageSummary(analyses) {
241
+ const byType = {
242
+ unit: 0,
243
+ integration: 0,
244
+ e2e: 0,
245
+ };
246
+ const allCoveredApis = new Set();
247
+ let totalTests = 0;
248
+ for (const analysis of analyses) {
249
+ const testType = determineTestType(analysis.file);
250
+ byType[testType] += analysis.testCases.length;
251
+ totalTests += analysis.testCases.length;
252
+ for (const api of analysis.coveredApis) {
253
+ allCoveredApis.add(api);
254
+ }
255
+ }
256
+ return {
257
+ totalTests,
258
+ byType,
259
+ coveredApis: Array.from(allCoveredApis),
260
+ };
261
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.testing-pyramid",
5
+ * "title": "Testing Pyramid",
6
+ * "category": "feature"
7
+ * }
8
+ *
9
+ * Provides category-based testing defaults and risk-level modifiers
10
+ * for the codex testing pyramid system. Resolves per-entry testing
11
+ * requirements by merging annotation overrides with category defaults
12
+ * and applying risk adjustments.
13
+ */
14
+ import type { TestingRequirements, RiskLevel } from './types.js';
15
+ /**
16
+ * Get the default testing requirements for a given codex category.
17
+ * Returns feature defaults for unknown categories.
18
+ * Returns a fresh copy — callers may mutate freely.
19
+ *
20
+ * @codexApi {"parent":"pithy.codex.testing-pyramid","name":"getTestingDefaults","stability":"stable","signature":"(category: string) => TestingRequirements"}
21
+ */
22
+ export declare function getTestingDefaults(category: string): TestingRequirements;
23
+ /**
24
+ * Apply risk-level modifiers to testing requirements.
25
+ * Returns a new object — does not mutate the input.
26
+ *
27
+ * @codexApi {"parent":"pithy.codex.testing-pyramid","name":"applyRiskModifiers","stability":"stable","signature":"(base: TestingRequirements, risk: RiskLevel | undefined) => TestingRequirements"}
28
+ */
29
+ export declare function applyRiskModifiers(base: TestingRequirements, risk: RiskLevel | undefined): TestingRequirements;
30
+ /**
31
+ * Resolve the final testing requirements for a codex entry.
32
+ * Merges annotation overrides with category defaults and applies risk modifiers.
33
+ *
34
+ * @codexApi {"parent":"pithy.codex.testing-pyramid","name":"resolveTestingRequirements","stability":"stable","signature":"(category: string, risk?: RiskLevel, override?: TestingRequirements) => TestingRequirements"}
35
+ */
36
+ export declare function resolveTestingRequirements(category: string, risk?: RiskLevel, override?: TestingRequirements): TestingRequirements;
37
+ /**
38
+ * Compute human-readable testing status from requirements.
39
+ * Returns "Complete" if all required levels are covered,
40
+ * "N/A" if no levels are defined, or "Missing X, Y" listing uncovered levels.
41
+ *
42
+ * @codexApi {"parent":"pithy.codex.testing-pyramid","name":"computeTestingStatus","stability":"stable","signature":"(reqs: TestingRequirements) => string"}
43
+ */
44
+ export declare function computeTestingStatus(reqs: TestingRequirements): string;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.testing-pyramid",
5
+ * "title": "Testing Pyramid",
6
+ * "category": "feature"
7
+ * }
8
+ *
9
+ * Provides category-based testing defaults and risk-level modifiers
10
+ * for the codex testing pyramid system. Resolves per-entry testing
11
+ * requirements by merging annotation overrides with category defaults
12
+ * and applying risk adjustments.
13
+ */
14
+ // ── Category Defaults ───────────────────────────────────────────
15
+ /** Category-based default testing requirements per the implementation plan. */
16
+ const CATEGORY_DEFAULTS = {
17
+ directive: {
18
+ unit: { required: true, coverage: 90 },
19
+ integration: { required: true, coverage: 80 },
20
+ e2e: { required: false },
21
+ },
22
+ feature: {
23
+ unit: { required: true, coverage: 85 },
24
+ integration: { required: true, coverage: 70 },
25
+ e2e: { required: false },
26
+ },
27
+ runtime: {
28
+ unit: { required: true, coverage: 90 },
29
+ integration: { required: true, coverage: 80 },
30
+ e2e: { required: true },
31
+ },
32
+ compiler: {
33
+ unit: { required: true, coverage: 95 },
34
+ integration: { required: true, coverage: 70 },
35
+ e2e: { required: false },
36
+ },
37
+ plugin: {
38
+ unit: { required: true, coverage: 70 },
39
+ integration: { required: true, coverage: 85 },
40
+ e2e: { required: true },
41
+ },
42
+ // Type-only modules have no runtime behavior to test
43
+ types: {},
44
+ };
45
+ const RISK_MODIFIERS = {
46
+ critical: { unitDelta: 5, integrationDelta: 10, e2eRequired: true },
47
+ high: { unitDelta: 0, integrationDelta: 5, e2eRequired: false },
48
+ medium: { unitDelta: -5, integrationDelta: 0, e2eRequired: false },
49
+ low: { unitDelta: -10, integrationDelta: -10, e2eRequired: false },
50
+ };
51
+ // ── Public API ──────────────────────────────────────────────────
52
+ /**
53
+ * Get the default testing requirements for a given codex category.
54
+ * Returns feature defaults for unknown categories.
55
+ * Returns a fresh copy — callers may mutate freely.
56
+ *
57
+ * @codexApi {"parent":"pithy.codex.testing-pyramid","name":"getTestingDefaults","stability":"stable","signature":"(category: string) => TestingRequirements"}
58
+ */
59
+ export function getTestingDefaults(category) {
60
+ const defaults = CATEGORY_DEFAULTS[category] ?? CATEGORY_DEFAULTS.feature;
61
+ return structuredClone(defaults);
62
+ }
63
+ /**
64
+ * Apply risk-level modifiers to testing requirements.
65
+ * Returns a new object — does not mutate the input.
66
+ *
67
+ * @codexApi {"parent":"pithy.codex.testing-pyramid","name":"applyRiskModifiers","stability":"stable","signature":"(base: TestingRequirements, risk: RiskLevel | undefined) => TestingRequirements"}
68
+ */
69
+ export function applyRiskModifiers(base, risk) {
70
+ if (!risk)
71
+ return structuredClone(base);
72
+ const modifier = RISK_MODIFIERS[risk];
73
+ if (!modifier)
74
+ return structuredClone(base);
75
+ const result = {};
76
+ if (base.unit) {
77
+ result.unit = {
78
+ ...base.unit,
79
+ coverage: base.unit.coverage !== undefined
80
+ ? clamp(base.unit.coverage + modifier.unitDelta, 0, 100)
81
+ : undefined,
82
+ };
83
+ }
84
+ if (base.integration) {
85
+ result.integration = {
86
+ ...base.integration,
87
+ coverage: base.integration.coverage !== undefined
88
+ ? clamp(base.integration.coverage + modifier.integrationDelta, 0, 100)
89
+ : undefined,
90
+ };
91
+ }
92
+ if (base.e2e) {
93
+ result.e2e = {
94
+ ...base.e2e,
95
+ required: modifier.e2eRequired || (base.e2e.required ?? false),
96
+ };
97
+ }
98
+ return result;
99
+ }
100
+ /**
101
+ * Resolve the final testing requirements for a codex entry.
102
+ * Merges annotation overrides with category defaults and applies risk modifiers.
103
+ *
104
+ * @codexApi {"parent":"pithy.codex.testing-pyramid","name":"resolveTestingRequirements","stability":"stable","signature":"(category: string, risk?: RiskLevel, override?: TestingRequirements) => TestingRequirements"}
105
+ */
106
+ export function resolveTestingRequirements(category, risk, override) {
107
+ const defaults = getTestingDefaults(category);
108
+ // Deep-merge: override fields take precedence per-level,
109
+ // but unspecified fields within a level retain category defaults
110
+ const merged = {
111
+ unit: mergeTestLevel(defaults.unit, override?.unit),
112
+ integration: mergeTestLevel(defaults.integration, override?.integration),
113
+ e2e: mergeTestLevel(defaults.e2e, override?.e2e),
114
+ };
115
+ // Clean up undefined levels (for "types" category)
116
+ if (merged.unit === undefined)
117
+ delete merged.unit;
118
+ if (merged.integration === undefined)
119
+ delete merged.integration;
120
+ if (merged.e2e === undefined)
121
+ delete merged.e2e;
122
+ return applyRiskModifiers(merged, risk);
123
+ }
124
+ /**
125
+ * Compute human-readable testing status from requirements.
126
+ * Returns "Complete" if all required levels are covered,
127
+ * "N/A" if no levels are defined, or "Missing X, Y" listing uncovered levels.
128
+ *
129
+ * @codexApi {"parent":"pithy.codex.testing-pyramid","name":"computeTestingStatus","stability":"stable","signature":"(reqs: TestingRequirements) => string"}
130
+ */
131
+ export function computeTestingStatus(reqs) {
132
+ const levels = [
133
+ ['unit', reqs.unit],
134
+ ['integration', reqs.integration],
135
+ ['e2e', reqs.e2e],
136
+ ];
137
+ const defined = levels.filter(([, level]) => level !== undefined);
138
+ if (defined.length === 0)
139
+ return 'N/A';
140
+ const missing = defined
141
+ .filter(([, level]) => level.required && !level.covered)
142
+ .map(([name]) => name);
143
+ return missing.length === 0 ? 'Complete' : `Missing ${missing.join(', ')}`;
144
+ }
145
+ // ── Internal Helpers ────────────────────────────────────────────
146
+ /**
147
+ * Deep-merge a test level override onto a default.
148
+ * Returns undefined if both are undefined (e.g. "types" category).
149
+ * Override fields win; unspecified fields retain defaults so that
150
+ * partial overrides (e.g. `{ covered: true }`) don't drop `required`.
151
+ */
152
+ function mergeTestLevel(base, override) {
153
+ if (!base && !override)
154
+ return undefined;
155
+ if (!override)
156
+ return base;
157
+ if (!base)
158
+ return override;
159
+ return { ...base, ...override };
160
+ }
161
+ function clamp(value, min, max) {
162
+ return Math.max(min, Math.min(max, value));
163
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.extraction.type-extractor",
5
+ * "title": "TypeScript Type Extractor",
6
+ * "category": "compiler"
7
+ * }
8
+ */
9
+ import type { ExtractedTypeDefinition } from './types.js';
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 declare function extractTypesFromFile(filePath: string, content: string): ExtractedTypeDefinition[];
19
+ /**
20
+ * @codexApi {"parent":"pithy.codex.extraction.type-extractor","name":"getTypeSignature","stability":"stable","signature":"(def: ExtractedTypeDefinition) => string"}
21
+ *
22
+ * Generates a human-readable signature for a type definition
23
+ * @param def - The extracted type definition
24
+ * @returns Formatted type signature
25
+ */
26
+ export declare function getTypeSignature(def: ExtractedTypeDefinition): string;
27
+ /**
28
+ * @codexApi {"parent":"pithy.codex.extraction.type-extractor","name":"generateMethodTable","stability":"stable","signature":"(def: ExtractedTypeDefinition) => string"}
29
+ *
30
+ * Generates a markdown table of members for documentation
31
+ * @param def - The extracted type definition
32
+ * @returns Markdown table string
33
+ */
34
+ export declare function generateMethodTable(def: ExtractedTypeDefinition): string;