@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
package/dist/sync.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @codex
|
|
4
|
+
* {
|
|
5
|
+
* "id": "pithy.codex.sync",
|
|
6
|
+
* "title": "Codex Sync",
|
|
7
|
+
* "category": "feature"
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* Orchestrates full sync workflow (scan, generate, write entries).
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs/promises';
|
|
13
|
+
import { join, dirname } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
import { scan } from './scan.js';
|
|
16
|
+
import { codexSchema } from './schema.js';
|
|
17
|
+
import { askLLM } from './llm.js';
|
|
18
|
+
import { CHANGED_ONLY_PATHSPEC } from './changed-scope.js';
|
|
19
|
+
import { simpleGit } from 'simple-git';
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const nowIso = () => new Date().toISOString();
|
|
22
|
+
async function loadIndex(root) {
|
|
23
|
+
try {
|
|
24
|
+
const raw = await fs.readFile(join(root, 'codex.index.json'), 'utf8');
|
|
25
|
+
const j = JSON.parse(raw);
|
|
26
|
+
return (j.byId || {});
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function loadProposalHashes(root) {
|
|
33
|
+
const proposalsDir = join(root, 'codex', '.proposals');
|
|
34
|
+
const hashes = {};
|
|
35
|
+
try {
|
|
36
|
+
const files = await fs.readdir(proposalsDir);
|
|
37
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
38
|
+
for (const file of jsonFiles) {
|
|
39
|
+
try {
|
|
40
|
+
const content = await fs.readFile(join(proposalsDir, file), 'utf8');
|
|
41
|
+
const proposal = JSON.parse(content);
|
|
42
|
+
if (proposal.id && proposal.meta?.hash) {
|
|
43
|
+
hashes[proposal.id] = proposal.meta.hash;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Skip malformed proposals
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Proposals directory doesn't exist yet
|
|
53
|
+
}
|
|
54
|
+
return hashes;
|
|
55
|
+
}
|
|
56
|
+
function determineChangeStatus(currentHash, existingHash, proposalHash) {
|
|
57
|
+
// If no previous hash exists, it's new
|
|
58
|
+
if (!existingHash && !proposalHash) {
|
|
59
|
+
return 'NEW';
|
|
60
|
+
}
|
|
61
|
+
// Compare with most recent hash (proposal takes precedence over applied)
|
|
62
|
+
const prevHash = proposalHash || existingHash;
|
|
63
|
+
if (currentHash !== prevHash) {
|
|
64
|
+
return 'CHANGED';
|
|
65
|
+
}
|
|
66
|
+
return 'UNCHANGED';
|
|
67
|
+
}
|
|
68
|
+
async function lastModifiedISO(root, files) {
|
|
69
|
+
const git = simpleGit(root);
|
|
70
|
+
try {
|
|
71
|
+
const rel = files.map(f => {
|
|
72
|
+
const r = f.startsWith(root) ? f.slice(root.length + 1) : f;
|
|
73
|
+
return r.replace(/\\/g, '/');
|
|
74
|
+
});
|
|
75
|
+
const res = await git.raw(['log', '-1', '--format=%cI', '--', ...rel]);
|
|
76
|
+
const iso = res.trim();
|
|
77
|
+
return iso || null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function needsUpdate(entryUpdatedISO, codeISO) {
|
|
84
|
+
if (!entryUpdatedISO && codeISO)
|
|
85
|
+
return true;
|
|
86
|
+
if (!codeISO)
|
|
87
|
+
return false;
|
|
88
|
+
return new Date(codeISO) > new Date(entryUpdatedISO || 0);
|
|
89
|
+
}
|
|
90
|
+
function buildPrompt(existing, id, category, tags, codeContext) {
|
|
91
|
+
const rules = `
|
|
92
|
+
Return a single JSON object for this schema:
|
|
93
|
+
{ id, title, category, tags[], updated, related[] }
|
|
94
|
+
|
|
95
|
+
Rules:
|
|
96
|
+
- id must be exactly "${id}".
|
|
97
|
+
- category must be "${category}".
|
|
98
|
+
- Merge these tags first: [${tags.join(', ')}].
|
|
99
|
+
- updated must be current ISO datetime.
|
|
100
|
+
- related: propose 0–3 likely related ids (or [] if unsure).
|
|
101
|
+
- If an existing entry is provided, keep fields consistent and improve only where helpful (title/tags/related). Do not invent unrelated info.
|
|
102
|
+
Return ONLY JSON.
|
|
103
|
+
`;
|
|
104
|
+
const existingBlock = existing
|
|
105
|
+
? `\nExisting entry:\n${JSON.stringify(existing, null, 2)}\n`
|
|
106
|
+
: '';
|
|
107
|
+
return [
|
|
108
|
+
{
|
|
109
|
+
role: 'system',
|
|
110
|
+
content: 'You are CodexSync. Keep entries concise and accurate.',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
role: 'user',
|
|
114
|
+
content: `${rules}\n${existingBlock}\nCode context (truncated):\n${codeContext}`,
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
async function extractContext(files) {
|
|
119
|
+
const chunks = [];
|
|
120
|
+
for (const f of files.slice(0, 20)) {
|
|
121
|
+
const src = await fs.readFile(f, 'utf8').catch(() => '');
|
|
122
|
+
if (!src)
|
|
123
|
+
continue;
|
|
124
|
+
const head = src.split('\n').slice(0, 200).join('\n');
|
|
125
|
+
chunks.push(`// ${f}\n${head}`);
|
|
126
|
+
if (chunks.join('\n\n').length > 120_000)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
return chunks.join('\n\n');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Deduplicates APIs by name+signature, keeping the first occurrence.
|
|
133
|
+
*/
|
|
134
|
+
function deduplicateApis(apis) {
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
return apis.filter(api => {
|
|
137
|
+
const key = `${api.name}::${api.signature || ''}`;
|
|
138
|
+
if (seen.has(key))
|
|
139
|
+
return false;
|
|
140
|
+
seen.add(key);
|
|
141
|
+
return true;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Builds a codex entry directly from scan data without LLM enrichment.
|
|
146
|
+
* All required fields (id, title, category) come from @codex annotations;
|
|
147
|
+
* APIs come from @codexApi annotations. No AI needed.
|
|
148
|
+
*/
|
|
149
|
+
function buildEntryFromScan(c, existing) {
|
|
150
|
+
return {
|
|
151
|
+
id: c.id,
|
|
152
|
+
title: c.title || existing?.title || c.id.split('.').pop() || c.id,
|
|
153
|
+
category: c.category,
|
|
154
|
+
tags: Array.from(new Set([...(c.tags || []), ...(existing?.tags || [])])),
|
|
155
|
+
updated: nowIso(),
|
|
156
|
+
public: existing?.public ?? true,
|
|
157
|
+
status: existing?.status ?? 'draft',
|
|
158
|
+
version: existing?.version ?? '1.0',
|
|
159
|
+
related: existing?.related ?? [],
|
|
160
|
+
dependsOn: existing?.dependsOn ?? [],
|
|
161
|
+
apis: deduplicateApis(c.apis && c.apis.length > 0 ? c.apis : (existing?.apis ?? [])),
|
|
162
|
+
rids: existing?.rids ?? [],
|
|
163
|
+
acceptance: existing?.acceptance ?? [],
|
|
164
|
+
tests_expected: existing?.tests_expected ?? {
|
|
165
|
+
unit: [],
|
|
166
|
+
integration: [],
|
|
167
|
+
e2e: [],
|
|
168
|
+
property: [],
|
|
169
|
+
contract: [],
|
|
170
|
+
},
|
|
171
|
+
meta: c.hash ? { hash: c.hash } : (existing?.meta ?? {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async function main() {
|
|
175
|
+
const rootArg = process.argv.includes('--root')
|
|
176
|
+
? process.argv[process.argv.indexOf('--root') + 1]
|
|
177
|
+
: '../../..';
|
|
178
|
+
// Resolve relative to __dirname (packages/codex/dist/)
|
|
179
|
+
const root = join(__dirname, rootArg);
|
|
180
|
+
const mode = process.argv.includes('--mode')
|
|
181
|
+
? process.argv[process.argv.indexOf('--mode') + 1]
|
|
182
|
+
: 'batch'; // "batch" | "one"
|
|
183
|
+
const targetId = process.argv.includes('--id')
|
|
184
|
+
? process.argv[process.argv.indexOf('--id') + 1]
|
|
185
|
+
: null;
|
|
186
|
+
const noLlm = process.argv.includes('--no-llm');
|
|
187
|
+
const direct = process.argv.includes('--direct');
|
|
188
|
+
const changedOnly = process.argv.includes('--changed-only');
|
|
189
|
+
const baseBranch = process.argv.includes('--base')
|
|
190
|
+
? process.argv[process.argv.indexOf('--base') + 1]
|
|
191
|
+
: 'origin/master';
|
|
192
|
+
// --direct writes to codex/ directly (skips proposals/review/apply cycle)
|
|
193
|
+
// --no-llm without --direct still uses the proposals workflow
|
|
194
|
+
const outputDir = direct
|
|
195
|
+
? join(root, 'codex')
|
|
196
|
+
: join(root, 'codex', '.proposals');
|
|
197
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
198
|
+
// When --changed-only, use git diff to find changed source files
|
|
199
|
+
let changedFiles;
|
|
200
|
+
if (changedOnly) {
|
|
201
|
+
const git = simpleGit(root);
|
|
202
|
+
try {
|
|
203
|
+
const diff = await git.raw([
|
|
204
|
+
'diff',
|
|
205
|
+
'--name-only',
|
|
206
|
+
baseBranch + '...HEAD',
|
|
207
|
+
'--',
|
|
208
|
+
...CHANGED_ONLY_PATHSPEC,
|
|
209
|
+
]);
|
|
210
|
+
changedFiles = diff
|
|
211
|
+
.trim()
|
|
212
|
+
.split('\n')
|
|
213
|
+
.filter(Boolean)
|
|
214
|
+
.map(f => join(root, f));
|
|
215
|
+
console.log(`[Codex] Changed files: ${changedFiles.length} (vs ${baseBranch})`);
|
|
216
|
+
if (changedFiles.length === 0) {
|
|
217
|
+
console.log('No source files changed. Nothing to sync.');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
console.warn(`[Codex] Failed to detect changed files, falling back to full scan`);
|
|
223
|
+
changedFiles = undefined;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const indexById = await loadIndex(root);
|
|
227
|
+
const proposalHashes = await loadProposalHashes(root);
|
|
228
|
+
const candidates = (await scan(root, changedFiles)).filter(c => !targetId || c.id === targetId);
|
|
229
|
+
let generated = 0;
|
|
230
|
+
let skipped = 0;
|
|
231
|
+
for (const c of candidates) {
|
|
232
|
+
const existing = indexById[c.id] ?? null;
|
|
233
|
+
// Enhanced change detection using content hashes
|
|
234
|
+
if (c.hash) {
|
|
235
|
+
const existingHash = existing?.meta?.hash || null;
|
|
236
|
+
const proposalHash = proposalHashes[c.id] || null;
|
|
237
|
+
const changeStatus = determineChangeStatus(c.hash, existingHash, proposalHash);
|
|
238
|
+
if (changeStatus === 'UNCHANGED') {
|
|
239
|
+
skipped++;
|
|
240
|
+
if (process.env.NODE_ENV === 'development') {
|
|
241
|
+
console.log(`[Codex] Skipping unchanged: ${c.id}`);
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (process.env.NODE_ENV === 'development') {
|
|
246
|
+
console.log(`[Codex] Processing ${changeStatus}: ${c.id}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
// Fallback to legacy timestamp-based detection for glob-only candidates
|
|
251
|
+
const lastCodeISO = await lastModifiedISO(root, c.files);
|
|
252
|
+
const entryUpdated = existing?.updated ?? null;
|
|
253
|
+
if (existing && !needsUpdate(entryUpdated, lastCodeISO)) {
|
|
254
|
+
skipped++;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
let json;
|
|
259
|
+
if (noLlm) {
|
|
260
|
+
// Build entry directly from annotations — no AI required
|
|
261
|
+
json = buildEntryFromScan(c, existing);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const ctx = await extractContext(c.files);
|
|
265
|
+
const messages = buildPrompt(existing, c.id, c.category, c.tags, ctx);
|
|
266
|
+
const text = await askLLM(messages);
|
|
267
|
+
try {
|
|
268
|
+
json = JSON.parse(text);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
json.id = c.id;
|
|
274
|
+
json.category = c.category;
|
|
275
|
+
json.updated = nowIso();
|
|
276
|
+
json.tags = Array.from(new Set([...(c.tags || []), ...(json.tags || [])]));
|
|
277
|
+
// Add discovered APIs if available
|
|
278
|
+
if (c.apis && c.apis.length > 0) {
|
|
279
|
+
json.apis = c.apis;
|
|
280
|
+
}
|
|
281
|
+
// Add content hash for change detection
|
|
282
|
+
if (c.hash) {
|
|
283
|
+
json.meta = { hash: c.hash };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const parsed = codexSchema.safeParse(json);
|
|
287
|
+
if (!parsed.success) {
|
|
288
|
+
if (process.env.NODE_ENV === 'development') {
|
|
289
|
+
console.warn(`[Codex] Schema validation failed for ${c.id}:`, parsed.error);
|
|
290
|
+
}
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
await fs.writeFile(join(outputDir, `${c.id}.json`), JSON.stringify(parsed.data, null, 2));
|
|
294
|
+
generated++;
|
|
295
|
+
if (mode === 'one')
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
if (direct) {
|
|
299
|
+
console.log(`Codex entries written directly (generated ${generated}, skipped ${skipped} unchanged).`);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// Regenerate review.md for whatever exists in .proposals
|
|
303
|
+
const { execa } = await import('execa');
|
|
304
|
+
try {
|
|
305
|
+
await execa('node', [join(__dirname, 'review.js'), '--root', root], {
|
|
306
|
+
stdio: 'inherit',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
void 0;
|
|
311
|
+
}
|
|
312
|
+
console.log(`Proposals ready in codex/.proposals (generated ${generated}, skipped ${skipped} unchanged).`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
main().catch(e => {
|
|
316
|
+
console.error(e);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @codex
|
|
4
|
+
* {
|
|
5
|
+
* "id": "pithy.codex.validate-cli",
|
|
6
|
+
* "title": "Validate CLI",
|
|
7
|
+
* "category": "plugin"
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* CLI for validating extracted documentation examples.
|
|
11
|
+
* Checks that TypeScript/JavaScript examples are syntactically valid.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* pnpm codex validate [options]
|
|
15
|
+
*
|
|
16
|
+
* Options:
|
|
17
|
+
* --root <path> Project root directory (default: cwd)
|
|
18
|
+
* --verbose Show detailed progress
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @codex
|
|
4
|
+
* {
|
|
5
|
+
* "id": "pithy.codex.validate-cli",
|
|
6
|
+
* "title": "Validate CLI",
|
|
7
|
+
* "category": "plugin"
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* CLI for validating extracted documentation examples.
|
|
11
|
+
* Checks that TypeScript/JavaScript examples are syntactically valid.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* pnpm codex validate [options]
|
|
15
|
+
*
|
|
16
|
+
* Options:
|
|
17
|
+
* --root <path> Project root directory (default: cwd)
|
|
18
|
+
* --verbose Show detailed progress
|
|
19
|
+
*/
|
|
20
|
+
import { resolve } from 'node:path';
|
|
21
|
+
import fs from 'node:fs/promises';
|
|
22
|
+
import { runExtractionPipeline } from './extraction/pipeline.js';
|
|
23
|
+
import { validateExtractionResult, formatValidationReport, } from './validate.js';
|
|
24
|
+
function parseArgs(args) {
|
|
25
|
+
const options = {
|
|
26
|
+
root: process.cwd(),
|
|
27
|
+
verbose: false,
|
|
28
|
+
};
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
const arg = args[i];
|
|
31
|
+
if (arg.startsWith('--root=')) {
|
|
32
|
+
options.root = arg.slice('--root='.length);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
switch (arg) {
|
|
36
|
+
case '--root': {
|
|
37
|
+
const next = args[i + 1];
|
|
38
|
+
if (!next || next.startsWith('-')) {
|
|
39
|
+
console.error('Error: --root option requires a path argument.');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
options.root = next;
|
|
43
|
+
i++;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case '--verbose':
|
|
47
|
+
case '-v':
|
|
48
|
+
options.verbose = true;
|
|
49
|
+
break;
|
|
50
|
+
case '--help':
|
|
51
|
+
case '-h':
|
|
52
|
+
printHelp();
|
|
53
|
+
process.exit(0);
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
if (arg.startsWith('-')) {
|
|
57
|
+
console.warn(`Warning: unknown option "${arg}" (ignored)`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return options;
|
|
62
|
+
}
|
|
63
|
+
function printHelp() {
|
|
64
|
+
console.log(`
|
|
65
|
+
Codex Validate
|
|
66
|
+
|
|
67
|
+
Validates that extracted documentation examples are syntactically correct.
|
|
68
|
+
TypeScript and JavaScript examples are parsed for syntax errors.
|
|
69
|
+
|
|
70
|
+
Usage:
|
|
71
|
+
pithy-codex-validate [options]
|
|
72
|
+
pnpm --filter @pithyjs/codex validate [-- options]
|
|
73
|
+
|
|
74
|
+
Options:
|
|
75
|
+
--root <path> Project root directory (default: current directory)
|
|
76
|
+
--verbose, -v Show detailed progress
|
|
77
|
+
--help, -h Show this help message
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
# Validate examples in project
|
|
81
|
+
pithy-codex-validate --root ../..
|
|
82
|
+
|
|
83
|
+
# Validate with verbose output
|
|
84
|
+
pithy-codex-validate --root ../.. --verbose
|
|
85
|
+
|
|
86
|
+
Exit Codes:
|
|
87
|
+
0 - All examples are valid
|
|
88
|
+
1 - One or more examples have syntax errors
|
|
89
|
+
`);
|
|
90
|
+
}
|
|
91
|
+
async function main() {
|
|
92
|
+
const args = process.argv.slice(2);
|
|
93
|
+
const options = parseArgs(args);
|
|
94
|
+
options.root = resolve(options.root);
|
|
95
|
+
// Verify root exists
|
|
96
|
+
try {
|
|
97
|
+
const stat = await fs.stat(options.root);
|
|
98
|
+
if (!stat.isDirectory()) {
|
|
99
|
+
console.error(`❌ Error: "${options.root}" is not a directory`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
console.error(`❌ Error: Root directory "${options.root}" does not exist`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
if (options.verbose) {
|
|
108
|
+
console.log('🔍 Codex Validate');
|
|
109
|
+
console.log(` Root: ${options.root}`);
|
|
110
|
+
console.log('');
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
// Run extraction first to get examples
|
|
114
|
+
if (options.verbose) {
|
|
115
|
+
console.log('📂 Running extraction pipeline...');
|
|
116
|
+
}
|
|
117
|
+
const extraction = await runExtractionPipeline({
|
|
118
|
+
root: options.root,
|
|
119
|
+
analyzeTests: true,
|
|
120
|
+
linkReadmes: true,
|
|
121
|
+
extractRunnableExamples: true,
|
|
122
|
+
});
|
|
123
|
+
if (options.verbose) {
|
|
124
|
+
console.log(` Found ${extraction.stats.componentsFound} components with ${extraction.stats.apisExtracted} APIs`);
|
|
125
|
+
console.log('');
|
|
126
|
+
console.log('📋 Validating examples...');
|
|
127
|
+
}
|
|
128
|
+
// Validate
|
|
129
|
+
const result = validateExtractionResult(extraction);
|
|
130
|
+
// Report
|
|
131
|
+
console.log(formatValidationReport(result));
|
|
132
|
+
if (result.invalidCount > 0) {
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.error('❌ Validation failed:', error instanceof Error ? error.message : error);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
main().catch(err => {
|
|
142
|
+
console.error('Fatal error:', err);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
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 type { ExtendedExtractionResult } from './extraction/types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Result of validating a single example
|
|
15
|
+
*/
|
|
16
|
+
export interface ExampleValidationResult {
|
|
17
|
+
/** The example code that was validated */
|
|
18
|
+
code: string;
|
|
19
|
+
/** Component the example belongs to */
|
|
20
|
+
componentId: string;
|
|
21
|
+
/** API name (if applicable) */
|
|
22
|
+
apiName?: string;
|
|
23
|
+
/** Whether the example is syntactically valid */
|
|
24
|
+
valid: boolean;
|
|
25
|
+
/** Diagnostic messages if invalid */
|
|
26
|
+
diagnostics: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Aggregate result of validating all examples
|
|
30
|
+
*/
|
|
31
|
+
export interface ValidationResult {
|
|
32
|
+
/** Total examples checked */
|
|
33
|
+
totalExamples: number;
|
|
34
|
+
/** Number of valid examples */
|
|
35
|
+
validCount: number;
|
|
36
|
+
/** Number of invalid examples */
|
|
37
|
+
invalidCount: number;
|
|
38
|
+
/** Number of examples skipped (non-TypeScript/JavaScript) */
|
|
39
|
+
skippedCount: number;
|
|
40
|
+
/** Individual results for invalid examples only */
|
|
41
|
+
invalidExamples: ExampleValidationResult[];
|
|
42
|
+
/** Total validation time in ms */
|
|
43
|
+
totalTimeMs: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* @codexApi {"parent":"pithy.codex.validate","name":"validateExampleSyntax","stability":"stable","signature":"(code: string, componentId?: string, apiName?: string, language?: string) => ExampleValidationResult"}
|
|
47
|
+
*
|
|
48
|
+
* Validates a single code example for syntactic correctness using TypeScript compiler.
|
|
49
|
+
* Does NOT type-check — only parses for syntax errors.
|
|
50
|
+
*
|
|
51
|
+
* @param code - The example code to validate
|
|
52
|
+
* @param componentId - The ID of the component the example belongs to (default: "")
|
|
53
|
+
* @param apiName - The name of the API the example belongs to
|
|
54
|
+
* @param language - The language of the example (default: "typescript")
|
|
55
|
+
* @returns Validation result with diagnostics if invalid
|
|
56
|
+
*/
|
|
57
|
+
export declare function validateExampleSyntax(code: string, componentId?: string, apiName?: string, language?: string): ExampleValidationResult;
|
|
58
|
+
/**
|
|
59
|
+
* @codexApi {"parent":"pithy.codex.validate","name":"validateExtractionResult","stability":"stable","signature":"(result: ExtendedExtractionResult) => ValidationResult"}
|
|
60
|
+
*
|
|
61
|
+
* Validates all extracted examples from an extraction pipeline result.
|
|
62
|
+
* Checks that TypeScript/JavaScript examples are syntactically valid.
|
|
63
|
+
*
|
|
64
|
+
* @param result - The extraction result to validate
|
|
65
|
+
* @returns Aggregate validation result
|
|
66
|
+
*/
|
|
67
|
+
export declare function validateExtractionResult(result: ExtendedExtractionResult): ValidationResult;
|
|
68
|
+
/**
|
|
69
|
+
* @codexApi {"parent":"pithy.codex.validate","name":"formatValidationReport","stability":"stable","signature":"(result: ValidationResult) => string"}
|
|
70
|
+
*
|
|
71
|
+
* Formats a human-readable validation report.
|
|
72
|
+
*
|
|
73
|
+
* @param result - Validation result to format
|
|
74
|
+
* @returns Formatted report string
|
|
75
|
+
*/
|
|
76
|
+
export declare function formatValidationReport(result: ValidationResult): string;
|