@plures/praxis 1.2.0 → 1.2.10
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/README.md +10 -96
- package/dist/browser/{adapter-TM4IS5KT.js → adapter-CIMBGDC7.js} +5 -3
- package/dist/browser/{chunk-LE2ZJYFC.js → chunk-K377RW4V.js} +76 -0
- package/dist/{node/chunk-JQ64KMLN.js → browser/chunk-MBVHLOU2.js} +12 -1
- package/dist/browser/index.d.ts +32 -5
- package/dist/browser/index.js +15 -7
- package/dist/browser/integrations/svelte.d.ts +2 -2
- package/dist/browser/integrations/svelte.js +1 -1
- package/dist/browser/{reactive-engine.svelte-C9OpcTHf.d.ts → reactive-engine.svelte-9aS0kTa8.d.ts} +136 -1
- package/dist/node/{adapter-K6DOX6XS.js → adapter-75ISSMWD.js} +5 -3
- package/dist/node/chunk-5RH7UAQC.js +486 -0
- package/dist/{browser/chunk-JQ64KMLN.js → node/chunk-MBVHLOU2.js} +12 -1
- package/dist/node/{chunk-LE2ZJYFC.js → chunk-PRPQO6R5.js} +3 -72
- package/dist/node/chunk-R2PSBPKQ.js +150 -0
- package/dist/node/chunk-WZ6B3LZ6.js +638 -0
- package/dist/node/cli/index.cjs +2316 -832
- package/dist/node/cli/index.js +18 -0
- package/dist/node/components/index.d.cts +3 -2
- package/dist/node/components/index.d.ts +3 -2
- package/dist/node/index.cjs +620 -38
- package/dist/node/index.d.cts +259 -5
- package/dist/node/index.d.ts +259 -5
- package/dist/node/index.js +55 -65
- package/dist/node/integrations/svelte.cjs +76 -0
- package/dist/node/integrations/svelte.d.cts +2 -2
- package/dist/node/integrations/svelte.d.ts +2 -2
- package/dist/node/integrations/svelte.js +2 -1
- package/dist/node/{reactive-engine.svelte-1M4m_C_v.d.cts → reactive-engine.svelte-BFIZfawz.d.cts} +199 -1
- package/dist/node/{reactive-engine.svelte-ChNFn4Hj.d.ts → reactive-engine.svelte-CRNqHlbv.d.ts} +199 -1
- package/dist/node/reverse-W7THPV45.js +193 -0
- package/dist/node/{terminal-adapter-CWka-yL8.d.ts → terminal-adapter-B-UK_Vdz.d.ts} +28 -3
- package/dist/node/{terminal-adapter-CDzxoLKR.d.cts → terminal-adapter-BQSIF5bf.d.cts} +28 -3
- package/dist/node/validate-CNHUULQE.js +180 -0
- package/docs/core/pluresdb-integration.md +15 -15
- package/docs/decision-ledger/BEHAVIOR_LEDGER.md +225 -0
- package/docs/decision-ledger/DecisionLedger.tla +180 -0
- package/docs/decision-ledger/IMPLEMENTATION_SUMMARY.md +217 -0
- package/docs/decision-ledger/LATEST.md +166 -0
- package/docs/guides/cicd-pipeline.md +142 -0
- package/package.json +2 -2
- package/src/__tests__/cli-validate.test.ts +197 -0
- package/src/__tests__/decision-ledger.test.ts +485 -0
- package/src/__tests__/reverse-generator.test.ts +189 -0
- package/src/__tests__/scanner.test.ts +215 -0
- package/src/cli/commands/reverse.ts +289 -0
- package/src/cli/commands/validate.ts +264 -0
- package/src/cli/index.ts +47 -0
- package/src/core/pluresdb/adapter.ts +45 -2
- package/src/core/rules.ts +133 -0
- package/src/decision-ledger/README.md +400 -0
- package/src/decision-ledger/REVERSE_ENGINEERING.md +484 -0
- package/src/decision-ledger/facts-events.ts +121 -0
- package/src/decision-ledger/index.ts +70 -0
- package/src/decision-ledger/ledger.ts +246 -0
- package/src/decision-ledger/logic-ledger.ts +158 -0
- package/src/decision-ledger/reverse-generator.ts +426 -0
- package/src/decision-ledger/scanner.ts +506 -0
- package/src/decision-ledger/types.ts +247 -0
- package/src/decision-ledger/validation.ts +336 -0
- package/src/dsl/index.ts +13 -2
- package/src/index.browser.ts +2 -0
- package/src/index.ts +36 -0
- package/src/integrations/pluresdb.ts +14 -2
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Ledger - Repository Scanner
|
|
3
|
+
*
|
|
4
|
+
* Scans repositories to discover existing rules, constraints, and related artifacts
|
|
5
|
+
* for reverse engineering contracts from existing codebases.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { promises as fs } from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import type { RuleDescriptor, ConstraintDescriptor } from '../core/rules.js';
|
|
11
|
+
import type { Contract } from './types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for repository scanning.
|
|
15
|
+
*/
|
|
16
|
+
export interface ScanOptions {
|
|
17
|
+
/** Root directory to scan */
|
|
18
|
+
rootDir: string;
|
|
19
|
+
/** File patterns to include (glob patterns) */
|
|
20
|
+
include?: string[];
|
|
21
|
+
/** File patterns to exclude (glob patterns) */
|
|
22
|
+
exclude?: string[];
|
|
23
|
+
/** Whether to scan for test files */
|
|
24
|
+
scanTests?: boolean;
|
|
25
|
+
/** Whether to scan for spec files */
|
|
26
|
+
scanSpecs?: boolean;
|
|
27
|
+
/** Maximum depth for directory traversal */
|
|
28
|
+
maxDepth?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Result of repository scanning.
|
|
33
|
+
*/
|
|
34
|
+
export interface ScanResult {
|
|
35
|
+
/** Discovered rules */
|
|
36
|
+
rules: RuleDescriptor[];
|
|
37
|
+
/** Discovered constraints */
|
|
38
|
+
constraints: ConstraintDescriptor[];
|
|
39
|
+
/** Mapping of rule IDs to test file paths */
|
|
40
|
+
testFiles: Map<string, string[]>;
|
|
41
|
+
/** Mapping of rule IDs to spec file paths */
|
|
42
|
+
specFiles: Map<string, string[]>;
|
|
43
|
+
/** Warnings encountered during scanning */
|
|
44
|
+
warnings: string[];
|
|
45
|
+
/** Total files scanned */
|
|
46
|
+
filesScanned: number;
|
|
47
|
+
/** Scan duration in milliseconds */
|
|
48
|
+
duration: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Discovered artifact information.
|
|
53
|
+
*/
|
|
54
|
+
export interface DiscoveredArtifact {
|
|
55
|
+
/** Rule or constraint ID */
|
|
56
|
+
ruleId: string;
|
|
57
|
+
/** Type of artifact */
|
|
58
|
+
type: 'test' | 'spec' | 'implementation';
|
|
59
|
+
/** File path */
|
|
60
|
+
filePath: string;
|
|
61
|
+
/** Line number where artifact is defined */
|
|
62
|
+
line?: number;
|
|
63
|
+
/** Inferred description */
|
|
64
|
+
description?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Scan a repository for existing rules and constraints.
|
|
69
|
+
*
|
|
70
|
+
* @param options Scan options
|
|
71
|
+
* @returns Scan result
|
|
72
|
+
*/
|
|
73
|
+
export async function scanRepository(
|
|
74
|
+
options: ScanOptions
|
|
75
|
+
): Promise<ScanResult> {
|
|
76
|
+
const startTime = Date.now();
|
|
77
|
+
const { rootDir, scanTests = true, scanSpecs = true, maxDepth = 10 } = options;
|
|
78
|
+
|
|
79
|
+
// Validate and normalize root directory
|
|
80
|
+
const normalizedRoot = path.resolve(rootDir);
|
|
81
|
+
|
|
82
|
+
// Check if directory exists
|
|
83
|
+
try {
|
|
84
|
+
const stats = await fs.stat(normalizedRoot);
|
|
85
|
+
if (!stats.isDirectory()) {
|
|
86
|
+
throw new Error(`Path is not a directory: ${normalizedRoot}`);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error(`Invalid root directory: ${normalizedRoot} - ${error instanceof Error ? error.message : String(error)}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const rules: RuleDescriptor[] = [];
|
|
93
|
+
const constraints: ConstraintDescriptor[] = [];
|
|
94
|
+
const testFiles = new Map<string, string[]>();
|
|
95
|
+
const specFiles = new Map<string, string[]>();
|
|
96
|
+
const scanWarnings: string[] = [];
|
|
97
|
+
let filesScanned = 0;
|
|
98
|
+
|
|
99
|
+
// Scan for implementation files
|
|
100
|
+
const implFiles = await findFiles(
|
|
101
|
+
normalizedRoot,
|
|
102
|
+
{
|
|
103
|
+
include: options.include || ['**/*.ts', '**/*.js'],
|
|
104
|
+
exclude: options.exclude || [
|
|
105
|
+
'**/node_modules/**',
|
|
106
|
+
'**/dist/**',
|
|
107
|
+
'**/build/**',
|
|
108
|
+
'**/*.test.ts',
|
|
109
|
+
'**/*.test.js',
|
|
110
|
+
'**/*.spec.ts',
|
|
111
|
+
'**/*.spec.js',
|
|
112
|
+
],
|
|
113
|
+
maxDepth,
|
|
114
|
+
},
|
|
115
|
+
scanWarnings
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
for (const file of implFiles) {
|
|
119
|
+
filesScanned++;
|
|
120
|
+
|
|
121
|
+
// Check file size to avoid memory issues with large files
|
|
122
|
+
const stats = await fs.stat(file);
|
|
123
|
+
const maxFileSize = 10 * 1024 * 1024; // 10 MB limit
|
|
124
|
+
|
|
125
|
+
if (stats.size > maxFileSize) {
|
|
126
|
+
scanWarnings.push(`Skipping large file (${(stats.size / 1024 / 1024).toFixed(2)} MB): ${file}`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
131
|
+
|
|
132
|
+
// Look for defineRule and defineConstraint patterns
|
|
133
|
+
const discoveredRules = await extractRulesFromFile(file, content);
|
|
134
|
+
const discoveredConstraints = await extractConstraintsFromFile(file, content);
|
|
135
|
+
|
|
136
|
+
rules.push(...discoveredRules);
|
|
137
|
+
constraints.push(...discoveredConstraints);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Scan for test files if requested
|
|
141
|
+
if (scanTests) {
|
|
142
|
+
const testFileList = await findFiles(
|
|
143
|
+
normalizedRoot,
|
|
144
|
+
{
|
|
145
|
+
include: ['**/*.test.ts', '**/*.test.js', '**/*.spec.ts', '**/*.spec.js'],
|
|
146
|
+
exclude: ['**/node_modules/**', '**/dist/**', '**/build/**'],
|
|
147
|
+
maxDepth,
|
|
148
|
+
},
|
|
149
|
+
scanWarnings
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
for (const testFile of testFileList) {
|
|
153
|
+
filesScanned++;
|
|
154
|
+
|
|
155
|
+
// Check file size to avoid memory issues
|
|
156
|
+
const stats = await fs.stat(testFile);
|
|
157
|
+
const maxFileSize = 10 * 1024 * 1024; // 10 MB limit
|
|
158
|
+
|
|
159
|
+
if (stats.size > maxFileSize) {
|
|
160
|
+
scanWarnings.push(`Skipping large test file (${(stats.size / 1024 / 1024).toFixed(2)} MB): ${testFile}`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const content = await fs.readFile(testFile, 'utf-8');
|
|
165
|
+
const mappings = await mapTestsToRules(testFile, content, rules);
|
|
166
|
+
|
|
167
|
+
for (const [ruleId, filePath] of mappings) {
|
|
168
|
+
if (!testFiles.has(ruleId)) {
|
|
169
|
+
testFiles.set(ruleId, []);
|
|
170
|
+
}
|
|
171
|
+
testFiles.get(ruleId)!.push(filePath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Scan for spec files if requested
|
|
177
|
+
if (scanSpecs) {
|
|
178
|
+
const specFileList = await findFiles(
|
|
179
|
+
normalizedRoot,
|
|
180
|
+
{
|
|
181
|
+
include: ['**/*.tla', '**/*.md', '**/spec/**/*.ts'],
|
|
182
|
+
exclude: ['**/node_modules/**', '**/dist/**', '**/build/**'],
|
|
183
|
+
maxDepth,
|
|
184
|
+
},
|
|
185
|
+
scanWarnings
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
for (const specFile of specFileList) {
|
|
189
|
+
filesScanned++;
|
|
190
|
+
|
|
191
|
+
// Check file size to avoid memory issues
|
|
192
|
+
const stats = await fs.stat(specFile);
|
|
193
|
+
const maxFileSize = 10 * 1024 * 1024; // 10 MB limit
|
|
194
|
+
|
|
195
|
+
if (stats.size > maxFileSize) {
|
|
196
|
+
scanWarnings.push(`Skipping large spec file (${(stats.size / 1024 / 1024).toFixed(2)} MB): ${specFile}`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const content = await fs.readFile(specFile, 'utf-8');
|
|
201
|
+
const mappings = await mapSpecsToRules(specFile, content, rules);
|
|
202
|
+
|
|
203
|
+
for (const [ruleId, filePath] of mappings) {
|
|
204
|
+
if (!specFiles.has(ruleId)) {
|
|
205
|
+
specFiles.set(ruleId, []);
|
|
206
|
+
}
|
|
207
|
+
specFiles.get(ruleId)!.push(filePath);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const duration = Date.now() - startTime;
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
rules,
|
|
216
|
+
constraints,
|
|
217
|
+
testFiles,
|
|
218
|
+
specFiles,
|
|
219
|
+
warnings: scanWarnings,
|
|
220
|
+
filesScanned,
|
|
221
|
+
duration,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Extract rules from a file's content.
|
|
227
|
+
*
|
|
228
|
+
* NOTE: This uses a simple regex-based approach with known limitations:
|
|
229
|
+
* - Will not correctly parse defineRule calls with nested objects or functions
|
|
230
|
+
* containing closing braces (e.g., `impl: (x) => { return []; }`)
|
|
231
|
+
* - For production use, consider upgrading to AST-based parsing using
|
|
232
|
+
* @babel/parser or TypeScript compiler API for more robust code analysis
|
|
233
|
+
*/
|
|
234
|
+
async function extractRulesFromFile(
|
|
235
|
+
filePath: string,
|
|
236
|
+
content: string
|
|
237
|
+
): Promise<RuleDescriptor[]> {
|
|
238
|
+
const rules: RuleDescriptor[] = [];
|
|
239
|
+
|
|
240
|
+
// Pattern to match defineRule calls
|
|
241
|
+
// LIMITATION: This simple pattern fails with nested braces
|
|
242
|
+
const defineRulePattern = /defineRule\s*\(\s*\{([^}]+)\}\s*\)/g;
|
|
243
|
+
let match;
|
|
244
|
+
|
|
245
|
+
while ((match = defineRulePattern.exec(content)) !== null) {
|
|
246
|
+
const ruleConfig = match[1];
|
|
247
|
+
|
|
248
|
+
// Extract id and description
|
|
249
|
+
const idMatch = /id:\s*['"]([^'"]+)['"]/.exec(ruleConfig);
|
|
250
|
+
const descMatch = /description:\s*['"]([^'"]+)['"]/.exec(ruleConfig);
|
|
251
|
+
|
|
252
|
+
if (idMatch) {
|
|
253
|
+
const id = idMatch[1];
|
|
254
|
+
const description = descMatch ? descMatch[1] : '';
|
|
255
|
+
|
|
256
|
+
rules.push({
|
|
257
|
+
id,
|
|
258
|
+
description,
|
|
259
|
+
impl: () => [], // Placeholder
|
|
260
|
+
meta: {
|
|
261
|
+
sourceFile: filePath,
|
|
262
|
+
discovered: true,
|
|
263
|
+
},
|
|
264
|
+
} as RuleDescriptor);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return rules;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Extract constraints from a file's content.
|
|
273
|
+
*
|
|
274
|
+
* NOTE: This uses a simple regex-based approach with known limitations:
|
|
275
|
+
* - Will not correctly parse defineConstraint calls with nested objects or functions
|
|
276
|
+
* containing closing braces (e.g., `impl: (state) => { return true; }`)
|
|
277
|
+
* - For production use, consider upgrading to AST-based parsing using
|
|
278
|
+
* @babel/parser or TypeScript compiler API for more robust code analysis
|
|
279
|
+
*/
|
|
280
|
+
async function extractConstraintsFromFile(
|
|
281
|
+
filePath: string,
|
|
282
|
+
content: string
|
|
283
|
+
): Promise<ConstraintDescriptor[]> {
|
|
284
|
+
const constraints: ConstraintDescriptor[] = [];
|
|
285
|
+
|
|
286
|
+
// Pattern to match defineConstraint calls
|
|
287
|
+
// LIMITATION: This simple pattern fails with nested braces
|
|
288
|
+
const defineConstraintPattern = /defineConstraint\s*\(\s*\{([^}]+)\}\s*\)/g;
|
|
289
|
+
let match;
|
|
290
|
+
|
|
291
|
+
while ((match = defineConstraintPattern.exec(content)) !== null) {
|
|
292
|
+
const constraintConfig = match[1];
|
|
293
|
+
|
|
294
|
+
// Extract id and description
|
|
295
|
+
const idMatch = /id:\s*['"]([^'"]+)['"]/.exec(constraintConfig);
|
|
296
|
+
const descMatch = /description:\s*['"]([^'"]+)['"]/.exec(constraintConfig);
|
|
297
|
+
|
|
298
|
+
if (idMatch) {
|
|
299
|
+
const id = idMatch[1];
|
|
300
|
+
const description = descMatch ? descMatch[1] : '';
|
|
301
|
+
|
|
302
|
+
constraints.push({
|
|
303
|
+
id,
|
|
304
|
+
description,
|
|
305
|
+
impl: () => true, // Placeholder
|
|
306
|
+
meta: {
|
|
307
|
+
sourceFile: filePath,
|
|
308
|
+
discovered: true,
|
|
309
|
+
},
|
|
310
|
+
} as ConstraintDescriptor);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return constraints;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Map test files to rule IDs.
|
|
319
|
+
*/
|
|
320
|
+
async function mapTestsToRules(
|
|
321
|
+
testFile: string,
|
|
322
|
+
content: string,
|
|
323
|
+
rules: RuleDescriptor[]
|
|
324
|
+
): Promise<Map<string, string>> {
|
|
325
|
+
const mappings = new Map<string, string>();
|
|
326
|
+
|
|
327
|
+
// Look for rule IDs mentioned in test descriptions or imports
|
|
328
|
+
for (const rule of rules) {
|
|
329
|
+
if (content.includes(rule.id)) {
|
|
330
|
+
mappings.set(rule.id, testFile);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return mappings;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Map spec files to rule IDs.
|
|
339
|
+
*/
|
|
340
|
+
async function mapSpecsToRules(
|
|
341
|
+
specFile: string,
|
|
342
|
+
content: string,
|
|
343
|
+
rules: RuleDescriptor[]
|
|
344
|
+
): Promise<Map<string, string>> {
|
|
345
|
+
const mappings = new Map<string, string>();
|
|
346
|
+
|
|
347
|
+
// Look for rule IDs mentioned in spec files
|
|
348
|
+
for (const rule of rules) {
|
|
349
|
+
if (content.includes(rule.id)) {
|
|
350
|
+
mappings.set(rule.id, specFile);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return mappings;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Find files matching patterns in a directory.
|
|
359
|
+
*/
|
|
360
|
+
async function findFiles(
|
|
361
|
+
rootDir: string,
|
|
362
|
+
options: {
|
|
363
|
+
include: string[];
|
|
364
|
+
exclude: string[];
|
|
365
|
+
maxDepth: number;
|
|
366
|
+
},
|
|
367
|
+
warnings: string[]
|
|
368
|
+
): Promise<string[]> {
|
|
369
|
+
const files: string[] = [];
|
|
370
|
+
|
|
371
|
+
async function walk(dir: string, depth: number): Promise<void> {
|
|
372
|
+
if (depth > options.maxDepth) {
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
378
|
+
|
|
379
|
+
for (const entry of entries) {
|
|
380
|
+
const fullPath = path.join(dir, entry.name);
|
|
381
|
+
const relativePath = path.relative(rootDir, fullPath);
|
|
382
|
+
|
|
383
|
+
// Check exclude patterns
|
|
384
|
+
if (shouldExclude(relativePath, options.exclude)) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (entry.isDirectory()) {
|
|
389
|
+
await walk(fullPath, depth + 1);
|
|
390
|
+
} else if (entry.isFile()) {
|
|
391
|
+
// Check include patterns
|
|
392
|
+
if (shouldInclude(relativePath, options.include)) {
|
|
393
|
+
files.push(fullPath);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
// Log permission errors but continue scanning
|
|
399
|
+
if (error instanceof Error && 'code' in error) {
|
|
400
|
+
const nodeError = error as NodeJS.ErrnoException;
|
|
401
|
+
if (nodeError.code === 'EACCES' || nodeError.code === 'EPERM') {
|
|
402
|
+
warnings.push(`Permission denied: ${dir}`);
|
|
403
|
+
} else {
|
|
404
|
+
warnings.push(`Error scanning ${dir}: ${error.message}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
await walk(rootDir, 0);
|
|
411
|
+
return files;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Check if a path should be excluded.
|
|
416
|
+
*/
|
|
417
|
+
function shouldExclude(relativePath: string, patterns: string[]): boolean {
|
|
418
|
+
return patterns.some((pattern) => {
|
|
419
|
+
const regex = globToRegex(pattern);
|
|
420
|
+
return regex.test(relativePath);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Check if a path should be included.
|
|
426
|
+
*/
|
|
427
|
+
function shouldInclude(relativePath: string, patterns: string[]): boolean {
|
|
428
|
+
return patterns.some((pattern) => {
|
|
429
|
+
const regex = globToRegex(pattern);
|
|
430
|
+
return regex.test(relativePath);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Convert a simple glob pattern to a regex.
|
|
436
|
+
* Supports: *, **, ?, and basic path matching.
|
|
437
|
+
*/
|
|
438
|
+
function globToRegex(pattern: string): RegExp {
|
|
439
|
+
let regexPattern = pattern
|
|
440
|
+
.replace(/\\/g, '\\\\')
|
|
441
|
+
.replace(/\./g, '\\.')
|
|
442
|
+
.replace(/\*\*/g, '___DOUBLESTAR___')
|
|
443
|
+
.replace(/\*/g, '[^/]*')
|
|
444
|
+
.replace(/___DOUBLESTAR___/g, '.*')
|
|
445
|
+
.replace(/\?/g, '.');
|
|
446
|
+
|
|
447
|
+
return new RegExp(`^${regexPattern}$`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Analyze a file to infer contract information.
|
|
452
|
+
*/
|
|
453
|
+
export async function inferContractFromFile(
|
|
454
|
+
filePath: string,
|
|
455
|
+
ruleId: string
|
|
456
|
+
): Promise<Partial<Contract>> {
|
|
457
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
458
|
+
|
|
459
|
+
// Basic inference from code comments and structure
|
|
460
|
+
const behavior = inferBehavior(content, ruleId);
|
|
461
|
+
const invariants = inferInvariants(content);
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
ruleId,
|
|
465
|
+
behavior,
|
|
466
|
+
invariants,
|
|
467
|
+
examples: [], // Populated separately from tests
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Infer behavior description from code.
|
|
473
|
+
*/
|
|
474
|
+
function inferBehavior(content: string, ruleId: string): string {
|
|
475
|
+
// Look for JSDoc comments or description strings
|
|
476
|
+
const jsdocMatch = /\/\*\*\s*\n\s*\*\s*([^\n]+)/.exec(content);
|
|
477
|
+
if (jsdocMatch) {
|
|
478
|
+
return jsdocMatch[1].trim();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Look for description in defineRule
|
|
482
|
+
const descMatch = /description:\s*['"]([^'"]+)['"]/.exec(content);
|
|
483
|
+
if (descMatch) {
|
|
484
|
+
return descMatch[1];
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Fallback: use rule ID as basis
|
|
488
|
+
return `Process ${ruleId} events`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Infer invariants from code.
|
|
493
|
+
*/
|
|
494
|
+
function inferInvariants(content: string): string[] {
|
|
495
|
+
const invariants: string[] = [];
|
|
496
|
+
|
|
497
|
+
// Look for assertions or validation checks
|
|
498
|
+
const assertPattern = /assert\s*\([^)]+,\s*['"]([^'"]+)['"]\)/g;
|
|
499
|
+
let match;
|
|
500
|
+
|
|
501
|
+
while ((match = assertPattern.exec(content)) !== null) {
|
|
502
|
+
invariants.push(match[1]);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return invariants;
|
|
506
|
+
}
|