@plures/praxis 1.3.0 → 1.4.4

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 (74) hide show
  1. package/dist/browser/{chunk-N63K4KWS.js → chunk-4IRUGWR3.js} +1 -1
  2. package/dist/browser/chunk-6SJ44Q64.js +473 -0
  3. package/dist/browser/chunk-BQOYZBWA.js +282 -0
  4. package/dist/browser/chunk-IG5BJ2MT.js +91 -0
  5. package/dist/browser/{chunk-MJK3IYTJ.js → chunk-JZDJU2DO.js} +4 -84
  6. package/dist/browser/chunk-ZEW4LJAJ.js +353 -0
  7. package/dist/browser/{engine-YIEGSX7U.js → engine-3B5WJPGT.js} +2 -1
  8. package/dist/browser/expectations/index.d.ts +180 -0
  9. package/dist/browser/expectations/index.js +14 -0
  10. package/dist/browser/factory/index.d.ts +149 -0
  11. package/dist/browser/factory/index.js +15 -0
  12. package/dist/browser/index.d.ts +274 -3
  13. package/dist/browser/index.js +407 -54
  14. package/dist/browser/integrations/svelte.d.ts +3 -2
  15. package/dist/browser/integrations/svelte.js +3 -2
  16. package/dist/browser/project/index.d.ts +176 -0
  17. package/dist/browser/project/index.js +19 -0
  18. package/dist/browser/reactive-engine.svelte-DgVTqHLc.d.ts +223 -0
  19. package/dist/browser/{reactive-engine.svelte-DjynI82A.d.ts → rules-i1LHpnGd.d.ts} +13 -221
  20. package/dist/node/chunk-2IUFZBH3.js +87 -0
  21. package/dist/node/{chunk-WZ6B3LZ6.js → chunk-7CSWBDFL.js} +3 -56
  22. package/dist/node/chunk-AZLNISFI.js +1690 -0
  23. package/dist/node/chunk-IG5BJ2MT.js +91 -0
  24. package/dist/node/{chunk-KMJWAFZV.js → chunk-JZDJU2DO.js} +4 -89
  25. package/dist/node/chunk-PGVSB6NR.js +59 -0
  26. package/dist/node/{chunk-5JQJZADT.js → chunk-ZO2LU4G4.js} +4 -4
  27. package/dist/node/cli/index.cjs +1126 -211
  28. package/dist/node/cli/index.js +21 -2
  29. package/dist/node/{engine-FEN5IYZ5.js → engine-VFHCIEM4.js} +2 -1
  30. package/dist/node/index.cjs +5623 -2765
  31. package/dist/node/index.d.cts +1181 -1
  32. package/dist/node/index.d.ts +1181 -1
  33. package/dist/node/index.js +1646 -79
  34. package/dist/node/integrations/svelte.js +4 -3
  35. package/dist/node/{reverse-W7THPV45.js → reverse-YD3CWIGM.js} +3 -2
  36. package/dist/node/rules-4DAJ4Z4N.js +7 -0
  37. package/dist/node/server-FKLVY57V.js +363 -0
  38. package/dist/node/{validate-EN3M4FUR.js → validate-5PSWJTIC.js} +5 -3
  39. package/package.json +50 -3
  40. package/src/__tests__/chronos-project.test.ts +799 -0
  41. package/src/__tests__/decision-ledger.test.ts +857 -402
  42. package/src/__tests__/expectations.test.ts +364 -0
  43. package/src/__tests__/factory.test.ts +426 -0
  44. package/src/__tests__/mcp-server.test.ts +310 -0
  45. package/src/__tests__/project.test.ts +396 -0
  46. package/src/chronos/diff.ts +336 -0
  47. package/src/chronos/hooks.ts +227 -0
  48. package/src/chronos/index.ts +83 -0
  49. package/src/chronos/project-chronicle.ts +198 -0
  50. package/src/chronos/timeline.ts +152 -0
  51. package/src/cli/index.ts +28 -0
  52. package/src/decision-ledger/analyzer-types.ts +280 -0
  53. package/src/decision-ledger/analyzer.ts +518 -0
  54. package/src/decision-ledger/contract-verification.ts +456 -0
  55. package/src/decision-ledger/derivation.ts +158 -0
  56. package/src/decision-ledger/index.ts +59 -0
  57. package/src/decision-ledger/report.ts +378 -0
  58. package/src/decision-ledger/suggestions.ts +287 -0
  59. package/src/expectations/expectations.ts +471 -0
  60. package/src/expectations/index.ts +29 -0
  61. package/src/expectations/types.ts +95 -0
  62. package/src/factory/factory.ts +634 -0
  63. package/src/factory/index.ts +27 -0
  64. package/src/factory/types.ts +64 -0
  65. package/src/index.browser.ts +83 -0
  66. package/src/index.ts +134 -0
  67. package/src/mcp/index.ts +33 -0
  68. package/src/mcp/server.ts +485 -0
  69. package/src/mcp/types.ts +161 -0
  70. package/src/project/index.ts +31 -0
  71. package/src/project/project.ts +423 -0
  72. package/src/project/types.ts +87 -0
  73. package/dist/node/chunk-PTH6MD6P.js +0 -487
  74. /package/dist/node/{chunk-R2PSBPKQ.js → chunk-TEMFJOIH.js} +0 -0
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Behavioral Diff Engine
3
+ *
4
+ * Compares registry snapshots, contract coverage, and expectation status
5
+ * to produce human-readable deltas and conventional commit messages.
6
+ *
7
+ * This is the foundation for "commit from state" — describing WHAT changed
8
+ * in behavioral terms rather than file terms.
9
+ */
10
+
11
+ import type { RuleDescriptor, ConstraintDescriptor } from '../core/rules.js';
12
+ import type { PraxisDiff } from '../project/types.js';
13
+
14
+ // ─── Types ──────────────────────────────────────────────────────────────────
15
+
16
+ /** Snapshot of a registry's state (used for before/after comparison). */
17
+ export interface RegistrySnapshot {
18
+ rules: Map<string, RuleDescriptor> | Array<RuleDescriptor>;
19
+ constraints: Map<string, ConstraintDescriptor> | Array<ConstraintDescriptor>;
20
+ }
21
+
22
+ /** Diff result for registries. */
23
+ export interface RegistryDiff {
24
+ rulesAdded: string[];
25
+ rulesRemoved: string[];
26
+ rulesModified: string[];
27
+ constraintsAdded: string[];
28
+ constraintsRemoved: string[];
29
+ constraintsModified: string[];
30
+ }
31
+
32
+ /** Contract coverage snapshot. */
33
+ export interface ContractCoverage {
34
+ /** rule id → has contract */
35
+ coverage: Map<string, boolean> | Record<string, boolean>;
36
+ }
37
+
38
+ /** Diff result for contract coverage. */
39
+ export interface ContractDiff {
40
+ contractsAdded: string[];
41
+ contractsRemoved: string[];
42
+ coverageBefore: number;
43
+ coverageAfter: number;
44
+ }
45
+
46
+ /** Expectation satisfaction snapshot. */
47
+ export interface ExpectationSnapshot {
48
+ /** expectation name → satisfied */
49
+ expectations: Map<string, boolean> | Record<string, boolean>;
50
+ }
51
+
52
+ /** Diff result for expectations. */
53
+ export interface ExpectationDiff {
54
+ newlySatisfied: string[];
55
+ newlyViolated: string[];
56
+ unchanged: string[];
57
+ }
58
+
59
+ /** Full behavioral diff combining all dimensions. */
60
+ export interface FullBehavioralDiff {
61
+ registry: RegistryDiff;
62
+ contracts: ContractDiff;
63
+ expectations: ExpectationDiff;
64
+ }
65
+
66
+ // ─── Registry Diff ──────────────────────────────────────────────────────────
67
+
68
+ /**
69
+ * Compare two registry snapshots.
70
+ *
71
+ * Detects rules/constraints that were added, removed, or modified
72
+ * (description or contract changed).
73
+ */
74
+ export function diffRegistries(before: RegistrySnapshot, after: RegistrySnapshot): RegistryDiff {
75
+ const beforeRules = toIdMap(before.rules);
76
+ const afterRules = toIdMap(after.rules);
77
+ const beforeConstraints = toIdMap(before.constraints);
78
+ const afterConstraints = toIdMap(after.constraints);
79
+
80
+ return {
81
+ rulesAdded: setDiff(afterRules, beforeRules),
82
+ rulesRemoved: setDiff(beforeRules, afterRules),
83
+ rulesModified: findModified(beforeRules, afterRules),
84
+ constraintsAdded: setDiff(afterConstraints, beforeConstraints),
85
+ constraintsRemoved: setDiff(beforeConstraints, afterConstraints),
86
+ constraintsModified: findModified(beforeConstraints, afterConstraints),
87
+ };
88
+ }
89
+
90
+ // ─── Contract Diff ──────────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * Compare contract coverage between two snapshots.
94
+ */
95
+ export function diffContracts(before: ContractCoverage, after: ContractCoverage): ContractDiff {
96
+ const beforeMap = toRecord(before.coverage);
97
+ const afterMap = toRecord(after.coverage);
98
+
99
+ const contractsAdded: string[] = [];
100
+ const contractsRemoved: string[] = [];
101
+
102
+ // Check for newly added contracts
103
+ for (const [id, has] of Object.entries(afterMap)) {
104
+ if (has && !beforeMap[id]) {
105
+ contractsAdded.push(id);
106
+ }
107
+ }
108
+
109
+ // Check for removed contracts
110
+ for (const [id, had] of Object.entries(beforeMap)) {
111
+ if (had && !afterMap[id]) {
112
+ contractsRemoved.push(id);
113
+ }
114
+ }
115
+
116
+ const countTrue = (r: Record<string, boolean>) =>
117
+ Object.values(r).filter(Boolean).length;
118
+ const totalBefore = Object.keys(beforeMap).length;
119
+ const totalAfter = Object.keys(afterMap).length;
120
+
121
+ return {
122
+ contractsAdded,
123
+ contractsRemoved,
124
+ coverageBefore: totalBefore > 0 ? countTrue(beforeMap) / totalBefore : 0,
125
+ coverageAfter: totalAfter > 0 ? countTrue(afterMap) / totalAfter : 0,
126
+ };
127
+ }
128
+
129
+ // ─── Expectation Diff ───────────────────────────────────────────────────────
130
+
131
+ /**
132
+ * Compare expectation satisfaction between two snapshots.
133
+ */
134
+ export function diffExpectations(
135
+ before: ExpectationSnapshot,
136
+ after: ExpectationSnapshot,
137
+ ): ExpectationDiff {
138
+ const beforeMap = toRecord(before.expectations);
139
+ const afterMap = toRecord(after.expectations);
140
+ const allKeys = new Set([...Object.keys(beforeMap), ...Object.keys(afterMap)]);
141
+
142
+ const newlySatisfied: string[] = [];
143
+ const newlyViolated: string[] = [];
144
+ const unchanged: string[] = [];
145
+
146
+ for (const key of allKeys) {
147
+ const was = beforeMap[key] ?? false;
148
+ const is = afterMap[key] ?? false;
149
+ if (!was && is) {
150
+ newlySatisfied.push(key);
151
+ } else if (was && !is) {
152
+ newlyViolated.push(key);
153
+ } else {
154
+ unchanged.push(key);
155
+ }
156
+ }
157
+
158
+ return { newlySatisfied, newlyViolated, unchanged };
159
+ }
160
+
161
+ // ─── Formatting ─────────────────────────────────────────────────────────────
162
+
163
+ /**
164
+ * Format a RegistryDiff as a human-readable delta string.
165
+ */
166
+ export function formatDelta(diff: RegistryDiff): string {
167
+ const lines: string[] = [];
168
+
169
+ if (diff.rulesAdded.length > 0)
170
+ lines.push(`+ Rules added: ${diff.rulesAdded.join(', ')}`);
171
+ if (diff.rulesRemoved.length > 0)
172
+ lines.push(`- Rules removed: ${diff.rulesRemoved.join(', ')}`);
173
+ if (diff.rulesModified.length > 0)
174
+ lines.push(`~ Rules modified: ${diff.rulesModified.join(', ')}`);
175
+ if (diff.constraintsAdded.length > 0)
176
+ lines.push(`+ Constraints added: ${diff.constraintsAdded.join(', ')}`);
177
+ if (diff.constraintsRemoved.length > 0)
178
+ lines.push(`- Constraints removed: ${diff.constraintsRemoved.join(', ')}`);
179
+ if (diff.constraintsModified.length > 0)
180
+ lines.push(`~ Constraints modified: ${diff.constraintsModified.join(', ')}`);
181
+
182
+ return lines.length > 0 ? lines.join('\n') : 'No behavioral changes.';
183
+ }
184
+
185
+ /**
186
+ * Generate a conventional commit message from a registry diff.
187
+ *
188
+ * Uses the same logic as `commitFromState` in `project/` but works
189
+ * directly from a RegistryDiff.
190
+ */
191
+ export function formatCommitMessage(diff: RegistryDiff): string {
192
+ const praxisDiff: PraxisDiff = {
193
+ rulesAdded: diff.rulesAdded,
194
+ rulesRemoved: diff.rulesRemoved,
195
+ rulesModified: diff.rulesModified,
196
+ contractsAdded: [],
197
+ contractsRemoved: [],
198
+ expectationsAdded: diff.constraintsAdded,
199
+ expectationsRemoved: diff.constraintsRemoved,
200
+ gateChanges: [],
201
+ };
202
+
203
+ return commitFromDiff(praxisDiff);
204
+ }
205
+
206
+ /**
207
+ * Aggregate multiple diffs into release notes.
208
+ */
209
+ export function formatReleaseNotes(diffs: RegistryDiff[]): string {
210
+ const sections: string[] = [];
211
+ const allAdded: string[] = [];
212
+ const allRemoved: string[] = [];
213
+ const allModified: string[] = [];
214
+
215
+ for (const diff of diffs) {
216
+ allAdded.push(...diff.rulesAdded, ...diff.constraintsAdded);
217
+ allRemoved.push(...diff.rulesRemoved, ...diff.constraintsRemoved);
218
+ allModified.push(...diff.rulesModified, ...diff.constraintsModified);
219
+ }
220
+
221
+ // Deduplicate
222
+ const added = [...new Set(allAdded)];
223
+ const removed = [...new Set(allRemoved)];
224
+ const modified = [...new Set(allModified)];
225
+
226
+ if (added.length > 0) {
227
+ sections.push(`### Added\n${added.map(id => `- ${id}`).join('\n')}`);
228
+ }
229
+ if (modified.length > 0) {
230
+ sections.push(`### Changed\n${modified.map(id => `- ${id}`).join('\n')}`);
231
+ }
232
+ if (removed.length > 0) {
233
+ sections.push(`### Removed\n${removed.map(id => `- ${id}`).join('\n')}`);
234
+ }
235
+
236
+ if (sections.length === 0) return 'No behavioral changes in this release.';
237
+ return `## Release Notes\n\n${sections.join('\n\n')}`;
238
+ }
239
+
240
+ // ─── Internal helpers ───────────────────────────────────────────────────────
241
+
242
+ function toIdMap(
243
+ input: Map<string, { id: string; description: string }> | Array<{ id: string; description: string }>,
244
+ ): Map<string, { id: string; description: string }> {
245
+ if (input instanceof Map) return input;
246
+ const map = new Map<string, { id: string; description: string }>();
247
+ for (const item of input) map.set(item.id, item);
248
+ return map;
249
+ }
250
+
251
+ function setDiff(a: Map<string, unknown>, b: Map<string, unknown>): string[] {
252
+ const result: string[] = [];
253
+ for (const key of a.keys()) {
254
+ if (!b.has(key)) result.push(key);
255
+ }
256
+ return result;
257
+ }
258
+
259
+ function findModified(
260
+ before: Map<string, { id: string; description: string }>,
261
+ after: Map<string, { id: string; description: string }>,
262
+ ): string[] {
263
+ const result: string[] = [];
264
+ for (const [key, beforeVal] of before) {
265
+ const afterVal = after.get(key);
266
+ if (afterVal && beforeVal.description !== afterVal.description) {
267
+ result.push(key);
268
+ }
269
+ }
270
+ return result;
271
+ }
272
+
273
+ function toRecord(input: Map<string, boolean> | Record<string, boolean>): Record<string, boolean> {
274
+ if (input instanceof Map) {
275
+ const result: Record<string, boolean> = {};
276
+ for (const [k, v] of input) result[k] = v;
277
+ return result;
278
+ }
279
+ return input;
280
+ }
281
+
282
+ // ── Inline commit message generation (mirrors project/project.ts logic) ──
283
+
284
+ function commitFromDiff(diff: PraxisDiff): string {
285
+ const parts: string[] = [];
286
+ const bodyParts: string[] = [];
287
+
288
+ const totalAdded = diff.rulesAdded.length + diff.contractsAdded.length + diff.expectationsAdded.length;
289
+ const totalRemoved = diff.rulesRemoved.length + diff.contractsRemoved.length + diff.expectationsRemoved.length;
290
+ const totalModified = diff.rulesModified.length;
291
+
292
+ if (totalAdded > 0 && totalRemoved === 0 && totalModified === 0) {
293
+ if (diff.rulesAdded.length > 0) {
294
+ const scope = inferScope(diff.rulesAdded);
295
+ parts.push(`feat(${scope}): add ${fmtIds(diff.rulesAdded)}`);
296
+ } else if (diff.contractsAdded.length > 0) {
297
+ parts.push(`feat(contracts): add contracts for ${fmtIds(diff.contractsAdded)}`);
298
+ } else {
299
+ parts.push(`feat(expectations): add ${fmtIds(diff.expectationsAdded)}`);
300
+ }
301
+ } else if (totalRemoved > 0 && totalAdded === 0) {
302
+ if (diff.rulesRemoved.length > 0) {
303
+ const scope = inferScope(diff.rulesRemoved);
304
+ parts.push(`refactor(${scope}): remove ${fmtIds(diff.rulesRemoved)}`);
305
+ } else {
306
+ parts.push(`refactor: remove ${totalRemoved} item(s)`);
307
+ }
308
+ } else if (totalModified > 0) {
309
+ const scope = inferScope(diff.rulesModified);
310
+ parts.push(`refactor(${scope}): update ${fmtIds(diff.rulesModified)}`);
311
+ } else {
312
+ parts.push('chore: behavioral state update');
313
+ }
314
+
315
+ if (diff.rulesAdded.length > 0) bodyParts.push(`Rules added: ${diff.rulesAdded.join(', ')}`);
316
+ if (diff.rulesRemoved.length > 0) bodyParts.push(`Rules removed: ${diff.rulesRemoved.join(', ')}`);
317
+ if (diff.rulesModified.length > 0) bodyParts.push(`Rules modified: ${diff.rulesModified.join(', ')}`);
318
+
319
+ const subject = parts[0] || 'chore: update';
320
+ return bodyParts.length > 0 ? `${subject}\n\n${bodyParts.join('\n')}` : subject;
321
+ }
322
+
323
+ function inferScope(ids: string[]): string {
324
+ if (ids.length === 0) return 'rules';
325
+ const prefixes = ids.map(id => {
326
+ const slash = id.indexOf('/');
327
+ return slash > 0 ? id.slice(0, slash) : id;
328
+ });
329
+ const unique = new Set(prefixes);
330
+ return unique.size === 1 ? prefixes[0] : 'rules';
331
+ }
332
+
333
+ function fmtIds(ids: string[]): string {
334
+ if (ids.length <= 3) return ids.join(', ');
335
+ return `${ids.slice(0, 2).join(', ')} (+${ids.length - 2} more)`;
336
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Auto-Recording Hooks — wire ProjectChronicle into PraxisRegistry & LogicEngine
3
+ *
4
+ * Opt-in: call `enableProjectChronicle(registry, engine)` to start recording.
5
+ * The hooks use Proxy wrapping to intercept method calls without modifying
6
+ * the original classes.
7
+ */
8
+
9
+ import type { PraxisRegistry, RuleDescriptor, PraxisModule } from '../core/rules.js';
10
+ import type { LogicEngine } from '../core/engine.js';
11
+ import type { PraxisEvent, PraxisStepResult, PraxisDiagnostics } from '../core/protocol.js';
12
+ import { ProjectChronicle, createProjectChronicle } from './project-chronicle.js';
13
+ import type { CompletenessReport } from '../core/completeness.js';
14
+
15
+ // ─── Types ──────────────────────────────────────────────────────────────────
16
+
17
+ /** Handle returned by enableProjectChronicle for cleanup. */
18
+ export interface ChronicleHandle {
19
+ /** The underlying chronicle being written to. */
20
+ chronicle: ProjectChronicle;
21
+ /** Disconnect all hooks (restores original methods). */
22
+ disconnect: () => void;
23
+ }
24
+
25
+ /** Options for enabling project chronicle hooks. */
26
+ export interface EnableChronicleOptions {
27
+ /** Provide an existing chronicle (otherwise a new one is created). */
28
+ chronicle?: ProjectChronicle;
29
+ /** Record engine step results (fact production/retraction). Default: true. */
30
+ recordSteps?: boolean;
31
+ /** Record constraint check results. Default: true. */
32
+ recordConstraints?: boolean;
33
+ }
34
+
35
+ // ─── Hook Implementation ────────────────────────────────────────────────────
36
+
37
+ /**
38
+ * Enable project-level chronicle recording.
39
+ *
40
+ * Wraps registry's `registerRule`, `registerModule` and engine's `step`,
41
+ * `checkConstraints` methods to automatically record events.
42
+ *
43
+ * @returns A handle with the chronicle and a `disconnect()` to undo all hooks.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * const { chronicle, disconnect } = enableProjectChronicle(registry, engine);
48
+ * registry.registerRule(myRule); // auto-recorded
49
+ * engine.step(events); // step results auto-recorded
50
+ * console.log(chronicle.size); // number of events recorded
51
+ * disconnect(); // stop recording
52
+ * ```
53
+ */
54
+ export function enableProjectChronicle<TContext = unknown>(
55
+ registry: PraxisRegistry<TContext>,
56
+ engine: LogicEngine<TContext>,
57
+ options: EnableChronicleOptions = {},
58
+ ): ChronicleHandle {
59
+ const chronicle = options.chronicle ?? createProjectChronicle();
60
+ const recordSteps = options.recordSteps ?? true;
61
+ const recordConstraints = options.recordConstraints ?? true;
62
+
63
+ // Keep originals for cleanup
64
+ const origRegisterRule = registry.registerRule.bind(registry);
65
+ const origRegisterModule = registry.registerModule.bind(registry);
66
+ const origStep = engine.step.bind(engine);
67
+ const origCheckConstraints = engine.checkConstraints.bind(engine);
68
+
69
+ // ── Hook: registerRule ──────────────────────────────────────────────────
70
+
71
+ registry.registerRule = function hookedRegisterRule(
72
+ descriptor: RuleDescriptor<TContext>,
73
+ ): void {
74
+ origRegisterRule(descriptor);
75
+ chronicle.recordRuleRegistered(descriptor.id, {
76
+ description: descriptor.description,
77
+ hasContract: !!descriptor.contract,
78
+ eventTypes: descriptor.eventTypes,
79
+ });
80
+ if (descriptor.contract) {
81
+ chronicle.recordContractAdded(descriptor.id, {
82
+ behavior: descriptor.contract.behavior,
83
+ examplesCount: descriptor.contract.examples?.length ?? 0,
84
+ invariantsCount: descriptor.contract.invariants?.length ?? 0,
85
+ });
86
+ }
87
+ };
88
+
89
+ // ── Hook: registerModule ────────────────────────────────────────────────
90
+
91
+ registry.registerModule = function hookedRegisterModule(
92
+ module: PraxisModule<TContext>,
93
+ ): void {
94
+ // Call original (which calls registerRule/registerConstraint internally)
95
+ // But we need to unhook registerRule temporarily to avoid double recording.
96
+ // Instead, record the module-level event and let the rules record individually.
97
+ origRegisterModule(module);
98
+
99
+ // Record a module-level event for each rule that was part of this module
100
+ for (const rule of module.rules) {
101
+ chronicle.recordRuleRegistered(rule.id, {
102
+ description: rule.description,
103
+ hasContract: !!rule.contract,
104
+ eventTypes: rule.eventTypes,
105
+ registeredVia: 'module',
106
+ });
107
+ if (rule.contract) {
108
+ chronicle.recordContractAdded(rule.id, {
109
+ behavior: rule.contract.behavior,
110
+ examplesCount: rule.contract.examples?.length ?? 0,
111
+ invariantsCount: rule.contract.invariants?.length ?? 0,
112
+ });
113
+ }
114
+ }
115
+ };
116
+
117
+ // ── Hook: engine.step ───────────────────────────────────────────────────
118
+
119
+ if (recordSteps) {
120
+ engine.step = function hookedStep(events: PraxisEvent[]): PraxisStepResult {
121
+ const result = origStep(events);
122
+
123
+ // Record each fact that was produced
124
+ const factTags = new Set<string>();
125
+ for (const fact of result.state.facts) {
126
+ factTags.add(fact.tag);
127
+ }
128
+
129
+ // Record step event
130
+ chronicle.record({
131
+ kind: 'build',
132
+ action: 'step-complete',
133
+ subject: 'engine',
134
+ metadata: {
135
+ eventsProcessed: events.length,
136
+ eventTags: events.map(e => e.tag),
137
+ factsAfter: result.state.facts.length,
138
+ diagnosticsCount: result.diagnostics.length,
139
+ },
140
+ });
141
+
142
+ // Record any rule errors or constraint violations
143
+ for (const diag of result.diagnostics) {
144
+ if (diag.kind === 'constraint-violation') {
145
+ chronicle.record({
146
+ kind: 'expectation',
147
+ action: 'violated',
148
+ subject: extractSubjectFromDiag(diag),
149
+ metadata: { message: diag.message, data: diag.data },
150
+ });
151
+ }
152
+ }
153
+
154
+ return result;
155
+ };
156
+ }
157
+
158
+ // ── Hook: engine.checkConstraints ───────────────────────────────────────
159
+
160
+ if (recordConstraints) {
161
+ engine.checkConstraints = function hookedCheckConstraints(): PraxisDiagnostics[] {
162
+ const diagnostics = origCheckConstraints();
163
+
164
+ chronicle.record({
165
+ kind: 'build',
166
+ action: 'constraints-checked',
167
+ subject: 'engine',
168
+ metadata: {
169
+ violations: diagnostics.length,
170
+ },
171
+ });
172
+
173
+ for (const diag of diagnostics) {
174
+ chronicle.record({
175
+ kind: 'expectation',
176
+ action: 'violated',
177
+ subject: extractSubjectFromDiag(diag),
178
+ metadata: { message: diag.message },
179
+ });
180
+ }
181
+
182
+ return diagnostics;
183
+ };
184
+ }
185
+
186
+ // ── Disconnect ──────────────────────────────────────────────────────────
187
+
188
+ function disconnect(): void {
189
+ registry.registerRule = origRegisterRule;
190
+ registry.registerModule = origRegisterModule;
191
+ engine.step = origStep;
192
+ engine.checkConstraints = origCheckConstraints;
193
+ }
194
+
195
+ return { chronicle, disconnect };
196
+ }
197
+
198
+ /**
199
+ * Record a completeness audit result into the chronicle.
200
+ *
201
+ * Standalone utility — call after `auditCompleteness()`.
202
+ */
203
+ export function recordAudit(
204
+ chronicle: ProjectChronicle,
205
+ report: CompletenessReport,
206
+ previousScore?: number,
207
+ ): void {
208
+ const delta = previousScore != null ? report.score - previousScore : 0;
209
+ chronicle.recordBuildAudit(report.score, delta, {
210
+ rating: report.rating,
211
+ rulesCovered: report.rules.covered,
212
+ rulesTotal: report.rules.total,
213
+ constraintsCovered: report.constraints.covered,
214
+ constraintsTotal: report.constraints.total,
215
+ contractsCovered: report.contracts.withContracts,
216
+ contractsTotal: report.contracts.total,
217
+ });
218
+ }
219
+
220
+ // ─── Internals ──────────────────────────────────────────────────────────────
221
+
222
+ function extractSubjectFromDiag(diag: PraxisDiagnostics): string {
223
+ const data = diag.data as Record<string, unknown> | undefined;
224
+ if (data?.constraintId && typeof data.constraintId === 'string') return data.constraintId;
225
+ if (data?.ruleId && typeof data.ruleId === 'string') return data.ruleId;
226
+ return 'unknown';
227
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Chronos — Project-Level Chronicle
3
+ *
4
+ * Records and queries the development lifecycle of a Praxis application:
5
+ * rule registrations, contract changes, gate transitions, build audits, etc.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import {
10
+ * createProjectChronicle,
11
+ * createTimeline,
12
+ * enableProjectChronicle,
13
+ * diffRegistries,
14
+ * } from '@plures/praxis';
15
+ *
16
+ * // Manual recording
17
+ * const chronicle = createProjectChronicle();
18
+ * chronicle.recordRuleRegistered('auth/login', { description: 'Login rule' });
19
+ *
20
+ * // Auto-recording via hooks
21
+ * const { chronicle: autoChronicle, disconnect } = enableProjectChronicle(registry, engine);
22
+ * registry.registerRule(myRule); // automatically recorded
23
+ *
24
+ * // Querying
25
+ * const timeline = createTimeline(autoChronicle);
26
+ * const ruleEvents = timeline.getTimeline({ kind: 'rule' });
27
+ * const delta = timeline.getDelta(startTs, endTs);
28
+ *
29
+ * // Behavioral diff
30
+ * const diff = diffRegistries(snapshotBefore, snapshotAfter);
31
+ * console.log(formatCommitMessage(diff));
32
+ * ```
33
+ */
34
+
35
+ // ── Project Chronicle (core event store) ────────────────────────────────────
36
+ export {
37
+ ProjectChronicle,
38
+ createProjectChronicle,
39
+ } from './project-chronicle.js';
40
+ export type {
41
+ ProjectEvent,
42
+ ProjectEventKind,
43
+ ProjectChronicleOptions,
44
+ } from './project-chronicle.js';
45
+
46
+ // ── Timeline (queryable view) ───────────────────────────────────────────────
47
+ export {
48
+ Timeline,
49
+ createTimeline,
50
+ } from './timeline.js';
51
+ export type {
52
+ TimelineFilter,
53
+ BehavioralDelta,
54
+ } from './timeline.js';
55
+
56
+ // ── Hooks (auto-recording) ──────────────────────────────────────────────────
57
+ export {
58
+ enableProjectChronicle,
59
+ recordAudit,
60
+ } from './hooks.js';
61
+ export type {
62
+ ChronicleHandle,
63
+ EnableChronicleOptions,
64
+ } from './hooks.js';
65
+
66
+ // ── Diff (behavioral comparison) ────────────────────────────────────────────
67
+ export {
68
+ diffRegistries,
69
+ diffContracts,
70
+ diffExpectations,
71
+ formatDelta,
72
+ formatCommitMessage,
73
+ formatReleaseNotes,
74
+ } from './diff.js';
75
+ export type {
76
+ RegistrySnapshot,
77
+ RegistryDiff,
78
+ ContractCoverage,
79
+ ContractDiff,
80
+ ExpectationSnapshot,
81
+ ExpectationDiff,
82
+ FullBehavioralDiff,
83
+ } from './diff.js';