@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.
- package/LICENSE +21 -0
- package/README.md +4401 -0
- package/dist/annotations.d.ts +106 -0
- package/dist/annotations.js +306 -0
- package/dist/apply.d.ts +2 -0
- package/dist/apply.js +80 -0
- package/dist/changed-scope.d.ts +23 -0
- package/dist/changed-scope.js +31 -0
- package/dist/check.d.ts +2 -0
- package/dist/check.js +117 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +67 -0
- package/dist/env.d.ts +5 -0
- package/dist/env.js +35 -0
- package/dist/extract-cli.d.ts +23 -0
- package/dist/extract-cli.js +192 -0
- package/dist/extraction/breaking-changes.d.ts +66 -0
- package/dist/extraction/breaking-changes.js +352 -0
- package/dist/extraction/example-extractor.d.ts +55 -0
- package/dist/extraction/example-extractor.js +272 -0
- package/dist/extraction/index.d.ts +27 -0
- package/dist/extraction/index.js +31 -0
- package/dist/extraction/jsdoc-parser.d.ts +43 -0
- package/dist/extraction/jsdoc-parser.js +274 -0
- package/dist/extraction/pipeline.d.ts +54 -0
- package/dist/extraction/pipeline.js +526 -0
- package/dist/extraction/readme-sync.d.ts +108 -0
- package/dist/extraction/readme-sync.js +592 -0
- package/dist/extraction/snapshot-store.d.ts +40 -0
- package/dist/extraction/snapshot-store.js +153 -0
- package/dist/extraction/source-linker.d.ts +80 -0
- package/dist/extraction/source-linker.js +316 -0
- package/dist/extraction/test-example-extractor.d.ts +69 -0
- package/dist/extraction/test-example-extractor.js +400 -0
- package/dist/extraction/test-pattern-extractor.d.ts +68 -0
- package/dist/extraction/test-pattern-extractor.js +261 -0
- package/dist/extraction/testing-pyramid.d.ts +44 -0
- package/dist/extraction/testing-pyramid.js +163 -0
- package/dist/extraction/type-extractor.d.ts +34 -0
- package/dist/extraction/type-extractor.js +494 -0
- package/dist/extraction/types.d.ts +401 -0
- package/dist/extraction/types.js +34 -0
- package/dist/indexer.d.ts +1 -0
- package/dist/indexer.js +107 -0
- package/dist/llm.d.ts +8 -0
- package/dist/llm.js +75 -0
- package/dist/readme-sync-cli.d.ts +20 -0
- package/dist/readme-sync-cli.js +167 -0
- package/dist/review.d.ts +2 -0
- package/dist/review.js +93 -0
- package/dist/scan.d.ts +29 -0
- package/dist/scan.js +221 -0
- package/dist/schema.d.ts +169 -0
- package/dist/schema.js +70 -0
- package/dist/snapshot-cli.d.ts +45 -0
- package/dist/snapshot-cli.js +217 -0
- package/dist/sync-cli.d.ts +22 -0
- package/dist/sync-cli.js +154 -0
- package/dist/sync-pipeline.d.ts +58 -0
- package/dist/sync-pipeline.js +104 -0
- package/dist/sync.d.ts +2 -0
- package/dist/sync.js +318 -0
- package/dist/validate-cli.d.ts +20 -0
- package/dist/validate-cli.js +144 -0
- package/dist/validate.d.ts +76 -0
- package/dist/validate.js +183 -0
- package/dist/watch-cli.d.ts +21 -0
- package/dist/watch-cli.js +220 -0
- 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;
|