@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,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @codex
|
|
3
|
+
* {
|
|
4
|
+
* "id": "pithy.codex.annotations",
|
|
5
|
+
* "title": "Codex Annotations",
|
|
6
|
+
* "category": "feature"
|
|
7
|
+
* }
|
|
8
|
+
*
|
|
9
|
+
* Parses @codex and @codexApi annotations from source files to discover
|
|
10
|
+
* components and APIs. Provides content normalization and fingerprinting
|
|
11
|
+
* for change detection.
|
|
12
|
+
*/
|
|
13
|
+
import type { RiskLevel, TestingRequirements } from './extraction/types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Component/Feature annotation format
|
|
16
|
+
*/
|
|
17
|
+
export interface CodexAnnotation {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
category: 'feature' | 'directive' | 'runtime' | 'compiler' | 'plugin' | 'types';
|
|
21
|
+
/** Risk level — affects testing requirements via pyramid modifiers */
|
|
22
|
+
risk?: RiskLevel;
|
|
23
|
+
/** Explicit testing requirements (overrides category defaults) */
|
|
24
|
+
testing?: TestingRequirements;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Public API annotation format
|
|
28
|
+
*/
|
|
29
|
+
export interface CodexApiAnnotation {
|
|
30
|
+
parent: string;
|
|
31
|
+
name: string;
|
|
32
|
+
stability?: 'internal' | 'experimental' | 'stable' | 'deprecated';
|
|
33
|
+
signature?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Discovered component with annotations and metadata
|
|
37
|
+
*/
|
|
38
|
+
export interface DiscoveredComponent {
|
|
39
|
+
id: string;
|
|
40
|
+
title: string;
|
|
41
|
+
category: string;
|
|
42
|
+
risk?: RiskLevel;
|
|
43
|
+
testing?: TestingRequirements;
|
|
44
|
+
apis: CodexApiAnnotation[];
|
|
45
|
+
files: string[];
|
|
46
|
+
sourceDigest: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parses @codex annotations from source code
|
|
50
|
+
* @param content - Source file content
|
|
51
|
+
* @returns Array of parsed CodexAnnotation objects
|
|
52
|
+
*
|
|
53
|
+
* @codexApi {"parent":"pithy.codex.annotations","name":"parseCodexAnnotations","stability":"stable","signature":"(content: string) => CodexAnnotation[]"}
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseCodexAnnotations(content: string): CodexAnnotation[];
|
|
56
|
+
/**
|
|
57
|
+
* Parses @codexApi annotations from source code
|
|
58
|
+
* @param content - Source file content
|
|
59
|
+
* @returns Array of parsed CodexApiAnnotation objects
|
|
60
|
+
*
|
|
61
|
+
* @codexApi {"parent":"pithy.codex.annotations","name":"parseCodexApiAnnotations","stability":"stable","signature":"(content: string) => CodexApiAnnotation[]"}
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseCodexApiAnnotations(content: string): CodexApiAnnotation[];
|
|
64
|
+
/**
|
|
65
|
+
* Normalizes source content for stable hashing by:
|
|
66
|
+
* - Removing comments (except annotations)
|
|
67
|
+
* - Normalizing whitespace
|
|
68
|
+
* - Preserving meaningful structure
|
|
69
|
+
* @param content - Source file content
|
|
70
|
+
* @returns Normalized content string
|
|
71
|
+
*/
|
|
72
|
+
export declare function normalizeSourceContent(content: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* Creates a stable content hash for a source file
|
|
75
|
+
* @param content - Source file content
|
|
76
|
+
* @returns SHA-256 hash of normalized content
|
|
77
|
+
*
|
|
78
|
+
* @codexApi {"parent":"pithy.codex.annotations","name":"createSourceDigest","stability":"stable","signature":"(content: string) => string"}
|
|
79
|
+
*/
|
|
80
|
+
export declare function createSourceDigest(content: string): string;
|
|
81
|
+
/**
|
|
82
|
+
* Creates a canonical fingerprint for a discovered component
|
|
83
|
+
* @param component - DiscoveredComponent to fingerprint
|
|
84
|
+
* @returns SHA-256 hash of canonical representation
|
|
85
|
+
*
|
|
86
|
+
* @codexApi {"parent":"pithy.codex.annotations","name":"createCanonicalFingerprint","stability":"stable","signature":"(component: DiscoveredComponent) => string"}
|
|
87
|
+
*/
|
|
88
|
+
export declare function createCanonicalFingerprint(component: DiscoveredComponent): string;
|
|
89
|
+
/**
|
|
90
|
+
* Discovers components from a single source file using annotations
|
|
91
|
+
* @param filePath - Path to the source file
|
|
92
|
+
* @param content - File content (optional, will read if not provided)
|
|
93
|
+
* @returns Array of discovered components
|
|
94
|
+
*
|
|
95
|
+
* @codexApi {"parent":"pithy.codex.annotations","name":"discoverComponentsFromFile","stability":"stable","signature":"(filePath: string, content?: string) => Promise<DiscoveredComponent[]>"}
|
|
96
|
+
*/
|
|
97
|
+
export declare function discoverComponentsFromFile(filePath: string, content?: string): Promise<DiscoveredComponent[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Validates annotation format and logs issues in DEV mode
|
|
100
|
+
* @param annotation - Annotation to validate
|
|
101
|
+
* @param type - Type of annotation for logging
|
|
102
|
+
* @returns True if valid
|
|
103
|
+
*
|
|
104
|
+
* @codexApi {"parent":"pithy.codex.annotations","name":"validateAnnotation","stability":"stable","signature":"(annotation: unknown, type: 'codex' | 'codexApi') => boolean"}
|
|
105
|
+
*/
|
|
106
|
+
export declare function validateAnnotation(annotation: unknown, type: 'codex' | 'codexApi'): boolean;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @codex
|
|
3
|
+
* {
|
|
4
|
+
* "id": "pithy.codex.annotations",
|
|
5
|
+
* "title": "Codex Annotations",
|
|
6
|
+
* "category": "feature"
|
|
7
|
+
* }
|
|
8
|
+
*
|
|
9
|
+
* Parses @codex and @codexApi annotations from source files to discover
|
|
10
|
+
* components and APIs. Provides content normalization and fingerprinting
|
|
11
|
+
* for change detection.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs/promises';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
/**
|
|
16
|
+
* Regex patterns for extracting annotations from source files.
|
|
17
|
+
*
|
|
18
|
+
* CODEX_JSON_BODY supports up to 3 levels of nesting (enough for
|
|
19
|
+
* `{ "testing": { "unit": { "required": true } } }`).
|
|
20
|
+
*
|
|
21
|
+
* Each BRACE_CONTENT level matches: non-brace/non-quote chars, quoted
|
|
22
|
+
* strings, or a deeper nested `{ ... }` group.
|
|
23
|
+
*/
|
|
24
|
+
const BRACE_CONTENT_L2 = String.raw `[^{}"]|"(?:[^"\\]|\\.)*"`;
|
|
25
|
+
const BRACE_CONTENT_L1 = String.raw `${BRACE_CONTENT_L2}|\{(?:${BRACE_CONTENT_L2})*\}`;
|
|
26
|
+
const BRACE_CONTENT_L0 = String.raw `${BRACE_CONTENT_L2}|\{(?:${BRACE_CONTENT_L1})*\}`;
|
|
27
|
+
const CODEX_JSON_BODY = `(\\{(?:${BRACE_CONTENT_L0})*\\})`;
|
|
28
|
+
const CODEX_ANNOTATION_REGEX = new RegExp(String.raw `/\*\*[\s*]*@codex[\s*]*` +
|
|
29
|
+
CODEX_JSON_BODY +
|
|
30
|
+
String.raw `[\s\S]*?\*/`, 'g');
|
|
31
|
+
// Matches @codexApi on a starred JSDoc line (e.g. ` * @codexApi {...}`).
|
|
32
|
+
// Used as a second pass inside JSDoc blocks found by JSDOC_BLOCK_REGEX
|
|
33
|
+
// to prevent false positives from regular /* ... */ block comments.
|
|
34
|
+
const CODEX_API_ANNOTATION_REGEX = new RegExp(String.raw `\*\s*@codexApi\s*` + CODEX_JSON_BODY, 'g');
|
|
35
|
+
const JSDOC_BLOCK_REGEX = /\/\*\*[\s\S]*?\*\//g;
|
|
36
|
+
/**
|
|
37
|
+
* Parses @codex annotations from source code
|
|
38
|
+
* @param content - Source file content
|
|
39
|
+
* @returns Array of parsed CodexAnnotation objects
|
|
40
|
+
*
|
|
41
|
+
* @codexApi {"parent":"pithy.codex.annotations","name":"parseCodexAnnotations","stability":"stable","signature":"(content: string) => CodexAnnotation[]"}
|
|
42
|
+
*/
|
|
43
|
+
export function parseCodexAnnotations(content) {
|
|
44
|
+
const annotations = [];
|
|
45
|
+
let match;
|
|
46
|
+
while ((match = CODEX_ANNOTATION_REGEX.exec(content)) !== null) {
|
|
47
|
+
try {
|
|
48
|
+
// Strip JSDoc line-prefix stars from multi-line JSON bodies
|
|
49
|
+
const jsonStr = match[1].replace(/^\s*\*\s?/gm, '');
|
|
50
|
+
const parsed = JSON.parse(jsonStr);
|
|
51
|
+
// Validate required fields and optional field shapes
|
|
52
|
+
if (validateAnnotation(parsed, 'codex')) {
|
|
53
|
+
annotations.push(parsed);
|
|
54
|
+
if (process.env.NODE_ENV === 'development') {
|
|
55
|
+
console.log(`[Codex] Found @codex annotation: ${parsed.id}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (process.env.NODE_ENV === 'development') {
|
|
61
|
+
console.warn(`[Codex] Failed to parse @codex annotation:`, match[1], error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return annotations;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Parses @codexApi annotations from source code
|
|
69
|
+
* @param content - Source file content
|
|
70
|
+
* @returns Array of parsed CodexApiAnnotation objects
|
|
71
|
+
*
|
|
72
|
+
* @codexApi {"parent":"pithy.codex.annotations","name":"parseCodexApiAnnotations","stability":"stable","signature":"(content: string) => CodexApiAnnotation[]"}
|
|
73
|
+
*/
|
|
74
|
+
export function parseCodexApiAnnotations(content) {
|
|
75
|
+
const annotations = [];
|
|
76
|
+
// Two-pass: first find JSDoc blocks (/** ... */), then extract
|
|
77
|
+
// @codexApi within each. This prevents false positives from
|
|
78
|
+
// regular /* ... */ block comments that happen to contain @codexApi.
|
|
79
|
+
JSDOC_BLOCK_REGEX.lastIndex = 0;
|
|
80
|
+
let blockMatch;
|
|
81
|
+
while ((blockMatch = JSDOC_BLOCK_REGEX.exec(content)) !== null) {
|
|
82
|
+
const block = blockMatch[0];
|
|
83
|
+
CODEX_API_ANNOTATION_REGEX.lastIndex = 0;
|
|
84
|
+
let match;
|
|
85
|
+
while ((match = CODEX_API_ANNOTATION_REGEX.exec(block)) !== null) {
|
|
86
|
+
try {
|
|
87
|
+
const jsonStr = match[1];
|
|
88
|
+
const parsed = JSON.parse(jsonStr);
|
|
89
|
+
// Validate required fields
|
|
90
|
+
if (parsed.parent && parsed.name) {
|
|
91
|
+
annotations.push({
|
|
92
|
+
...parsed,
|
|
93
|
+
stability: parsed.stability || 'stable',
|
|
94
|
+
});
|
|
95
|
+
if (process.env.NODE_ENV === 'development') {
|
|
96
|
+
console.log(`[Codex] Found @codexApi annotation: ${parsed.parent}.${parsed.name}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (process.env.NODE_ENV === 'development') {
|
|
101
|
+
console.warn(`[Codex] Invalid @codexApi annotation missing required fields:`, parsed);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (process.env.NODE_ENV === 'development') {
|
|
107
|
+
console.warn(`[Codex] Failed to parse @codexApi annotation:`, match[1], error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return annotations;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Normalizes source content for stable hashing by:
|
|
116
|
+
* - Removing comments (except annotations)
|
|
117
|
+
* - Normalizing whitespace
|
|
118
|
+
* - Preserving meaningful structure
|
|
119
|
+
* @param content - Source file content
|
|
120
|
+
* @returns Normalized content string
|
|
121
|
+
*/
|
|
122
|
+
export function normalizeSourceContent(content) {
|
|
123
|
+
return (content
|
|
124
|
+
// Normalize line endings first for cross-platform consistency
|
|
125
|
+
.replace(/\r\n/g, '\n')
|
|
126
|
+
// Preserve JSDoc blocks (/** ... */) containing @codex or @codexApi annotations,
|
|
127
|
+
// remove all other block comments. Only JSDoc blocks are preserved because
|
|
128
|
+
// parseCodexApiAnnotations only extracts from /** ... */ blocks.
|
|
129
|
+
.replace(/\/\*[\s\S]*?\*\//g, match => /^\/\*\*/.test(match) && /@codex(?:Api)?/.test(match)
|
|
130
|
+
? `ANNOTATION:${createHash('md5').update(match).digest('hex')}`
|
|
131
|
+
: '')
|
|
132
|
+
.replace(/\/\/.*$/gm, '')
|
|
133
|
+
// Normalize whitespace
|
|
134
|
+
.replace(/\s+/g, ' ')
|
|
135
|
+
.trim());
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Creates a stable content hash for a source file
|
|
139
|
+
* @param content - Source file content
|
|
140
|
+
* @returns SHA-256 hash of normalized content
|
|
141
|
+
*
|
|
142
|
+
* @codexApi {"parent":"pithy.codex.annotations","name":"createSourceDigest","stability":"stable","signature":"(content: string) => string"}
|
|
143
|
+
*/
|
|
144
|
+
export function createSourceDigest(content) {
|
|
145
|
+
const normalized = normalizeSourceContent(content);
|
|
146
|
+
return createHash('sha256').update(normalized, 'utf8').digest('hex');
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Creates a canonical fingerprint for a discovered component
|
|
150
|
+
* @param component - DiscoveredComponent to fingerprint
|
|
151
|
+
* @returns SHA-256 hash of canonical representation
|
|
152
|
+
*
|
|
153
|
+
* @codexApi {"parent":"pithy.codex.annotations","name":"createCanonicalFingerprint","stability":"stable","signature":"(component: DiscoveredComponent) => string"}
|
|
154
|
+
*/
|
|
155
|
+
export function createCanonicalFingerprint(component) {
|
|
156
|
+
const canonical = JSON.stringify({
|
|
157
|
+
id: component.id,
|
|
158
|
+
title: component.title,
|
|
159
|
+
category: component.category,
|
|
160
|
+
apis: component.apis.sort((a, b) => a.name.localeCompare(b.name)), // deterministic order
|
|
161
|
+
sourceDigest: component.sourceDigest,
|
|
162
|
+
});
|
|
163
|
+
return createHash('sha256').update(canonical, 'utf8').digest('hex');
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Discovers components from a single source file using annotations
|
|
167
|
+
* @param filePath - Path to the source file
|
|
168
|
+
* @param content - File content (optional, will read if not provided)
|
|
169
|
+
* @returns Array of discovered components
|
|
170
|
+
*
|
|
171
|
+
* @codexApi {"parent":"pithy.codex.annotations","name":"discoverComponentsFromFile","stability":"stable","signature":"(filePath: string, content?: string) => Promise<DiscoveredComponent[]>"}
|
|
172
|
+
*/
|
|
173
|
+
export async function discoverComponentsFromFile(filePath, content) {
|
|
174
|
+
const fileContent = content || (await fs.readFile(filePath, 'utf8').catch(() => ''));
|
|
175
|
+
if (!fileContent)
|
|
176
|
+
return [];
|
|
177
|
+
const codexAnnotations = parseCodexAnnotations(fileContent);
|
|
178
|
+
const apiAnnotations = parseCodexApiAnnotations(fileContent);
|
|
179
|
+
const sourceDigest = createSourceDigest(fileContent);
|
|
180
|
+
const components = [];
|
|
181
|
+
for (const annotation of codexAnnotations) {
|
|
182
|
+
// Find APIs that belong to this component
|
|
183
|
+
const componentApis = apiAnnotations.filter(api => api.parent === annotation.id);
|
|
184
|
+
components.push({
|
|
185
|
+
id: annotation.id,
|
|
186
|
+
title: annotation.title,
|
|
187
|
+
category: annotation.category,
|
|
188
|
+
risk: annotation.risk,
|
|
189
|
+
testing: annotation.testing,
|
|
190
|
+
apis: componentApis,
|
|
191
|
+
files: [filePath],
|
|
192
|
+
sourceDigest,
|
|
193
|
+
});
|
|
194
|
+
if (process.env.NODE_ENV === 'development') {
|
|
195
|
+
console.log(`[Codex] Discovered component: ${annotation.id} with ${componentApis.length} APIs`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return components;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Validates annotation format and logs issues in DEV mode
|
|
202
|
+
* @param annotation - Annotation to validate
|
|
203
|
+
* @param type - Type of annotation for logging
|
|
204
|
+
* @returns True if valid
|
|
205
|
+
*
|
|
206
|
+
* @codexApi {"parent":"pithy.codex.annotations","name":"validateAnnotation","stability":"stable","signature":"(annotation: unknown, type: 'codex' | 'codexApi') => boolean"}
|
|
207
|
+
*/
|
|
208
|
+
export function validateAnnotation(annotation, type) {
|
|
209
|
+
if (typeof annotation !== 'object' || annotation === null) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const obj = annotation;
|
|
213
|
+
if (type === 'codex') {
|
|
214
|
+
const required = ['id', 'title', 'category'];
|
|
215
|
+
const missing = required.filter(field => !obj[field]);
|
|
216
|
+
if (missing.length > 0) {
|
|
217
|
+
if (process.env.NODE_ENV === 'development') {
|
|
218
|
+
console.warn(`[Codex] Invalid @codex annotation missing: ${missing.join(', ')}`, obj);
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const validCategories = [
|
|
223
|
+
'feature',
|
|
224
|
+
'directive',
|
|
225
|
+
'runtime',
|
|
226
|
+
'compiler',
|
|
227
|
+
'plugin',
|
|
228
|
+
'types',
|
|
229
|
+
];
|
|
230
|
+
if (!validCategories.includes(obj.category)) {
|
|
231
|
+
if (process.env.NODE_ENV === 'development') {
|
|
232
|
+
console.warn(`[Codex] Invalid @codex category: ${obj.category}. Must be one of: ${validCategories.join(', ')}`);
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
// Validate optional risk field
|
|
237
|
+
if (obj.risk !== undefined) {
|
|
238
|
+
const validRiskLevels = ['critical', 'high', 'medium', 'low'];
|
|
239
|
+
if (!validRiskLevels.includes(obj.risk)) {
|
|
240
|
+
if (process.env.NODE_ENV === 'development') {
|
|
241
|
+
console.warn(`[Codex] Invalid @codex risk level: ${obj.risk}. Must be one of: ${validRiskLevels.join(', ')}`);
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Validate optional testing field structure
|
|
247
|
+
if (obj.testing !== undefined) {
|
|
248
|
+
if (typeof obj.testing !== 'object' || obj.testing === null) {
|
|
249
|
+
if (process.env.NODE_ENV === 'development') {
|
|
250
|
+
console.warn(`[Codex] Invalid @codex testing field: must be an object`);
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
const testing = obj.testing;
|
|
255
|
+
const validTestLevels = ['unit', 'integration', 'e2e'];
|
|
256
|
+
for (const key of Object.keys(testing)) {
|
|
257
|
+
if (!validTestLevels.includes(key)) {
|
|
258
|
+
if (process.env.NODE_ENV === 'development') {
|
|
259
|
+
console.warn(`[Codex] Invalid @codex testing level: ${key}. Must be one of: ${validTestLevels.join(', ')}`);
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
const rawLevel = testing[key];
|
|
264
|
+
if (typeof rawLevel !== 'object' || rawLevel === null) {
|
|
265
|
+
if (process.env.NODE_ENV === 'development') {
|
|
266
|
+
console.warn(`[Codex] Invalid @codex testing.${key}: must be an object`);
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
const level = rawLevel;
|
|
271
|
+
if ('required' in level && typeof level.required !== 'boolean') {
|
|
272
|
+
if (process.env.NODE_ENV === 'development') {
|
|
273
|
+
console.warn(`[Codex] Invalid @codex testing.${key}.required: must be a boolean`);
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
if ('coverage' in level &&
|
|
278
|
+
(typeof level.coverage !== 'number' ||
|
|
279
|
+
level.coverage < 0 ||
|
|
280
|
+
level.coverage > 100)) {
|
|
281
|
+
if (process.env.NODE_ENV === 'development') {
|
|
282
|
+
console.warn(`[Codex] Invalid @codex testing.${key}.coverage: must be a number between 0 and 100`);
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
if ('covered' in level && typeof level.covered !== 'boolean') {
|
|
287
|
+
if (process.env.NODE_ENV === 'development') {
|
|
288
|
+
console.warn(`[Codex] Invalid @codex testing.${key}.covered: must be a boolean`);
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (type === 'codexApi') {
|
|
296
|
+
const required = ['parent', 'name'];
|
|
297
|
+
const missing = required.filter(field => !obj[field]);
|
|
298
|
+
if (missing.length > 0) {
|
|
299
|
+
if (process.env.NODE_ENV === 'development') {
|
|
300
|
+
console.warn(`[Codex] Invalid @codexApi annotation missing: ${missing.join(', ')}`, obj);
|
|
301
|
+
}
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
package/dist/apply.d.ts
ADDED
package/dist/apply.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @codex
|
|
4
|
+
* {
|
|
5
|
+
* "id": "pithy.codex.apply",
|
|
6
|
+
* "title": "Entry Applier",
|
|
7
|
+
* "category": "feature"
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* Applies reviewed codex entries to the knowledge base. Reads review.md for
|
|
11
|
+
* [x]-marked approvals, validates each proposal against the Zod schema, and
|
|
12
|
+
* copies approved JSON entries from .proposals/ into the codex/ directory.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import { join, dirname, basename } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { codexSchema } from './schema.js';
|
|
18
|
+
import { execa } from 'execa';
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
async function main() {
|
|
21
|
+
const root = process.argv.includes('--root')
|
|
22
|
+
? process.argv[process.argv.indexOf('--root') + 1]
|
|
23
|
+
: join(__dirname, '../../..');
|
|
24
|
+
const proposalsDir = join(root, 'codex', '.proposals');
|
|
25
|
+
const reviewPath = join(proposalsDir, 'review.md');
|
|
26
|
+
const review = await fs.readFile(reviewPath, 'utf8').catch(() => '');
|
|
27
|
+
const APPROVED_RE = /^\s*-\s*\[(x|X)\]\s*.*?\*\*([^*]+)\*\*/gmu;
|
|
28
|
+
const matches = Array.from(review.matchAll(APPROVED_RE));
|
|
29
|
+
console.log(`[DEBUG] Found ${matches.length} regex matches`);
|
|
30
|
+
if (matches.length > 0) {
|
|
31
|
+
console.log(`[DEBUG] First match: '${matches[0][2].trim()}'`);
|
|
32
|
+
}
|
|
33
|
+
const approvedIds = matches.map(m => m[2].trim());
|
|
34
|
+
if (approvedIds.length === 0) {
|
|
35
|
+
console.log('No approved proposals found. Edit codex/.proposals/review.md and mark with [x].');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const outDir = join(root, 'codex');
|
|
39
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
40
|
+
for (const id of approvedIds) {
|
|
41
|
+
const file = join(proposalsDir, `${id}.json`);
|
|
42
|
+
const raw = await fs.readFile(file, 'utf8');
|
|
43
|
+
const json = JSON.parse(raw);
|
|
44
|
+
json.updated = new Date().toISOString();
|
|
45
|
+
// Preserve hash metadata for future change detection
|
|
46
|
+
if (json.meta?.hash) {
|
|
47
|
+
if (process.env.NODE_ENV === 'development') {
|
|
48
|
+
console.log(`[Codex] Preserving hash for ${id}: ${json.meta.hash.substring(0, 8)}...`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const parsed = codexSchema.safeParse(json);
|
|
52
|
+
if (!parsed.success) {
|
|
53
|
+
console.error(`❌ Invalid entry ${id}:`, parsed.error.issues.map(i => i.message).join('; '));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const dest = join(outDir, `${id}.json`);
|
|
57
|
+
try {
|
|
58
|
+
await fs.access(dest);
|
|
59
|
+
console.log(`↷ Updating ${basename(dest)}`);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
console.log(`+ Creating ${basename(dest)}`);
|
|
63
|
+
}
|
|
64
|
+
await fs.writeFile(dest, JSON.stringify(parsed.data, null, 2));
|
|
65
|
+
console.log(`✅ Wrote ${basename(dest)}`);
|
|
66
|
+
}
|
|
67
|
+
// Rebuild index via the package script
|
|
68
|
+
try {
|
|
69
|
+
await execa('pnpm', ['-w', '--filter', '@pithyjs/codex', 'index'], {
|
|
70
|
+
stdio: 'inherit',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
console.log('Run `pnpm -w --filter @pithyjs/codex index` manually if needed.');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
main().catch(e => {
|
|
78
|
+
console.error(e);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @codex { "id": "pithy.codex.changed-scope", "title": "Codex Changed-File Scope", "category": "types" } */
|
|
2
|
+
/**
|
|
3
|
+
* Which files `--changed-only` looks at.
|
|
4
|
+
*
|
|
5
|
+
* These are GIT PATHSPECS, not globs, and the difference is not cosmetic: git's
|
|
6
|
+
* `*` already crosses `/`, while its doubled form requires at least one
|
|
7
|
+
* intervening directory. The previous spelling resolved 875 files and dropped
|
|
8
|
+
* every file sitting directly in a `src/` — 114 of them, including
|
|
9
|
+
* `packages/signals/src/signal.ts` and `apps/studio/src/main.ts` — while the
|
|
10
|
+
* same string handed to globby in `scan.ts` matches all 989. One spelling, two
|
|
11
|
+
* engines, two scopes.
|
|
12
|
+
*
|
|
13
|
+
* The test directories are here because `codex-sync.yml` triggers on them: a
|
|
14
|
+
* marker-only change would otherwise reach an empty changed set and return at
|
|
15
|
+
* "Nothing to sync". The marker itself is harvested by the pipeline step, which
|
|
16
|
+
* globs `testPatterns` and is not changed-scoped — this keeps the two filters
|
|
17
|
+
* describing one set rather than gating that.
|
|
18
|
+
*
|
|
19
|
+
* Its own module because `sync.ts` calls `main()` at import: anything importing
|
|
20
|
+
* the constant from there would start a full sync during module evaluation.
|
|
21
|
+
*/
|
|
22
|
+
/** @codexApi {"parent":"pithy.codex.changed-scope","name":"CHANGED_ONLY_PATHSPEC","stability":"stable","signature":"readonly string[]"} */
|
|
23
|
+
export declare const CHANGED_ONLY_PATHSPEC: readonly string[];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** @codex { "id": "pithy.codex.changed-scope", "title": "Codex Changed-File Scope", "category": "types" } */
|
|
2
|
+
/**
|
|
3
|
+
* Which files `--changed-only` looks at.
|
|
4
|
+
*
|
|
5
|
+
* These are GIT PATHSPECS, not globs, and the difference is not cosmetic: git's
|
|
6
|
+
* `*` already crosses `/`, while its doubled form requires at least one
|
|
7
|
+
* intervening directory. The previous spelling resolved 875 files and dropped
|
|
8
|
+
* every file sitting directly in a `src/` — 114 of them, including
|
|
9
|
+
* `packages/signals/src/signal.ts` and `apps/studio/src/main.ts` — while the
|
|
10
|
+
* same string handed to globby in `scan.ts` matches all 989. One spelling, two
|
|
11
|
+
* engines, two scopes.
|
|
12
|
+
*
|
|
13
|
+
* The test directories are here because `codex-sync.yml` triggers on them: a
|
|
14
|
+
* marker-only change would otherwise reach an empty changed set and return at
|
|
15
|
+
* "Nothing to sync". The marker itself is harvested by the pipeline step, which
|
|
16
|
+
* globs `testPatterns` and is not changed-scoped — this keeps the two filters
|
|
17
|
+
* describing one set rather than gating that.
|
|
18
|
+
*
|
|
19
|
+
* Its own module because `sync.ts` calls `main()` at import: anything importing
|
|
20
|
+
* the constant from there would start a full sync during module evaluation.
|
|
21
|
+
*/
|
|
22
|
+
/** @codexApi {"parent":"pithy.codex.changed-scope","name":"CHANGED_ONLY_PATHSPEC","stability":"stable","signature":"readonly string[]"} */
|
|
23
|
+
export const CHANGED_ONLY_PATHSPEC = [
|
|
24
|
+
'packages/*/src/*.ts',
|
|
25
|
+
'packages/*/src/*.js',
|
|
26
|
+
'apps/*/src/*.ts',
|
|
27
|
+
'apps/*/src/*.js',
|
|
28
|
+
'packages/*/tests/*.ts',
|
|
29
|
+
'apps/*/tests/*.ts',
|
|
30
|
+
'e2e/tests/*.ts',
|
|
31
|
+
];
|
package/dist/check.d.ts
ADDED
package/dist/check.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @codex
|
|
4
|
+
* {
|
|
5
|
+
* "id": "pithy.codex.check",
|
|
6
|
+
* "title": "Integrity Checker",
|
|
7
|
+
* "category": "feature"
|
|
8
|
+
* }
|
|
9
|
+
*
|
|
10
|
+
* Validates codex entries against source annotations. Walks all JSON files in
|
|
11
|
+
* codex/, validates each against the Zod schema, checks for duplicate IDs,
|
|
12
|
+
* verifies ISO date formats, and reports errors/warnings.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import { join, dirname } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { codexSchema } from './schema.js';
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const repoRoot = join(__dirname, '../../..');
|
|
20
|
+
const codexDir = join(repoRoot, 'codex');
|
|
21
|
+
async function getJsonFiles(dir) {
|
|
22
|
+
const out = [];
|
|
23
|
+
try {
|
|
24
|
+
const dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
25
|
+
for (const d of dirents) {
|
|
26
|
+
const p = join(dir, d.name);
|
|
27
|
+
// Skip .proposals directory to avoid duplicate ID errors
|
|
28
|
+
if (d.isDirectory() && d.name !== '.proposals') {
|
|
29
|
+
out.push(...(await getJsonFiles(p)));
|
|
30
|
+
}
|
|
31
|
+
else if (d.isFile() && p.endsWith('.json')) {
|
|
32
|
+
out.push(p);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// no codex dir yet
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function isIsoDate(s) {
|
|
42
|
+
// Zod .datetime() already checks, but we double-check for clarity
|
|
43
|
+
const d = new Date(s);
|
|
44
|
+
return !Number.isNaN(d.valueOf()) && /^\d{4}-\d{2}-\d{2}T/.test(s);
|
|
45
|
+
}
|
|
46
|
+
async function main() {
|
|
47
|
+
const files = await getJsonFiles(codexDir);
|
|
48
|
+
const issues = [];
|
|
49
|
+
const entriesById = new Map();
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = await fs.readFile(file, 'utf8');
|
|
53
|
+
const json = JSON.parse(raw);
|
|
54
|
+
const parsed = codexSchema.safeParse(json);
|
|
55
|
+
if (!parsed.success) {
|
|
56
|
+
issues.push({
|
|
57
|
+
type: 'error',
|
|
58
|
+
file,
|
|
59
|
+
msg: 'Schema validation failed: ' +
|
|
60
|
+
parsed.error.issues.map(i => i.message).join('; '),
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const e = parsed.data;
|
|
65
|
+
// Duplicate ID check
|
|
66
|
+
if (entriesById.has(e.id)) {
|
|
67
|
+
const prev = entriesById.get(e.id);
|
|
68
|
+
issues.push({
|
|
69
|
+
type: 'error',
|
|
70
|
+
file,
|
|
71
|
+
msg: `Duplicate id "${e.id}" also used in ${prev.file}`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
entriesById.set(e.id, { file, entry: e });
|
|
76
|
+
}
|
|
77
|
+
// ISO date sanity (redundant but explicit)
|
|
78
|
+
if (!isIsoDate(e.updated)) {
|
|
79
|
+
issues.push({
|
|
80
|
+
type: 'error',
|
|
81
|
+
file,
|
|
82
|
+
msg: `updated is not a valid ISO date: "${e.updated}"`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
88
|
+
issues.push({ type: 'error', file, msg: `Read/parse failed: ${msg}` });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Related reference checks (warn if dangling)
|
|
92
|
+
for (const { file, entry } of entriesById.values()) {
|
|
93
|
+
for (const rel of entry.related ?? []) {
|
|
94
|
+
if (!entriesById.has(rel)) {
|
|
95
|
+
issues.push({
|
|
96
|
+
type: 'warn',
|
|
97
|
+
file,
|
|
98
|
+
msg: `related references missing id "${rel}"`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Output
|
|
104
|
+
const errors = issues.filter(i => i.type === 'error');
|
|
105
|
+
const warns = issues.filter(i => i.type === 'warn');
|
|
106
|
+
for (const i of issues) {
|
|
107
|
+
const tag = i.type === 'error' ? '❌' : '⚠️';
|
|
108
|
+
console.log(`${tag} ${i.type.toUpperCase()} in ${i.file}: ${i.msg}`);
|
|
109
|
+
}
|
|
110
|
+
console.log(`\nCodex check complete: ${entriesById.size} entries, ${errors.length} error(s), ${warns.length} warning(s).`);
|
|
111
|
+
if (errors.length > 0)
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
main().catch(err => {
|
|
115
|
+
console.error(err);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
});
|
package/dist/cli.d.ts
ADDED