@plures/praxis 1.2.13 → 1.2.41
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 +44 -0
- package/dist/browser/{chunk-VOMLVI6V.js → chunk-BBP2F7TT.js} +70 -1
- package/dist/browser/{chunk-K377RW4V.js → chunk-FCEH7WMH.js} +1 -1
- package/dist/browser/{engine-YJZV4SLD.js → engine-65QDGCAN.js} +1 -1
- package/dist/browser/index.d.ts +104 -2
- package/dist/browser/index.js +181 -5
- package/dist/browser/integrations/svelte.d.ts +2 -2
- package/dist/browser/integrations/svelte.js +2 -2
- package/dist/browser/{reactive-engine.svelte-9aS0kTa8.d.ts → reactive-engine.svelte-Cqd8Mod2.d.ts} +56 -1
- package/dist/node/{chunk-PRPQO6R5.js → chunk-32YFEEML.js} +1 -1
- package/dist/node/{chunk-VOMLVI6V.js → chunk-BBP2F7TT.js} +70 -1
- package/dist/node/{chunk-5RH7UAQC.js → chunk-PTH6MD6P.js} +1 -0
- package/dist/node/cli/index.cjs +1553 -839
- package/dist/node/cli/index.js +39 -2
- package/dist/node/cloud/index.d.cts +1 -1
- package/dist/node/cloud/index.d.ts +1 -1
- package/dist/node/components/index.d.cts +2 -2
- package/dist/node/components/index.d.ts +2 -2
- package/dist/node/conversations-KQBXTP3N.js +596 -0
- package/dist/node/{engine-2DQBKBJC.js → engine-7CXQV6RC.js} +1 -1
- package/dist/node/index.cjs +408 -3
- package/dist/node/index.d.cts +308 -7
- package/dist/node/index.d.ts +308 -7
- package/dist/node/index.js +336 -6
- package/dist/node/integrations/svelte.cjs +70 -1
- package/dist/node/integrations/svelte.d.cts +3 -3
- package/dist/node/integrations/svelte.d.ts +3 -3
- package/dist/node/integrations/svelte.js +2 -2
- package/dist/node/{protocol-Qek7ebBl.d.ts → protocol-BocKczNv.d.cts} +1 -1
- package/dist/node/{protocol-Qek7ebBl.d.cts → protocol-BocKczNv.d.ts} +1 -1
- package/dist/node/{reactive-engine.svelte-CRNqHlbv.d.ts → reactive-engine.svelte-CGe8SpVE.d.cts} +57 -2
- package/dist/node/{reactive-engine.svelte-BFIZfawz.d.cts → reactive-engine.svelte-D-xTDxT5.d.ts} +57 -2
- package/dist/node/{terminal-adapter-B-UK_Vdz.d.ts → terminal-adapter-CvIvgTo4.d.ts} +1 -1
- package/dist/node/{terminal-adapter-BQSIF5bf.d.cts → terminal-adapter-Db-snPJ3.d.cts} +1 -1
- package/dist/node/{validate-CNHUULQE.js → validate-EN3M4FUR.js} +1 -1
- package/dist/node/{verify-KLJRXVJS.js → verify-7VZRP2WS.js} +2 -2
- package/docs/BOT_UPDATE_POLICY.md +125 -0
- package/docs/DOGFOODING_CHECKLIST.md +254 -0
- package/docs/DOGFOODING_INDEX.md +169 -0
- package/docs/DOGFOODING_QUICK_START.md +140 -0
- package/docs/KNO_ENG_EXTRACTION_PLAN.md +577 -0
- package/docs/PLURES_TOOLS_INVENTORY.md +170 -0
- package/docs/README.md +12 -0
- package/docs/TESTING_BOT_WORKFLOWS.md +154 -0
- package/docs/conversations/INTEGRATION_POINTS.md +719 -0
- package/docs/conversations/README.md +168 -0
- package/docs/core/extending-praxis-core.md +604 -0
- package/docs/core/praxis-core-api.md +385 -0
- package/docs/decision-ledger/contract-index.json +2 -2
- package/docs/decision-ledger/decisions/2026-02-01-monorepo-organization.md +130 -0
- package/docs/examples/DOGFOODING_WORKFLOW_EXAMPLE.md +295 -0
- package/docs/examples/README.md +41 -0
- package/docs/workflows/pr-overlap-guard.md +50 -0
- package/package.json +7 -2
- package/src/__tests__/chronicle.test.ts +512 -0
- package/src/__tests__/conversations.test.ts +312 -0
- package/src/__tests__/edge-cases.test.ts +1 -1
- package/src/__tests__/engine-dx.test.ts +355 -0
- package/src/cli/commands/conversations.ts +252 -0
- package/src/cli/index.ts +73 -0
- package/src/conversations/README.md +230 -0
- package/src/conversations/candidate.schema.json +123 -0
- package/src/conversations/candidates.ts +114 -0
- package/src/conversations/capture.ts +56 -0
- package/src/conversations/classify.ts +110 -0
- package/src/conversations/conversation.schema.json +106 -0
- package/src/conversations/emitters/fs.ts +65 -0
- package/src/conversations/emitters/github.ts +115 -0
- package/src/conversations/gate.ts +102 -0
- package/src/conversations/index.ts +28 -0
- package/src/conversations/normalize.ts +51 -0
- package/src/conversations/redact.ts +57 -0
- package/src/conversations/types.ts +96 -0
- package/src/core/chronicle/chronicle.ts +227 -0
- package/src/core/chronicle/context.ts +80 -0
- package/src/core/chronicle/index.ts +53 -0
- package/src/core/chronicle/mcp.ts +135 -0
- package/src/core/chronicle/types.ts +61 -0
- package/src/core/engine.ts +99 -1
- package/src/core/pluresdb/index.ts +22 -0
- package/src/core/pluresdb/store.ts +162 -5
- package/src/core/rules.ts +12 -0
- package/src/dsl/index.ts +6 -0
- package/src/index.ts +18 -0
- package/src/integrations/pluresdb.ts +22 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chronicle Types
|
|
3
|
+
*
|
|
4
|
+
* Core types for the Chronicle causal tracking system.
|
|
5
|
+
* Records state transitions as a causal graph stored in PluresDB.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Direction for traversing causal chains.
|
|
10
|
+
*/
|
|
11
|
+
export type TraceDirection = 'backward' | 'forward' | 'both';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Causal relationship type between Chronicle nodes.
|
|
15
|
+
* - `causes`: node A caused node B (explicit causal link)
|
|
16
|
+
* - `context`: node B belongs to the same session/request as node A
|
|
17
|
+
* - `follows`: node B happened after node A in the same context
|
|
18
|
+
*/
|
|
19
|
+
export type EdgeType = 'causes' | 'context' | 'follows';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A recorded state transition event passed to Chronicle.
|
|
23
|
+
*/
|
|
24
|
+
export interface ChronicleEvent {
|
|
25
|
+
/** Path to the changed value (fact or event stream path) */
|
|
26
|
+
path: string;
|
|
27
|
+
/** Value before the change (undefined for creates) */
|
|
28
|
+
before?: unknown;
|
|
29
|
+
/** Value after the change */
|
|
30
|
+
after?: unknown;
|
|
31
|
+
/** Parent span/node ID that caused this change */
|
|
32
|
+
cause?: string;
|
|
33
|
+
/** Session or request ID grouping related changes */
|
|
34
|
+
context?: string;
|
|
35
|
+
/** Additional metadata key-value pairs */
|
|
36
|
+
metadata: Record<string, string>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A Chronicle node representing a single recorded state transition.
|
|
41
|
+
*/
|
|
42
|
+
export interface ChronicleNode {
|
|
43
|
+
/** Unique node ID: `chronos:{timestamp}-{counter}` */
|
|
44
|
+
id: string;
|
|
45
|
+
/** Timestamp (ms since epoch) when this node was recorded */
|
|
46
|
+
timestamp: number;
|
|
47
|
+
/** The recorded state transition */
|
|
48
|
+
event: ChronicleEvent;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A directed edge in the causal graph connecting two Chronicle nodes.
|
|
53
|
+
*/
|
|
54
|
+
export interface ChronicleEdge {
|
|
55
|
+
/** Source node ID */
|
|
56
|
+
from: string;
|
|
57
|
+
/** Target node ID */
|
|
58
|
+
to: string;
|
|
59
|
+
/** Type of causal relationship */
|
|
60
|
+
type: EdgeType;
|
|
61
|
+
}
|
package/src/core/engine.ts
CHANGED
|
@@ -28,6 +28,20 @@ export interface PraxisEngineOptions<TContext = unknown> {
|
|
|
28
28
|
initialFacts?: PraxisFact[];
|
|
29
29
|
/** Initial metadata (optional) */
|
|
30
30
|
initialMeta?: Record<string, unknown>;
|
|
31
|
+
/**
|
|
32
|
+
* Fact deduplication strategy (default: 'last-write-wins').
|
|
33
|
+
*
|
|
34
|
+
* - 'none': facts accumulate without dedup (original behavior)
|
|
35
|
+
* - 'last-write-wins': only keep the latest fact per tag (most common)
|
|
36
|
+
* - 'append': keep all facts but cap at maxFacts
|
|
37
|
+
*/
|
|
38
|
+
factDedup?: 'none' | 'last-write-wins' | 'append';
|
|
39
|
+
/**
|
|
40
|
+
* Maximum number of facts to retain (default: 1000).
|
|
41
|
+
* When exceeded, oldest facts are evicted (FIFO).
|
|
42
|
+
* Set to 0 for unlimited (not recommended).
|
|
43
|
+
*/
|
|
44
|
+
maxFacts?: number;
|
|
31
45
|
}
|
|
32
46
|
|
|
33
47
|
/**
|
|
@@ -69,9 +83,13 @@ function safeClone<T>(value: T): T {
|
|
|
69
83
|
export class LogicEngine<TContext = unknown> {
|
|
70
84
|
private state: PraxisState & { context: TContext };
|
|
71
85
|
private readonly registry: PraxisRegistry<TContext>;
|
|
86
|
+
private readonly factDedup: 'none' | 'last-write-wins' | 'append';
|
|
87
|
+
private readonly maxFacts: number;
|
|
72
88
|
|
|
73
89
|
constructor(options: PraxisEngineOptions<TContext>) {
|
|
74
90
|
this.registry = options.registry;
|
|
91
|
+
this.factDedup = options.factDedup ?? 'last-write-wins';
|
|
92
|
+
this.maxFacts = options.maxFacts ?? 1000;
|
|
75
93
|
this.state = {
|
|
76
94
|
context: options.initialContext,
|
|
77
95
|
facts: options.initialFacts ?? [],
|
|
@@ -134,6 +152,7 @@ export class LogicEngine<TContext = unknown> {
|
|
|
134
152
|
|
|
135
153
|
// Apply rules
|
|
136
154
|
const newFacts: PraxisFact[] = [];
|
|
155
|
+
const eventTags = new Set(events.map(e => e.tag));
|
|
137
156
|
for (const ruleId of config.ruleIds) {
|
|
138
157
|
const rule = this.registry.getRule(ruleId);
|
|
139
158
|
if (!rule) {
|
|
@@ -145,6 +164,15 @@ export class LogicEngine<TContext = unknown> {
|
|
|
145
164
|
continue;
|
|
146
165
|
}
|
|
147
166
|
|
|
167
|
+
// Event type filtering: if rule declares eventTypes, skip unless
|
|
168
|
+
// at least one event in the batch matches.
|
|
169
|
+
if (rule.eventTypes) {
|
|
170
|
+
const filterTags = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
|
|
171
|
+
if (!filterTags.some(t => eventTags.has(t))) {
|
|
172
|
+
continue; // No matching events — skip this rule
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
148
176
|
try {
|
|
149
177
|
const ruleFacts = rule.impl(newState, events);
|
|
150
178
|
newFacts.push(...ruleFacts);
|
|
@@ -157,10 +185,35 @@ export class LogicEngine<TContext = unknown> {
|
|
|
157
185
|
}
|
|
158
186
|
}
|
|
159
187
|
|
|
188
|
+
// Merge new facts with deduplication
|
|
189
|
+
let mergedFacts: PraxisFact[];
|
|
190
|
+
switch (this.factDedup) {
|
|
191
|
+
case 'last-write-wins': {
|
|
192
|
+
// Build a map keyed by tag — new facts overwrite old ones with same tag
|
|
193
|
+
const factMap = new Map<string, PraxisFact>();
|
|
194
|
+
for (const f of newState.facts) factMap.set(f.tag, f);
|
|
195
|
+
for (const f of newFacts) factMap.set(f.tag, f);
|
|
196
|
+
mergedFacts = Array.from(factMap.values());
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 'append':
|
|
200
|
+
mergedFacts = [...newState.facts, ...newFacts];
|
|
201
|
+
break;
|
|
202
|
+
case 'none':
|
|
203
|
+
default:
|
|
204
|
+
mergedFacts = [...newState.facts, ...newFacts];
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Enforce maxFacts limit (evict oldest first)
|
|
209
|
+
if (this.maxFacts > 0 && mergedFacts.length > this.maxFacts) {
|
|
210
|
+
mergedFacts = mergedFacts.slice(mergedFacts.length - this.maxFacts);
|
|
211
|
+
}
|
|
212
|
+
|
|
160
213
|
// Add new facts to state
|
|
161
214
|
newState = {
|
|
162
215
|
...newState,
|
|
163
|
-
facts:
|
|
216
|
+
facts: mergedFacts,
|
|
164
217
|
};
|
|
165
218
|
|
|
166
219
|
// Check constraints
|
|
@@ -221,6 +274,35 @@ export class LogicEngine<TContext = unknown> {
|
|
|
221
274
|
};
|
|
222
275
|
}
|
|
223
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Atomically update context AND process events in a single call.
|
|
279
|
+
*
|
|
280
|
+
* This avoids the fragile pattern of calling updateContext() then step()
|
|
281
|
+
* separately, where rules could see stale context if the ordering is wrong.
|
|
282
|
+
*
|
|
283
|
+
* @param updater Function that produces new context from old context
|
|
284
|
+
* @param events Events to process after context is updated
|
|
285
|
+
* @returns Result with new state and diagnostics
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* engine.stepWithContext(
|
|
289
|
+
* ctx => ({ ...ctx, sprintName: sprint.name, items: sprint.items }),
|
|
290
|
+
* [{ tag: 'sprint.update', payload: { name: sprint.name } }]
|
|
291
|
+
* );
|
|
292
|
+
*/
|
|
293
|
+
stepWithContext(
|
|
294
|
+
updater: (context: TContext) => TContext,
|
|
295
|
+
events: PraxisEvent[]
|
|
296
|
+
): PraxisStepResult {
|
|
297
|
+
// Update context first — rules see fresh data
|
|
298
|
+
this.state = {
|
|
299
|
+
...this.state,
|
|
300
|
+
context: updater(this.state.context),
|
|
301
|
+
};
|
|
302
|
+
// Then step with the now-current context
|
|
303
|
+
return this.step(events);
|
|
304
|
+
}
|
|
305
|
+
|
|
224
306
|
/**
|
|
225
307
|
* Add facts directly (for exceptional cases).
|
|
226
308
|
* Generally, facts should be added through rules.
|
|
@@ -234,6 +316,22 @@ export class LogicEngine<TContext = unknown> {
|
|
|
234
316
|
};
|
|
235
317
|
}
|
|
236
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Check all constraints without processing any events.
|
|
321
|
+
*
|
|
322
|
+
* Useful for validation-only scenarios (e.g., form validation,
|
|
323
|
+
* pre-save checks) where you want constraint diagnostics without
|
|
324
|
+
* triggering any rules.
|
|
325
|
+
*
|
|
326
|
+
* @returns Array of constraint violation diagnostics (empty = all passing)
|
|
327
|
+
*/
|
|
328
|
+
checkConstraints(): PraxisDiagnostics[] {
|
|
329
|
+
return this.stepWithConfig([], {
|
|
330
|
+
ruleIds: [],
|
|
331
|
+
constraintIds: this.registry.getConstraintIds(),
|
|
332
|
+
}).diagnostics;
|
|
333
|
+
}
|
|
334
|
+
|
|
237
335
|
/**
|
|
238
336
|
* Clear all facts
|
|
239
337
|
*/
|
|
@@ -32,3 +32,25 @@ export {
|
|
|
32
32
|
// Config Generator - Generate PluresDB config from schemas
|
|
33
33
|
export type { PluresDBGeneratorOptions, GeneratedPluresDBFile } from './generator.js';
|
|
34
34
|
export { PluresDBGenerator, createPluresDBGenerator } from './generator.js';
|
|
35
|
+
|
|
36
|
+
// Chronicle - Causal graph tracking for state transitions
|
|
37
|
+
export type {
|
|
38
|
+
TraceDirection,
|
|
39
|
+
EdgeType,
|
|
40
|
+
ChronicleEvent,
|
|
41
|
+
ChronicleNode,
|
|
42
|
+
ChronicleEdge,
|
|
43
|
+
Chronicle,
|
|
44
|
+
ChronosTraceParams,
|
|
45
|
+
ChronosSearchParams,
|
|
46
|
+
McpToolResult,
|
|
47
|
+
ChronosMcpTools,
|
|
48
|
+
} from '../chronicle/index.js';
|
|
49
|
+
export {
|
|
50
|
+
ChronicleContext,
|
|
51
|
+
PluresDbChronicle,
|
|
52
|
+
createChronicle,
|
|
53
|
+
CHRONICLE_PATHS,
|
|
54
|
+
createChronosMcpTools,
|
|
55
|
+
} from '../chronicle/index.js';
|
|
56
|
+
export type { ChronicleSpan } from '../chronicle/index.js';
|
|
@@ -11,6 +11,8 @@ declare const process: { env: { [key: string]: string | undefined } } | undefine
|
|
|
11
11
|
import type { PraxisDB, UnsubscribeFn } from './adapter.js';
|
|
12
12
|
import type { PraxisRegistry } from '../rules.js';
|
|
13
13
|
import type { PraxisFact, PraxisEvent, PraxisState } from '../protocol.js';
|
|
14
|
+
import type { Chronicle } from '../chronicle/chronicle.js';
|
|
15
|
+
import { ChronicleContext } from '../chronicle/context.js';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Key paths for Praxis data in PluresDB
|
|
@@ -110,6 +112,7 @@ export class PraxisDBStore<TContext = unknown> {
|
|
|
110
112
|
private subscriptions: UnsubscribeFn[] = [];
|
|
111
113
|
private factWatchers = new Map<string, Set<(facts: PraxisFact[]) => void>>();
|
|
112
114
|
private onRuleError: RuleErrorHandler;
|
|
115
|
+
private chronicle?: Chronicle;
|
|
113
116
|
|
|
114
117
|
constructor(options: PraxisDBStoreOptions<TContext> & { onRuleError?: RuleErrorHandler }) {
|
|
115
118
|
this.db = options.db;
|
|
@@ -118,6 +121,26 @@ export class PraxisDBStore<TContext = unknown> {
|
|
|
118
121
|
this.onRuleError = options.onRuleError ?? defaultErrorHandler;
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Attach a Chronicle observer to this store.
|
|
126
|
+
*
|
|
127
|
+
* Every subsequent `storeFact` and `appendEvent` call will be recorded as a
|
|
128
|
+
* causal graph node in PluresDB, enabling full observability for free.
|
|
129
|
+
*
|
|
130
|
+
* @param chronicle Chronicle implementation to attach
|
|
131
|
+
* @returns `this` for fluent chaining
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* const store = createPraxisDBStore(db, registry)
|
|
136
|
+
* .withChronicle(createChronicle(db));
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
withChronicle(chronicle: Chronicle): this {
|
|
140
|
+
this.chronicle = chronicle;
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
121
144
|
/**
|
|
122
145
|
* Store a fact in PluresDB
|
|
123
146
|
*
|
|
@@ -134,8 +157,37 @@ export class PraxisDBStore<TContext = unknown> {
|
|
|
134
157
|
throw new Error(`Constraint violation: ${constraintResult.errors.join(', ')}`);
|
|
135
158
|
}
|
|
136
159
|
|
|
160
|
+
// Capture before state for Chronicle (best-effort; ignore errors)
|
|
161
|
+
let before: unknown;
|
|
162
|
+
if (this.chronicle) {
|
|
163
|
+
const payload = fact.payload as Record<string, unknown> | undefined;
|
|
164
|
+
const id = payload?.id as string | undefined;
|
|
165
|
+
if (id) {
|
|
166
|
+
before = await this.getFact(fact.tag, id);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
137
170
|
await this.persistFact(fact);
|
|
138
171
|
|
|
172
|
+
// Record state transition in Chronicle
|
|
173
|
+
if (this.chronicle) {
|
|
174
|
+
const payload = fact.payload as Record<string, unknown> | undefined;
|
|
175
|
+
const id = (payload?.id as string | undefined) ?? '';
|
|
176
|
+
const span = ChronicleContext.current;
|
|
177
|
+
try {
|
|
178
|
+
await this.chronicle.record({
|
|
179
|
+
path: getFactPath(fact.tag, id),
|
|
180
|
+
before,
|
|
181
|
+
after: fact,
|
|
182
|
+
cause: span?.spanId,
|
|
183
|
+
context: span?.contextId,
|
|
184
|
+
metadata: { factTag: fact.tag, operation: 'storeFact' },
|
|
185
|
+
});
|
|
186
|
+
} catch {
|
|
187
|
+
// Chronicle errors must never fail the primary operation
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
139
191
|
// Trigger rule evaluation - facts stored directly may trigger derived computations
|
|
140
192
|
await this.triggerRules([fact]);
|
|
141
193
|
}
|
|
@@ -153,7 +205,36 @@ export class PraxisDBStore<TContext = unknown> {
|
|
|
153
205
|
}
|
|
154
206
|
|
|
155
207
|
for (const fact of facts) {
|
|
208
|
+
// Capture before state for Chronicle (best-effort)
|
|
209
|
+
let before: unknown;
|
|
210
|
+
if (this.chronicle) {
|
|
211
|
+
const payload = fact.payload as Record<string, unknown> | undefined;
|
|
212
|
+
const id = payload?.id as string | undefined;
|
|
213
|
+
if (id) {
|
|
214
|
+
before = await this.getFact(fact.tag, id);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
156
218
|
await this.persistFact(fact);
|
|
219
|
+
|
|
220
|
+
// Record state transition in Chronicle
|
|
221
|
+
if (this.chronicle) {
|
|
222
|
+
const payload = fact.payload as Record<string, unknown> | undefined;
|
|
223
|
+
const id = (payload?.id as string | undefined) ?? '';
|
|
224
|
+
const span = ChronicleContext.current;
|
|
225
|
+
try {
|
|
226
|
+
await this.chronicle.record({
|
|
227
|
+
path: getFactPath(fact.tag, id),
|
|
228
|
+
before,
|
|
229
|
+
after: fact,
|
|
230
|
+
cause: span?.spanId,
|
|
231
|
+
context: span?.contextId,
|
|
232
|
+
metadata: { factTag: fact.tag, operation: 'storeFacts' },
|
|
233
|
+
});
|
|
234
|
+
} catch {
|
|
235
|
+
// Chronicle errors must never fail the primary operation
|
|
236
|
+
}
|
|
237
|
+
}
|
|
157
238
|
}
|
|
158
239
|
|
|
159
240
|
// Trigger rule evaluation
|
|
@@ -207,8 +288,37 @@ export class PraxisDBStore<TContext = unknown> {
|
|
|
207
288
|
const newEvents = [...existingEvents, entry];
|
|
208
289
|
await this.db.set(path, newEvents);
|
|
209
290
|
|
|
210
|
-
//
|
|
211
|
-
|
|
291
|
+
// Record event in Chronicle and capture node ID so derived facts can link back to it
|
|
292
|
+
let eventNodeId: string | undefined;
|
|
293
|
+
if (this.chronicle) {
|
|
294
|
+
const span = ChronicleContext.current;
|
|
295
|
+
try {
|
|
296
|
+
const node = await this.chronicle.record({
|
|
297
|
+
path,
|
|
298
|
+
before: existingEvents.length > 0 ? existingEvents[existingEvents.length - 1] : undefined,
|
|
299
|
+
after: entry,
|
|
300
|
+
cause: span?.spanId,
|
|
301
|
+
context: span?.contextId,
|
|
302
|
+
metadata: { eventTag: event.tag, sequence: String(entry.sequence), operation: 'appendEvent' },
|
|
303
|
+
});
|
|
304
|
+
eventNodeId = node.id;
|
|
305
|
+
} catch {
|
|
306
|
+
// Chronicle errors must never fail the primary operation
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Trigger rules within a causal span attributed to this event node,
|
|
311
|
+
// so any derived facts automatically link back to this event via 'causes' edges.
|
|
312
|
+
const outerSpan = ChronicleContext.current;
|
|
313
|
+
const ruleSpan = eventNodeId
|
|
314
|
+
? { spanId: eventNodeId, contextId: outerSpan?.contextId }
|
|
315
|
+
: outerSpan;
|
|
316
|
+
|
|
317
|
+
if (ruleSpan && this.chronicle) {
|
|
318
|
+
await ChronicleContext.runAsync(ruleSpan, () => this.triggerRulesForEvents([event]));
|
|
319
|
+
} else {
|
|
320
|
+
await this.triggerRulesForEvents([event]);
|
|
321
|
+
}
|
|
212
322
|
}
|
|
213
323
|
|
|
214
324
|
/**
|
|
@@ -224,7 +334,8 @@ export class PraxisDBStore<TContext = unknown> {
|
|
|
224
334
|
eventsByTag.set(event.tag, [...existing, event]);
|
|
225
335
|
}
|
|
226
336
|
|
|
227
|
-
// Append each group
|
|
337
|
+
// Append each group and collect the last recorded event node ID for causal linking
|
|
338
|
+
let lastEventNodeId: string | undefined;
|
|
228
339
|
for (const [tag, tagEvents] of eventsByTag) {
|
|
229
340
|
const path = getEventPath(tag);
|
|
230
341
|
const existingEvents = (await this.db.get<EventStreamEntry[]>(path)) ?? [];
|
|
@@ -237,10 +348,38 @@ export class PraxisDBStore<TContext = unknown> {
|
|
|
237
348
|
}));
|
|
238
349
|
|
|
239
350
|
await this.db.set(path, [...existingEvents, ...newEntries]);
|
|
351
|
+
|
|
352
|
+
// Record each appended event in Chronicle
|
|
353
|
+
if (this.chronicle) {
|
|
354
|
+
const span = ChronicleContext.current;
|
|
355
|
+
for (const entry of newEntries) {
|
|
356
|
+
try {
|
|
357
|
+
const node = await this.chronicle.record({
|
|
358
|
+
path,
|
|
359
|
+
after: entry,
|
|
360
|
+
cause: span?.spanId,
|
|
361
|
+
context: span?.contextId,
|
|
362
|
+
metadata: { eventTag: tag, sequence: String(entry.sequence), operation: 'appendEvents' },
|
|
363
|
+
});
|
|
364
|
+
lastEventNodeId = node.id;
|
|
365
|
+
} catch {
|
|
366
|
+
// Chronicle errors must never fail the primary operation
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
240
370
|
}
|
|
241
371
|
|
|
242
|
-
// Trigger rules
|
|
243
|
-
|
|
372
|
+
// Trigger rules in a causal span attributed to the last event node
|
|
373
|
+
const outerSpan = ChronicleContext.current;
|
|
374
|
+
const ruleSpan = lastEventNodeId
|
|
375
|
+
? { spanId: lastEventNodeId, contextId: outerSpan?.contextId }
|
|
376
|
+
: outerSpan;
|
|
377
|
+
|
|
378
|
+
if (ruleSpan && this.chronicle) {
|
|
379
|
+
await ChronicleContext.runAsync(ruleSpan, () => this.triggerRulesForEvents(events));
|
|
380
|
+
} else {
|
|
381
|
+
await this.triggerRulesForEvents(events);
|
|
382
|
+
}
|
|
244
383
|
}
|
|
245
384
|
|
|
246
385
|
/**
|
|
@@ -386,6 +525,24 @@ export class PraxisDBStore<TContext = unknown> {
|
|
|
386
525
|
if (constraintResult.valid) {
|
|
387
526
|
for (const fact of derivedFacts) {
|
|
388
527
|
await this.persistFact(fact);
|
|
528
|
+
|
|
529
|
+
// Record derived fact in Chronicle using the current causal span
|
|
530
|
+
if (this.chronicle) {
|
|
531
|
+
const payload = fact.payload as Record<string, unknown> | undefined;
|
|
532
|
+
const id = (payload?.id as string | undefined) ?? '';
|
|
533
|
+
const span = ChronicleContext.current;
|
|
534
|
+
try {
|
|
535
|
+
await this.chronicle.record({
|
|
536
|
+
path: getFactPath(fact.tag, id),
|
|
537
|
+
after: fact,
|
|
538
|
+
cause: span?.spanId,
|
|
539
|
+
context: span?.contextId,
|
|
540
|
+
metadata: { factTag: fact.tag, operation: 'derivedFact' },
|
|
541
|
+
});
|
|
542
|
+
} catch {
|
|
543
|
+
// Chronicle errors must never fail the primary operation
|
|
544
|
+
}
|
|
545
|
+
}
|
|
389
546
|
}
|
|
390
547
|
}
|
|
391
548
|
}
|
package/src/core/rules.ts
CHANGED
|
@@ -61,6 +61,18 @@ export interface RuleDescriptor<TContext = unknown> {
|
|
|
61
61
|
description: string;
|
|
62
62
|
/** Implementation function */
|
|
63
63
|
impl: RuleFn<TContext>;
|
|
64
|
+
/**
|
|
65
|
+
* Optional event type filter — only evaluate this rule when at least one
|
|
66
|
+
* event in the batch has a matching `tag`. When omitted, the rule runs on
|
|
67
|
+
* every step (catch-all).
|
|
68
|
+
*
|
|
69
|
+
* Accepts a single tag string or an array of tags.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* { id: 'sprint-behind', eventTypes: ['sprint.update'], impl: ... }
|
|
73
|
+
* { id: 'note-check', eventTypes: 'note.update', impl: ... }
|
|
74
|
+
*/
|
|
75
|
+
eventTypes?: string | string[];
|
|
64
76
|
/** Optional contract for rule behavior */
|
|
65
77
|
contract?: Contract;
|
|
66
78
|
/** Optional metadata */
|
package/src/dsl/index.ts
CHANGED
|
@@ -85,6 +85,11 @@ export interface DefineRuleOptions<TContext = unknown> {
|
|
|
85
85
|
id: string;
|
|
86
86
|
description: string;
|
|
87
87
|
impl: RuleFn<TContext>;
|
|
88
|
+
/**
|
|
89
|
+
* Optional event type filter — only evaluate this rule when at least one
|
|
90
|
+
* event in the batch has a matching `tag`. Accepts a single tag or array.
|
|
91
|
+
*/
|
|
92
|
+
eventTypes?: string | string[];
|
|
88
93
|
contract?: Contract;
|
|
89
94
|
meta?: Record<string, unknown>;
|
|
90
95
|
}
|
|
@@ -115,6 +120,7 @@ export function defineRule<TContext = unknown>(
|
|
|
115
120
|
id: options.id,
|
|
116
121
|
description: options.description,
|
|
117
122
|
impl: options.impl,
|
|
123
|
+
eventTypes: options.eventTypes,
|
|
118
124
|
contract,
|
|
119
125
|
meta,
|
|
120
126
|
};
|
package/src/index.ts
CHANGED
|
@@ -208,6 +208,18 @@ export type {
|
|
|
208
208
|
GeneratedPluresDBFile,
|
|
209
209
|
PluresDBAdapter,
|
|
210
210
|
PluresDBAdapterOptions,
|
|
211
|
+
// Chronicle
|
|
212
|
+
TraceDirection,
|
|
213
|
+
EdgeType,
|
|
214
|
+
ChronicleEvent,
|
|
215
|
+
ChronicleNode,
|
|
216
|
+
ChronicleEdge,
|
|
217
|
+
Chronicle,
|
|
218
|
+
ChronicleSpan,
|
|
219
|
+
ChronosTraceParams,
|
|
220
|
+
ChronosSearchParams,
|
|
221
|
+
McpToolResult,
|
|
222
|
+
ChronosMcpTools,
|
|
211
223
|
} from './integrations/pluresdb.js';
|
|
212
224
|
export {
|
|
213
225
|
InMemoryPraxisDB,
|
|
@@ -229,6 +241,12 @@ export {
|
|
|
229
241
|
createPluresDBGenerator,
|
|
230
242
|
createPluresDBAdapter,
|
|
231
243
|
attachToEngine,
|
|
244
|
+
// Chronicle
|
|
245
|
+
ChronicleContext,
|
|
246
|
+
PluresDbChronicle,
|
|
247
|
+
createChronicle,
|
|
248
|
+
CHRONICLE_PATHS,
|
|
249
|
+
createChronosMcpTools,
|
|
232
250
|
} from './integrations/pluresdb.js';
|
|
233
251
|
|
|
234
252
|
// Unum Integration (Identity & Channels)
|
|
@@ -57,6 +57,28 @@ export type {
|
|
|
57
57
|
GeneratedPluresDBFile,
|
|
58
58
|
} from '../core/pluresdb/generator.js';
|
|
59
59
|
|
|
60
|
+
// Chronicle - Causal graph tracking for Praxis state transitions
|
|
61
|
+
export type {
|
|
62
|
+
TraceDirection,
|
|
63
|
+
EdgeType,
|
|
64
|
+
ChronicleEvent,
|
|
65
|
+
ChronicleNode,
|
|
66
|
+
ChronicleEdge,
|
|
67
|
+
Chronicle,
|
|
68
|
+
ChronicleSpan,
|
|
69
|
+
ChronosTraceParams,
|
|
70
|
+
ChronosSearchParams,
|
|
71
|
+
McpToolResult,
|
|
72
|
+
ChronosMcpTools,
|
|
73
|
+
} from '../core/chronicle/index.js';
|
|
74
|
+
export {
|
|
75
|
+
ChronicleContext,
|
|
76
|
+
PluresDbChronicle,
|
|
77
|
+
createChronicle,
|
|
78
|
+
CHRONICLE_PATHS,
|
|
79
|
+
createChronosMcpTools,
|
|
80
|
+
} from '../core/chronicle/index.js';
|
|
81
|
+
|
|
60
82
|
/**
|
|
61
83
|
* PluresDB adapter interface for engine integration
|
|
62
84
|
*
|