@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,183 @@
1
+ /**
2
+ * @codex
3
+ * {
4
+ * "id": "pithy.codex.validate",
5
+ * "title": "Documentation Validator",
6
+ * "category": "feature"
7
+ * }
8
+ *
9
+ * Validates extracted documentation examples by checking they are
10
+ * syntactically valid TypeScript. Used by `pnpm codex validate` and CI drift checks.
11
+ */
12
+ import ts from 'typescript';
13
+ /**
14
+ * Languages we can validate syntactically
15
+ */
16
+ const VALIDATABLE_LANGUAGES = new Set(['typescript', 'ts', 'javascript', 'js']);
17
+ /**
18
+ * @codexApi {"parent":"pithy.codex.validate","name":"validateExampleSyntax","stability":"stable","signature":"(code: string, componentId?: string, apiName?: string, language?: string) => ExampleValidationResult"}
19
+ *
20
+ * Validates a single code example for syntactic correctness using TypeScript compiler.
21
+ * Does NOT type-check — only parses for syntax errors.
22
+ *
23
+ * @param code - The example code to validate
24
+ * @param componentId - The ID of the component the example belongs to (default: "")
25
+ * @param apiName - The name of the API the example belongs to
26
+ * @param language - The language of the example (default: "typescript")
27
+ * @returns Validation result with diagnostics if invalid
28
+ */
29
+ export function validateExampleSyntax(code, componentId = '', apiName, language = 'typescript') {
30
+ const lang = language.toLowerCase();
31
+ if (!VALIDATABLE_LANGUAGES.has(lang)) {
32
+ return {
33
+ code,
34
+ componentId,
35
+ apiName,
36
+ valid: true,
37
+ diagnostics: [],
38
+ };
39
+ }
40
+ const isJs = lang === 'javascript' || lang === 'js';
41
+ const fileName = isJs ? 'example.js' : 'example.ts';
42
+ const scriptKind = isJs ? ts.ScriptKind.JS : ts.ScriptKind.TS;
43
+ const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true, scriptKind);
44
+ // Collect parse diagnostics (syntax errors only)
45
+ const diagnostics = [];
46
+ // TypeScript's createSourceFile doesn't expose parseDiagnostics directly,
47
+ // so we use a minimal compiler host for syntax-only check
48
+ const compilerOptions = {
49
+ noEmit: true,
50
+ allowJs: true,
51
+ target: ts.ScriptTarget.Latest,
52
+ module: ts.ModuleKind.ESNext,
53
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
54
+ // Loose settings — we only care about syntax, not type errors
55
+ strict: false,
56
+ noImplicitAny: false,
57
+ skipLibCheck: true,
58
+ };
59
+ const host = ts.createCompilerHost(compilerOptions);
60
+ const originalGetSourceFile = host.getSourceFile;
61
+ host.getSourceFile = (fileName, languageVersion) => {
62
+ if (fileName === sourceFile.fileName) {
63
+ return sourceFile;
64
+ }
65
+ return originalGetSourceFile.call(host, fileName, languageVersion);
66
+ };
67
+ host.fileExists = fileName => fileName === sourceFile.fileName || ts.sys.fileExists(fileName);
68
+ host.readFile = fileName => fileName === sourceFile.fileName ? code : ts.sys.readFile(fileName);
69
+ const program = ts.createProgram([sourceFile.fileName], compilerOptions, host);
70
+ // Only get syntactic diagnostics (parse errors), not semantic
71
+ const syntacticDiags = program.getSyntacticDiagnostics(sourceFile);
72
+ for (const diag of syntacticDiags) {
73
+ const message = ts.flattenDiagnosticMessageText(diag.messageText, '\n');
74
+ const line = diag.file && diag.start !== undefined
75
+ ? diag.file.getLineAndCharacterOfPosition(diag.start).line + 1
76
+ : 0;
77
+ diagnostics.push(`Line ${line}: ${message}`);
78
+ }
79
+ return {
80
+ code,
81
+ componentId,
82
+ apiName,
83
+ valid: diagnostics.length === 0,
84
+ diagnostics,
85
+ };
86
+ }
87
+ /**
88
+ * @codexApi {"parent":"pithy.codex.validate","name":"validateExtractionResult","stability":"stable","signature":"(result: ExtendedExtractionResult) => ValidationResult"}
89
+ *
90
+ * Validates all extracted examples from an extraction pipeline result.
91
+ * Checks that TypeScript/JavaScript examples are syntactically valid.
92
+ *
93
+ * @param result - The extraction result to validate
94
+ * @returns Aggregate validation result
95
+ */
96
+ export function validateExtractionResult(result) {
97
+ const startTime = performance.now();
98
+ const invalidExamples = [];
99
+ let totalExamples = 0;
100
+ let validCount = 0;
101
+ let invalidCount = 0;
102
+ let skippedCount = 0;
103
+ for (const component of result.components) {
104
+ // Validate component-level examples
105
+ for (const example of component.examples) {
106
+ totalExamples++;
107
+ if (!VALIDATABLE_LANGUAGES.has(example.language.toLowerCase())) {
108
+ skippedCount++;
109
+ continue;
110
+ }
111
+ const validation = validateExampleSyntax(example.code, component.id, undefined, example.language);
112
+ if (validation.valid) {
113
+ validCount++;
114
+ }
115
+ else {
116
+ invalidCount++;
117
+ invalidExamples.push(validation);
118
+ }
119
+ }
120
+ // Validate API-level examples
121
+ for (const api of component.apis) {
122
+ for (const example of api.examples) {
123
+ totalExamples++;
124
+ if (!VALIDATABLE_LANGUAGES.has(example.language.toLowerCase())) {
125
+ skippedCount++;
126
+ continue;
127
+ }
128
+ const validation = validateExampleSyntax(example.code, component.id, api.name, example.language);
129
+ if (validation.valid) {
130
+ validCount++;
131
+ }
132
+ else {
133
+ invalidCount++;
134
+ invalidExamples.push(validation);
135
+ }
136
+ }
137
+ }
138
+ }
139
+ return {
140
+ totalExamples,
141
+ validCount,
142
+ invalidCount,
143
+ skippedCount,
144
+ invalidExamples,
145
+ totalTimeMs: performance.now() - startTime,
146
+ };
147
+ }
148
+ /**
149
+ * @codexApi {"parent":"pithy.codex.validate","name":"formatValidationReport","stability":"stable","signature":"(result: ValidationResult) => string"}
150
+ *
151
+ * Formats a human-readable validation report.
152
+ *
153
+ * @param result - Validation result to format
154
+ * @returns Formatted report string
155
+ */
156
+ export function formatValidationReport(result) {
157
+ const lines = [
158
+ 'šŸ“‹ Example Validation Report',
159
+ '',
160
+ `Total examples: ${result.totalExamples}`,
161
+ ` āœ… Valid: ${result.validCount}`,
162
+ ` āŒ Invalid: ${result.invalidCount}`,
163
+ ` ā­ Skipped: ${result.skippedCount}`,
164
+ ` ā± Time: ${result.totalTimeMs.toFixed(0)}ms`,
165
+ ];
166
+ if (result.invalidExamples.length > 0) {
167
+ lines.push('');
168
+ lines.push('Invalid Examples:');
169
+ for (const ex of result.invalidExamples) {
170
+ const location = ex.apiName
171
+ ? `${ex.componentId}.${ex.apiName}`
172
+ : ex.componentId;
173
+ lines.push(`\n ── ${location} ──`);
174
+ const firstLine = ex.code.split('\n')[0] || '';
175
+ const preview = firstLine.length > 80 ? firstLine.slice(0, 77) + '...' : firstLine;
176
+ lines.push(` Code: ${preview}`);
177
+ for (const diag of ex.diagnostics) {
178
+ lines.push(` ${diag}`);
179
+ }
180
+ }
181
+ }
182
+ return lines.join('\n');
183
+ }
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @codex
4
+ * {
5
+ * "id": "pithy.codex.watch-cli",
6
+ * "title": "Watch CLI",
7
+ * "category": "plugin"
8
+ * }
9
+ *
10
+ * Watch mode for codex sync. Monitors source files for changes and
11
+ * re-runs the sync pipeline automatically.
12
+ *
13
+ * Usage:
14
+ * pnpm codex watch [options]
15
+ *
16
+ * Options:
17
+ * --root <path> Project root directory (default: cwd)
18
+ * --debounce <ms> Debounce interval in milliseconds (default: 300)
19
+ * --verbose Show detailed progress
20
+ */
21
+ export {};
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @codex
4
+ * {
5
+ * "id": "pithy.codex.watch-cli",
6
+ * "title": "Watch CLI",
7
+ * "category": "plugin"
8
+ * }
9
+ *
10
+ * Watch mode for codex sync. Monitors source files for changes and
11
+ * re-runs the sync pipeline automatically.
12
+ *
13
+ * Usage:
14
+ * pnpm codex watch [options]
15
+ *
16
+ * Options:
17
+ * --root <path> Project root directory (default: cwd)
18
+ * --debounce <ms> Debounce interval in milliseconds (default: 300)
19
+ * --verbose Show detailed progress
20
+ */
21
+ import { resolve, relative } from 'node:path';
22
+ import fs from 'node:fs/promises';
23
+ import { watch } from 'node:fs';
24
+ import { runSyncPipeline } from './sync-pipeline.js';
25
+ function parseArgs(args) {
26
+ const options = {
27
+ root: process.cwd(),
28
+ debounceMs: 300,
29
+ verbose: false,
30
+ };
31
+ for (let i = 0; i < args.length; i++) {
32
+ const arg = args[i];
33
+ if (arg.startsWith('--root=')) {
34
+ options.root = arg.slice('--root='.length);
35
+ continue;
36
+ }
37
+ switch (arg) {
38
+ case '--root': {
39
+ const next = args[i + 1];
40
+ if (!next || next.startsWith('-')) {
41
+ console.error('Error: --root option requires a path argument.');
42
+ process.exit(1);
43
+ }
44
+ options.root = next;
45
+ i++;
46
+ break;
47
+ }
48
+ case '--debounce': {
49
+ const next = args[i + 1];
50
+ if (!next || isNaN(Number(next))) {
51
+ console.error('Error: --debounce option requires a number in milliseconds.');
52
+ process.exit(1);
53
+ }
54
+ options.debounceMs = Number(next);
55
+ i++;
56
+ break;
57
+ }
58
+ case '--verbose':
59
+ case '-v':
60
+ options.verbose = true;
61
+ break;
62
+ case '--help':
63
+ case '-h':
64
+ printHelp();
65
+ process.exit(0);
66
+ break;
67
+ default:
68
+ if (arg.startsWith('-')) {
69
+ console.warn(`Warning: unknown option "${arg}" (ignored)`);
70
+ }
71
+ }
72
+ }
73
+ return options;
74
+ }
75
+ function printHelp() {
76
+ console.log(`
77
+ Codex Watch
78
+
79
+ Watches for source file changes and re-runs the sync pipeline automatically.
80
+ Monitors TypeScript (.ts), JavaScript (.js), and README files in the packages/ directory.
81
+
82
+ Usage:
83
+ pithy-codex-watch [options]
84
+ pnpm --filter @pithyjs/codex watch [-- options]
85
+
86
+ Options:
87
+ --root <path> Project root directory (default: current directory)
88
+ --debounce <ms> Debounce interval in milliseconds (default: 300)
89
+ --verbose, -v Show detailed progress
90
+ --help, -h Show this help message
91
+
92
+ Examples:
93
+ # Watch from project root
94
+ pithy-codex-watch --root ../..
95
+
96
+ # Watch with longer debounce
97
+ pithy-codex-watch --root ../.. --debounce 1000 --verbose
98
+ `);
99
+ }
100
+ /** Directories to watch within the root */
101
+ const WATCH_DIRS = ['packages'];
102
+ /** File patterns to trigger re-sync */
103
+ function shouldTriggerSync(filename) {
104
+ if (!filename)
105
+ return false;
106
+ return (filename.endsWith('.ts') ||
107
+ filename.endsWith('.js') ||
108
+ /README(?:\.md)?$/i.test(filename));
109
+ }
110
+ /**
111
+ * Creates a debounced function that delays invoking func until after
112
+ * wait milliseconds have elapsed since the last time it was invoked.
113
+ */
114
+ function debounce(func, wait) {
115
+ let timeoutId = null;
116
+ return (...args) => {
117
+ if (timeoutId !== null) {
118
+ clearTimeout(timeoutId);
119
+ }
120
+ timeoutId = setTimeout(() => {
121
+ timeoutId = null;
122
+ func(...args);
123
+ }, wait);
124
+ };
125
+ }
126
+ async function main() {
127
+ const args = process.argv.slice(2);
128
+ const options = parseArgs(args);
129
+ options.root = resolve(options.root);
130
+ // Verify root exists
131
+ try {
132
+ const stat = await fs.stat(options.root);
133
+ if (!stat.isDirectory()) {
134
+ console.error(`āŒ Error: "${options.root}" is not a directory`);
135
+ process.exit(1);
136
+ }
137
+ }
138
+ catch {
139
+ console.error(`āŒ Error: Root directory "${options.root}" does not exist`);
140
+ process.exit(1);
141
+ }
142
+ const logger = {
143
+ log: msg => console.log(msg),
144
+ warn: msg => console.warn(msg),
145
+ error: msg => console.error(msg),
146
+ };
147
+ let running = false;
148
+ const runSync = async () => {
149
+ if (running)
150
+ return;
151
+ running = true;
152
+ console.log('\nšŸ”„ Change detected — re-syncing...\n');
153
+ try {
154
+ await runSyncPipeline({
155
+ root: options.root,
156
+ verbose: options.verbose,
157
+ }, logger);
158
+ }
159
+ catch (error) {
160
+ console.error('āŒ Sync failed:', error instanceof Error ? error.message : error);
161
+ }
162
+ running = false;
163
+ };
164
+ const debouncedSync = debounce(runSync, options.debounceMs);
165
+ // Initial sync
166
+ console.log('šŸ‘ Codex Watch Mode');
167
+ console.log(` Root: ${options.root}`);
168
+ console.log(` Debounce: ${options.debounceMs}ms`);
169
+ console.log('');
170
+ console.log('Running initial sync...');
171
+ await runSync();
172
+ // Set up file watchers
173
+ const activeWatchers = [];
174
+ const watchDirs = [];
175
+ for (const dir of WATCH_DIRS) {
176
+ const fullDir = resolve(options.root, dir);
177
+ try {
178
+ await fs.stat(fullDir);
179
+ watchDirs.push(fullDir);
180
+ }
181
+ catch {
182
+ // Directory doesn't exist, skip
183
+ }
184
+ }
185
+ if (watchDirs.length === 0) {
186
+ console.error('āŒ No watch directories found');
187
+ process.exit(1);
188
+ }
189
+ for (const dir of watchDirs) {
190
+ // Note: { recursive: true } is supported on Windows and macOS.
191
+ // On Linux, consider using chokidar for recursive watching.
192
+ const watcher = watch(dir, { recursive: true }, (_event, filename) => {
193
+ if (filename && shouldTriggerSync(filename)) {
194
+ if (options.verbose) {
195
+ console.log(` šŸ“„ ${relative(options.root, resolve(dir, filename))}`);
196
+ }
197
+ debouncedSync();
198
+ }
199
+ });
200
+ watcher.on('error', err => {
201
+ console.error(`Watch error on ${dir}:`, err.message);
202
+ });
203
+ activeWatchers.push(watcher);
204
+ console.log(` Watching: ${relative(options.root, dir)}/`);
205
+ }
206
+ // Cleanup on exit
207
+ process.on('SIGINT', () => {
208
+ for (const w of activeWatchers) {
209
+ w.close();
210
+ }
211
+ console.log('\nšŸ‘‹ Watch stopped');
212
+ process.exit(0);
213
+ });
214
+ console.log('');
215
+ console.log('Waiting for changes... (Ctrl+C to stop)');
216
+ }
217
+ main().catch(err => {
218
+ console.error('Fatal error:', err);
219
+ process.exit(1);
220
+ });
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@pithyjs/codex",
3
+ "version": "0.1.0-beta.0",
4
+ "type": "module",
5
+ "main": "dist/cli.js",
6
+ "exports": {
7
+ ".": "./dist/cli.js",
8
+ "./extraction": "./dist/extraction/index.js"
9
+ },
10
+ "bin": {
11
+ "pithy-codex": "dist/cli.js",
12
+ "pithy-codex-check": "dist/check.js",
13
+ "pithy-codex-index": "dist/indexer.js",
14
+ "pithy-codex-extract": "dist/extract-cli.js",
15
+ "pithy-codex-readme-sync": "dist/readme-sync-cli.js",
16
+ "pithy-codex-sync": "dist/sync-cli.js",
17
+ "pithy-codex-validate": "dist/validate-cli.js",
18
+ "pithy-codex-watch": "dist/watch-cli.js",
19
+ "pithy-codex-snapshot": "dist/snapshot-cli.js"
20
+ },
21
+ "dependencies": {
22
+ "dotenv": "^17.2.1",
23
+ "execa": "^9.6.0",
24
+ "globby": "^14.1.0",
25
+ "openai": "^5.12.2",
26
+ "simple-git": "^3.28.0",
27
+ "typescript": "^5.8.3",
28
+ "zod": "^3.24.4"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22",
32
+ "vitest": "3.2.4"
33
+ },
34
+ "license": "MIT",
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.json",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "sync": "node dist/sync.js --root ../../..",
46
+ "sync:direct": "node dist/sync.js --root ../../.. --no-llm --direct",
47
+ "sync:debug": "node dist/sync.js --root ../../.. --debug",
48
+ "sync:prompts": "node dist/sync.js --root ../../.. --emit-prompts --debug",
49
+ "sync-pipeline": "node dist/sync-cli.js",
50
+ "validate": "node dist/validate-cli.js",
51
+ "watch": "node dist/watch-cli.js",
52
+ "review": "node dist/review.js --root ../../..",
53
+ "apply": "node dist/apply.js --root ../../..",
54
+ "check": "node dist/check.js",
55
+ "index": "node dist/indexer.js",
56
+ "extract": "node dist/extract-cli.js --root ../..",
57
+ "readme-sync": "node dist/readme-sync-cli.js --root ../..",
58
+ "readme-sync:dry": "node dist/readme-sync-cli.js --root ../.. --dry-run --verbose",
59
+ "snapshot": "node dist/snapshot-cli.js --root ../..",
60
+ "gen": "pnpm -w --filter @pithyjs/codex sync && pnpm -w --filter @pithyjs/codex review && pnpm -w --filter @pithyjs/codex apply"
61
+ }
62
+ }