@plures/praxis 1.2.0 → 1.2.11

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 (63) hide show
  1. package/README.md +93 -96
  2. package/dist/browser/{adapter-TM4IS5KT.js → adapter-CIMBGDC7.js} +5 -3
  3. package/dist/browser/{chunk-LE2ZJYFC.js → chunk-K377RW4V.js} +76 -0
  4. package/dist/{node/chunk-JQ64KMLN.js → browser/chunk-MBVHLOU2.js} +12 -1
  5. package/dist/browser/index.d.ts +32 -5
  6. package/dist/browser/index.js +15 -7
  7. package/dist/browser/integrations/svelte.d.ts +2 -2
  8. package/dist/browser/integrations/svelte.js +1 -1
  9. package/dist/browser/{reactive-engine.svelte-C9OpcTHf.d.ts → reactive-engine.svelte-9aS0kTa8.d.ts} +136 -1
  10. package/dist/node/{adapter-K6DOX6XS.js → adapter-75ISSMWD.js} +5 -3
  11. package/dist/node/chunk-5RH7UAQC.js +486 -0
  12. package/dist/{browser/chunk-JQ64KMLN.js → node/chunk-MBVHLOU2.js} +12 -1
  13. package/dist/node/{chunk-LE2ZJYFC.js → chunk-PRPQO6R5.js} +3 -72
  14. package/dist/node/chunk-R2PSBPKQ.js +150 -0
  15. package/dist/node/chunk-WZ6B3LZ6.js +638 -0
  16. package/dist/node/cli/index.cjs +2316 -832
  17. package/dist/node/cli/index.js +18 -0
  18. package/dist/node/components/index.d.cts +3 -2
  19. package/dist/node/components/index.d.ts +3 -2
  20. package/dist/node/index.cjs +620 -38
  21. package/dist/node/index.d.cts +259 -5
  22. package/dist/node/index.d.ts +259 -5
  23. package/dist/node/index.js +55 -65
  24. package/dist/node/integrations/svelte.cjs +76 -0
  25. package/dist/node/integrations/svelte.d.cts +2 -2
  26. package/dist/node/integrations/svelte.d.ts +2 -2
  27. package/dist/node/integrations/svelte.js +2 -1
  28. package/dist/node/{reactive-engine.svelte-1M4m_C_v.d.cts → reactive-engine.svelte-BFIZfawz.d.cts} +199 -1
  29. package/dist/node/{reactive-engine.svelte-ChNFn4Hj.d.ts → reactive-engine.svelte-CRNqHlbv.d.ts} +199 -1
  30. package/dist/node/reverse-W7THPV45.js +193 -0
  31. package/dist/node/{terminal-adapter-CWka-yL8.d.ts → terminal-adapter-B-UK_Vdz.d.ts} +28 -3
  32. package/dist/node/{terminal-adapter-CDzxoLKR.d.cts → terminal-adapter-BQSIF5bf.d.cts} +28 -3
  33. package/dist/node/validate-CNHUULQE.js +180 -0
  34. package/docs/core/pluresdb-integration.md +15 -15
  35. package/docs/decision-ledger/BEHAVIOR_LEDGER.md +225 -0
  36. package/docs/decision-ledger/DecisionLedger.tla +180 -0
  37. package/docs/decision-ledger/IMPLEMENTATION_SUMMARY.md +217 -0
  38. package/docs/decision-ledger/LATEST.md +166 -0
  39. package/docs/guides/cicd-pipeline.md +142 -0
  40. package/package.json +2 -2
  41. package/src/__tests__/cli-validate.test.ts +197 -0
  42. package/src/__tests__/decision-ledger.test.ts +485 -0
  43. package/src/__tests__/reverse-generator.test.ts +189 -0
  44. package/src/__tests__/scanner.test.ts +215 -0
  45. package/src/cli/commands/reverse.ts +289 -0
  46. package/src/cli/commands/validate.ts +264 -0
  47. package/src/cli/index.ts +47 -0
  48. package/src/core/pluresdb/adapter.ts +45 -2
  49. package/src/core/rules.ts +133 -0
  50. package/src/decision-ledger/README.md +400 -0
  51. package/src/decision-ledger/REVERSE_ENGINEERING.md +484 -0
  52. package/src/decision-ledger/facts-events.ts +121 -0
  53. package/src/decision-ledger/index.ts +70 -0
  54. package/src/decision-ledger/ledger.ts +246 -0
  55. package/src/decision-ledger/logic-ledger.ts +158 -0
  56. package/src/decision-ledger/reverse-generator.ts +426 -0
  57. package/src/decision-ledger/scanner.ts +506 -0
  58. package/src/decision-ledger/types.ts +247 -0
  59. package/src/decision-ledger/validation.ts +336 -0
  60. package/src/dsl/index.ts +13 -2
  61. package/src/index.browser.ts +2 -0
  62. package/src/index.ts +36 -0
  63. 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
+ }