@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.
Files changed (63) hide show
  1. package/README.md +10 -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,264 @@
1
+ /**
2
+ * Praxis CLI - Validate Command
3
+ *
4
+ * Validates contract coverage for rules and constraints in the registry.
5
+ */
6
+
7
+ import { PraxisRegistry } from '../../core/rules.js';
8
+ import {
9
+ validateContracts,
10
+ formatValidationReport,
11
+ formatValidationReportJSON,
12
+ formatValidationReportSARIF,
13
+ writeLogicLedgerEntry,
14
+ type ArtifactIndex,
15
+ type Contract,
16
+ } from '../../decision-ledger/index.js';
17
+ import { ContractMissing } from '../../decision-ledger/index.js';
18
+ import type { PraxisEvent, PraxisFact } from '../../core/protocol.js';
19
+ import type { ContractGap } from '../../decision-ledger/types.js';
20
+
21
+ interface ValidateOptions {
22
+ output?: 'console' | 'json' | 'sarif';
23
+ strict?: boolean;
24
+ registry?: string;
25
+ tests?: boolean;
26
+ spec?: boolean;
27
+ emitFacts?: boolean;
28
+ gapOutput?: string;
29
+ ledger?: string;
30
+ author?: string;
31
+ }
32
+
33
+ /**
34
+ * Validate command implementation.
35
+ *
36
+ * @param options Command options
37
+ */
38
+ export async function validateCommand(options: ValidateOptions): Promise<void> {
39
+ const outputFormat = options.output || 'console';
40
+ const strict = options.strict || false;
41
+
42
+ // In a real implementation, this would load the registry from the project
43
+ // For now, we'll create a demo registry to show how it works
44
+ const registry = await loadRegistry(options.registry);
45
+ const artifactIndex = await buildArtifactIndex(registry, {
46
+ includeTests: options.tests ?? true,
47
+ includeSpec: options.spec ?? true,
48
+ });
49
+
50
+ // Validate contracts
51
+ const report = validateContracts(registry, {
52
+ strict,
53
+ requiredFields: ['behavior', 'examples', 'invariants'],
54
+ missingSeverity: strict ? 'error' : 'warning',
55
+ artifactIndex,
56
+ });
57
+
58
+ if (options.emitFacts) {
59
+ const facts = gapsToFacts(report.incomplete);
60
+ const events = gapsToEvents(report.incomplete);
61
+ await emitGapArtifacts({ facts, events, gapOutput: options.gapOutput });
62
+ }
63
+
64
+ if (options.ledger) {
65
+ await writeLedgerSnapshots(registry, {
66
+ rootDir: options.ledger,
67
+ author: options.author ?? 'system',
68
+ artifactIndex,
69
+ });
70
+ }
71
+
72
+ // Format and output the report
73
+ switch (outputFormat) {
74
+ case 'json':
75
+ console.log(formatValidationReportJSON(report));
76
+ break;
77
+ case 'sarif':
78
+ console.log(formatValidationReportSARIF(report));
79
+ break;
80
+ case 'console':
81
+ default:
82
+ console.log(formatValidationReport(report));
83
+ break;
84
+ }
85
+
86
+ // Exit with error code if in strict mode and there are issues
87
+ if (strict && (report.incomplete.length > 0 || report.missing.length > 0)) {
88
+ // Count errors from incomplete contracts
89
+ const incompleteErrors = report.incomplete.filter((gap: ContractGap) => gap.severity === 'error').length;
90
+ // In strict mode, missing contracts are also errors
91
+ const totalErrors = incompleteErrors + report.missing.length;
92
+
93
+ if (totalErrors > 0) {
94
+ console.error(`\n❌ Validation failed: ${totalErrors} error(s) found`);
95
+ process.exit(1);
96
+ }
97
+ }
98
+
99
+ // Exit with success (only show messages in console mode)
100
+ if (outputFormat === 'console') {
101
+ if (report.incomplete.length === 0 && report.missing.length === 0) {
102
+ console.log('\n✅ All contracts validated successfully!');
103
+ } else {
104
+ const warningCount = report.incomplete.filter((gap: ContractGap) => gap.severity === 'warning').length;
105
+ if (warningCount > 0) {
106
+ console.log(`\n⚠️ ${warningCount} warning(s) found`);
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Load the registry from the project.
114
+ *
115
+ * In a real implementation, this would:
116
+ * 1. Load the project's Praxis configuration
117
+ * 2. Import and instantiate the registry
118
+ * 3. Load all registered rules and constraints
119
+ *
120
+ * For now, it returns an empty registry for demonstration.
121
+ *
122
+ * @param registryPath Optional path to registry module
123
+ * @returns The loaded registry
124
+ */
125
+ async function loadRegistry(registryPath?: string): Promise<PraxisRegistry> {
126
+ const registry = new PraxisRegistry();
127
+
128
+ // If a registry path is provided, try to load it
129
+ if (registryPath) {
130
+ try {
131
+ // Dynamic import would happen here
132
+ // const module = await import(registryPath);
133
+ // return module.registry || module.default;
134
+ const module = await import(resolveRegistryPath(registryPath));
135
+ const candidate = module.registry ?? module.default ?? module.createRegistry?.();
136
+ if (candidate && candidate instanceof PraxisRegistry) {
137
+ return candidate;
138
+ }
139
+ throw new Error('Registry module did not export a PraxisRegistry instance');
140
+ } catch (error) {
141
+ console.warn(`Warning: Could not load registry from ${registryPath}:`, error);
142
+ }
143
+ }
144
+
145
+ // Return empty registry for now
146
+ // In practice, this would scan the project for rules/constraints
147
+ return registry;
148
+ }
149
+
150
+ function resolveRegistryPath(registryPath: string): string {
151
+ if (registryPath.startsWith('.') || registryPath.startsWith('/')) {
152
+ return new URL(registryPath, `file://${process.cwd()}/`).href;
153
+ }
154
+
155
+ return registryPath;
156
+ }
157
+
158
+ async function buildArtifactIndex(
159
+ registry: PraxisRegistry,
160
+ options: { includeTests: boolean; includeSpec: boolean }
161
+ ): Promise<ArtifactIndex> {
162
+ const index: ArtifactIndex = {};
163
+ const ruleIds = new Set(registry.getRuleIds().concat(registry.getConstraintIds()));
164
+
165
+ if (options.includeTests) {
166
+ index.tests = new Set();
167
+ for (const id of ruleIds) {
168
+ if (await hasArtifactFile('tests', id)) {
169
+ index.tests.add(id);
170
+ }
171
+ }
172
+ }
173
+
174
+ if (options.includeSpec) {
175
+ index.spec = new Set();
176
+ for (const id of ruleIds) {
177
+ if (await hasArtifactFile('spec', id)) {
178
+ index.spec.add(id);
179
+ }
180
+ }
181
+ }
182
+
183
+ return index;
184
+ }
185
+
186
+ async function writeLedgerSnapshots(
187
+ registry: PraxisRegistry,
188
+ options: { rootDir: string; author: string; artifactIndex?: ArtifactIndex }
189
+ ): Promise<void> {
190
+ const { rootDir, author, artifactIndex } = options;
191
+
192
+ // Process rules and constraints separately to avoid type issues
193
+ const processDescriptor = async (descriptor: { contract?: Contract; meta?: Record<string, unknown> } & { id: string }) => {
194
+ if (!descriptor.contract && !descriptor.meta?.contract) {
195
+ return;
196
+ }
197
+ const contract = descriptor.contract ?? (descriptor.meta?.contract as any);
198
+ await writeLogicLedgerEntry(contract, {
199
+ rootDir,
200
+ author,
201
+ testsPresent: artifactIndex?.tests?.has(contract.ruleId) ?? false,
202
+ specPresent: artifactIndex?.spec?.has(contract.ruleId) ?? false,
203
+ });
204
+ };
205
+
206
+ // Process all rules
207
+ for (const descriptor of registry.getAllRules()) {
208
+ await processDescriptor(descriptor);
209
+ }
210
+
211
+ // Process all constraints
212
+ for (const descriptor of registry.getAllConstraints()) {
213
+ await processDescriptor(descriptor);
214
+ }
215
+ }
216
+
217
+ async function hasArtifactFile(type: 'tests' | 'spec', ruleId: string): Promise<boolean> {
218
+ const fs = await import('node:fs/promises');
219
+ const path = await import('node:path');
220
+ const candidateDirs = type === 'tests' ? ['src/__tests__', 'tests', 'test'] : ['spec', 'specs'];
221
+ const sanitized = ruleId.replace(/[^a-zA-Z0-9_-]/g, '_');
222
+
223
+ for (const dir of candidateDirs) {
224
+ const fullDir = path.resolve(process.cwd(), dir);
225
+ try {
226
+ const entries = await fs.readdir(fullDir);
227
+ if (entries.some((file) => file.includes(sanitized))) {
228
+ return true;
229
+ }
230
+ } catch {
231
+ // ignore missing directories
232
+ }
233
+ }
234
+
235
+ return false;
236
+ }
237
+
238
+ function gapsToFacts(gaps: ContractGap[]): PraxisFact[] {
239
+ return gaps.map((gap) =>
240
+ ContractMissing.create({
241
+ ruleId: gap.ruleId,
242
+ missing: gap.missing,
243
+ severity: gap.severity,
244
+ message: gap.message,
245
+ })
246
+ );
247
+ }
248
+
249
+ function gapsToEvents(_gaps: ContractGap[]): PraxisEvent[] {
250
+ return [];
251
+ }
252
+
253
+ async function emitGapArtifacts(payload: {
254
+ facts: PraxisFact[];
255
+ events: PraxisEvent[];
256
+ gapOutput?: string;
257
+ }): Promise<void> {
258
+ if (payload.gapOutput) {
259
+ const fs = await import('node:fs/promises');
260
+ await fs.writeFile(payload.gapOutput, JSON.stringify(payload, null, 2));
261
+ } else {
262
+ console.log(JSON.stringify(payload, null, 2));
263
+ }
264
+ }
package/src/cli/index.ts CHANGED
@@ -254,4 +254,51 @@ program
254
254
  }
255
255
  });
256
256
 
257
+ // Validate command (Decision Ledger)
258
+ program
259
+ .command('validate')
260
+ .description('Validate contract coverage for rules and constraints')
261
+ .option('--output <format>', 'Output format (console, json, sarif)', 'console')
262
+ .option('--strict', 'Exit with error if contracts are missing', false)
263
+ .option('--registry <path>', 'Path to registry module')
264
+ .option('--tests', 'Check for tests for each rule/constraint', true)
265
+ .option('--spec', 'Check for specs for each rule/constraint', true)
266
+ .option('--emit-facts', 'Emit ContractMissing facts JSON payload', false)
267
+ .option('--gap-output <file>', 'Write contract-gap payload to file')
268
+ .option('--ledger <dir>', 'Write logic ledger snapshots to directory')
269
+ .option('--author <name>', 'Author name for ledger entries', 'system')
270
+ .action(async (options) => {
271
+ try {
272
+ const { validateCommand } = await import('./commands/validate.js');
273
+ await validateCommand(options);
274
+ } catch (error) {
275
+ console.error('Error validating contracts:', error);
276
+ process.exit(1);
277
+ }
278
+ });
279
+
280
+ // Reverse command (Decision Ledger - Reverse Engineering)
281
+ program
282
+ .command('reverse')
283
+ .description('Reverse engineer contracts from existing codebase')
284
+ .option('-d, --dir <path>', 'Root directory to scan', process.cwd())
285
+ .option('--ai <provider>', 'AI provider (none, github-copilot, openai, auto)', 'none')
286
+ .option('-o, --output <dir>', 'Output directory for contracts', './contracts')
287
+ .option('--ledger', 'Write to logic ledger', false)
288
+ .option('--dry-run', 'Dry run mode (no files written)', false)
289
+ .option('-i, --interactive', 'Interactive mode (prompt for each)', false)
290
+ .option('--confidence <threshold>', 'Confidence threshold (0.0-1.0)', '0.7')
291
+ .option('--limit <n>', 'Max number of rules to process')
292
+ .option('--author <name>', 'Author name for ledger entries', 'reverse-engineer')
293
+ .option('--format <format>', 'Output format (json, yaml)', 'json')
294
+ .action(async (options) => {
295
+ try {
296
+ const { reverseCommand } = await import('./commands/reverse.js');
297
+ await reverseCommand(options);
298
+ } catch (error) {
299
+ console.error('Error reverse engineering contracts:', error);
300
+ process.exit(1);
301
+ }
302
+ });
303
+
257
304
  program.parse();
@@ -5,6 +5,8 @@
5
5
  * This module defines the core interface and an in-memory implementation.
6
6
  */
7
7
 
8
+ import type { LocalFirstOptions, PluresDBLocalFirst } from '@plures/pluresdb/local-first';
9
+
8
10
  /**
9
11
  * Function to unsubscribe from a watch
10
12
  */
@@ -121,7 +123,10 @@ export function createInMemoryDB(): InMemoryPraxisDB {
121
123
  */
122
124
  export type PluresDBInstance = {
123
125
  get(key: string): Promise<any>;
124
- put(key: string, value: any): Promise<void>;
126
+ put(key: string, value: any): Promise<any>;
127
+ delete?(key: string): Promise<void>;
128
+ list?(): Promise<any[]>;
129
+ close?(): Promise<void>;
125
130
  };
126
131
 
127
132
  /**
@@ -258,7 +263,7 @@ export class PluresDBPraxisAdapter implements PraxisDB {
258
263
  *
259
264
  * @example
260
265
  * ```typescript
261
- * import { PluresNode } from 'pluresdb';
266
+ * import { PluresNode } from '@plures/pluresdb';
262
267
  * import { createPluresDB } from '@plures/praxis';
263
268
  *
264
269
  * const pluresdb = new PluresNode({ autoStart: true });
@@ -280,3 +285,41 @@ export class PluresDBPraxisAdapter implements PraxisDB {
280
285
  export function createPluresDB(config: PluresDBAdapterConfig | PluresDBInstance): PluresDBPraxisAdapter {
281
286
  return new PluresDBPraxisAdapter(config);
282
287
  }
288
+
289
+ /**
290
+ * Options for creating a local-first PluresDB adapter using the unified API
291
+ */
292
+ export interface PraxisLocalFirstOptions extends LocalFirstOptions {
293
+ /** Optional polling interval override for watch semantics (ms). Defaults to 1000ms. */
294
+ pollInterval?: number;
295
+ }
296
+
297
+ /**
298
+ * Create a PraxisDB adapter backed by PluresDB's unified local-first API.
299
+ *
300
+ * This will auto-detect the best backend (WASM/Tauri/IPC/network) unless a mode is provided.
301
+ * Uses dynamic import to avoid bundling the local-first module in environments that don't need it.
302
+ *
303
+ * @example
304
+ * ```typescript
305
+ * const db = await createPraxisLocalFirst({ mode: 'auto' });
306
+ * await db.set('/_praxis/facts/user/1', { id: '1', name: 'Alice' });
307
+ * ```
308
+ */
309
+ export async function createPraxisLocalFirst(
310
+ options: PraxisLocalFirstOptions = {}
311
+ ): Promise<PluresDBPraxisAdapter> {
312
+ const { pollInterval, ...localOptions } = options;
313
+
314
+ const mod = await import('@plures/pluresdb/local-first');
315
+ const LocalFirstCtor =
316
+ (mod as { PluresDBLocalFirst?: new (opts?: LocalFirstOptions) => PluresDBLocalFirst }).PluresDBLocalFirst ??
317
+ (mod as { default?: new (opts?: LocalFirstOptions) => PluresDBLocalFirst }).default;
318
+
319
+ if (!LocalFirstCtor) {
320
+ throw new Error('Failed to load PluresDBLocalFirst from @plures/pluresdb/local-first');
321
+ }
322
+
323
+ const db = new LocalFirstCtor(localOptions as LocalFirstOptions);
324
+ return new PluresDBPraxisAdapter({ db, pollInterval });
325
+ }
package/src/core/rules.ts CHANGED
@@ -7,6 +7,15 @@
7
7
  */
8
8
 
9
9
  import type { PraxisEvent, PraxisFact, PraxisState } from './protocol.js';
10
+ import type { Contract, ContractGap, MissingArtifact, Severity } from '../decision-ledger/types.js';
11
+
12
+ declare const process:
13
+ | {
14
+ env?: {
15
+ NODE_ENV?: string;
16
+ };
17
+ }
18
+ | undefined;
10
19
 
11
20
  /**
12
21
  * Unique identifier for a rule
@@ -52,6 +61,8 @@ export interface RuleDescriptor<TContext = unknown> {
52
61
  description: string;
53
62
  /** Implementation function */
54
63
  impl: RuleFn<TContext>;
64
+ /** Optional contract for rule behavior */
65
+ contract?: Contract;
55
66
  /** Optional metadata */
56
67
  meta?: Record<string, unknown>;
57
68
  }
@@ -66,6 +77,8 @@ export interface ConstraintDescriptor<TContext = unknown> {
66
77
  description: string;
67
78
  /** Implementation function */
68
79
  impl: ConstraintFn<TContext>;
80
+ /** Optional contract for constraint behavior */
81
+ contract?: Contract;
69
82
  /** Optional metadata */
70
83
  meta?: Record<string, unknown>;
71
84
  }
@@ -83,6 +96,27 @@ export interface PraxisModule<TContext = unknown> {
83
96
  meta?: Record<string, unknown>;
84
97
  }
85
98
 
99
+ /**
100
+ * Compliance validation options for rule/constraint registration.
101
+ */
102
+ export interface RegistryComplianceOptions {
103
+ /** Enable contract checks during registration (default: true in dev) */
104
+ enabled?: boolean;
105
+ /** Required contract fields to be present */
106
+ requiredFields?: Array<'behavior' | 'examples' | 'invariants'>;
107
+ /** Severity to use for missing contracts */
108
+ missingSeverity?: Severity;
109
+ /** Callback for contract gaps (e.g., to emit facts) */
110
+ onGap?: (gap: ContractGap) => void;
111
+ }
112
+
113
+ /**
114
+ * PraxisRegistry configuration options.
115
+ */
116
+ export interface PraxisRegistryOptions {
117
+ compliance?: RegistryComplianceOptions;
118
+ }
119
+
86
120
  /**
87
121
  * Registry for rules and constraints.
88
122
  * Maps IDs to their descriptors.
@@ -90,6 +124,18 @@ export interface PraxisModule<TContext = unknown> {
90
124
  export class PraxisRegistry<TContext = unknown> {
91
125
  private rules = new Map<RuleId, RuleDescriptor<TContext>>();
92
126
  private constraints = new Map<ConstraintId, ConstraintDescriptor<TContext>>();
127
+ private readonly compliance: RegistryComplianceOptions;
128
+ private contractGaps: ContractGap[] = [];
129
+
130
+ constructor(options: PraxisRegistryOptions = {}) {
131
+ const defaultEnabled = typeof process !== 'undefined' ? process.env?.NODE_ENV !== 'production' : false;
132
+ this.compliance = {
133
+ enabled: defaultEnabled,
134
+ requiredFields: ['behavior', 'examples', 'invariants'],
135
+ missingSeverity: 'warning',
136
+ ...options.compliance,
137
+ };
138
+ }
93
139
 
94
140
  /**
95
141
  * Register a rule
@@ -99,6 +145,7 @@ export class PraxisRegistry<TContext = unknown> {
99
145
  throw new Error(`Rule with id "${descriptor.id}" already registered`);
100
146
  }
101
147
  this.rules.set(descriptor.id, descriptor);
148
+ this.trackContractCompliance(descriptor.id, descriptor);
102
149
  }
103
150
 
104
151
  /**
@@ -109,6 +156,7 @@ export class PraxisRegistry<TContext = unknown> {
109
156
  throw new Error(`Constraint with id "${descriptor.id}" already registered`);
110
157
  }
111
158
  this.constraints.set(descriptor.id, descriptor);
159
+ this.trackContractCompliance(descriptor.id, descriptor);
112
160
  }
113
161
 
114
162
  /**
@@ -164,4 +212,89 @@ export class PraxisRegistry<TContext = unknown> {
164
212
  getAllConstraints(): ConstraintDescriptor<TContext>[] {
165
213
  return Array.from(this.constraints.values());
166
214
  }
215
+
216
+ /**
217
+ * Get collected contract gaps from registration-time validation.
218
+ */
219
+ getContractGaps(): ContractGap[] {
220
+ return [...this.contractGaps];
221
+ }
222
+
223
+ /**
224
+ * Clear collected contract gaps.
225
+ */
226
+ clearContractGaps(): void {
227
+ this.contractGaps = [];
228
+ }
229
+
230
+ private trackContractCompliance(
231
+ id: string,
232
+ descriptor: RuleDescriptor<TContext> | ConstraintDescriptor<TContext>
233
+ ): void {
234
+ if (!this.compliance.enabled) {
235
+ return;
236
+ }
237
+
238
+ const gaps = this.validateDescriptorContract(id, descriptor);
239
+ for (const gap of gaps) {
240
+ this.contractGaps.push(gap);
241
+ if (this.compliance.onGap) {
242
+ this.compliance.onGap(gap);
243
+ } else {
244
+ const label = gap.severity === 'error' ? 'ERROR' : gap.severity === 'warning' ? 'WARN' : 'INFO';
245
+ console.warn(`[Praxis][${label}] Contract gap for "${gap.ruleId}": missing ${gap.missing.join(', ')}`);
246
+ }
247
+ }
248
+ }
249
+
250
+ private validateDescriptorContract(
251
+ id: string,
252
+ descriptor: RuleDescriptor<TContext> | ConstraintDescriptor<TContext>
253
+ ): ContractGap[] {
254
+ const requiredFields = this.compliance.requiredFields ?? ['behavior', 'examples', 'invariants'];
255
+ const missingSeverity = this.compliance.missingSeverity ?? 'warning';
256
+ const contract =
257
+ descriptor.contract ??
258
+ (descriptor.meta?.contract && typeof descriptor.meta.contract === 'object'
259
+ ? (descriptor.meta.contract as Contract)
260
+ : undefined);
261
+
262
+ if (!contract) {
263
+ return [
264
+ {
265
+ ruleId: id,
266
+ missing: ['contract'],
267
+ severity: missingSeverity,
268
+ message: `Contract missing for "${id}"`,
269
+ },
270
+ ];
271
+ }
272
+
273
+ const missing: MissingArtifact[] = [];
274
+
275
+ if (requiredFields.includes('behavior') && (!contract.behavior || contract.behavior.trim() === '')) {
276
+ missing.push('behavior');
277
+ }
278
+
279
+ if (requiredFields.includes('examples') && (!contract.examples || contract.examples.length === 0)) {
280
+ missing.push('examples');
281
+ }
282
+
283
+ if (requiredFields.includes('invariants') && (!contract.invariants || contract.invariants.length === 0)) {
284
+ missing.push('invariants');
285
+ }
286
+
287
+ if (missing.length === 0) {
288
+ return [];
289
+ }
290
+
291
+ return [
292
+ {
293
+ ruleId: id,
294
+ missing,
295
+ severity: 'warning',
296
+ message: `Contract for "${id}" is incomplete: missing ${missing.join(', ')}`,
297
+ },
298
+ ];
299
+ }
167
300
  }