@plures/praxis 1.2.12 → 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.
Files changed (85) hide show
  1. package/README.md +63 -0
  2. package/dist/browser/{chunk-VOMLVI6V.js → chunk-BBP2F7TT.js} +70 -1
  3. package/dist/browser/{chunk-K377RW4V.js → chunk-FCEH7WMH.js} +1 -1
  4. package/dist/browser/{engine-YJZV4SLD.js → engine-65QDGCAN.js} +1 -1
  5. package/dist/browser/index.d.ts +104 -2
  6. package/dist/browser/index.js +181 -5
  7. package/dist/browser/integrations/svelte.d.ts +2 -2
  8. package/dist/browser/integrations/svelte.js +2 -2
  9. package/dist/browser/{reactive-engine.svelte-9aS0kTa8.d.ts → reactive-engine.svelte-Cqd8Mod2.d.ts} +56 -1
  10. package/dist/node/{chunk-PRPQO6R5.js → chunk-32YFEEML.js} +1 -1
  11. package/dist/node/{chunk-VOMLVI6V.js → chunk-BBP2F7TT.js} +70 -1
  12. package/dist/node/{chunk-5RH7UAQC.js → chunk-PTH6MD6P.js} +1 -0
  13. package/dist/node/cli/index.cjs +1553 -839
  14. package/dist/node/cli/index.js +39 -2
  15. package/dist/node/cloud/index.d.cts +1 -1
  16. package/dist/node/cloud/index.d.ts +1 -1
  17. package/dist/node/components/index.d.cts +2 -2
  18. package/dist/node/components/index.d.ts +2 -2
  19. package/dist/node/conversations-KQBXTP3N.js +596 -0
  20. package/dist/node/{engine-2DQBKBJC.js → engine-7CXQV6RC.js} +1 -1
  21. package/dist/node/index.cjs +408 -3
  22. package/dist/node/index.d.cts +308 -7
  23. package/dist/node/index.d.ts +308 -7
  24. package/dist/node/index.js +336 -6
  25. package/dist/node/integrations/svelte.cjs +70 -1
  26. package/dist/node/integrations/svelte.d.cts +3 -3
  27. package/dist/node/integrations/svelte.d.ts +3 -3
  28. package/dist/node/integrations/svelte.js +2 -2
  29. package/dist/node/{protocol-Qek7ebBl.d.ts → protocol-BocKczNv.d.cts} +1 -1
  30. package/dist/node/{protocol-Qek7ebBl.d.cts → protocol-BocKczNv.d.ts} +1 -1
  31. package/dist/node/{reactive-engine.svelte-CRNqHlbv.d.ts → reactive-engine.svelte-CGe8SpVE.d.cts} +57 -2
  32. package/dist/node/{reactive-engine.svelte-BFIZfawz.d.cts → reactive-engine.svelte-D-xTDxT5.d.ts} +57 -2
  33. package/dist/node/{terminal-adapter-B-UK_Vdz.d.ts → terminal-adapter-CvIvgTo4.d.ts} +1 -1
  34. package/dist/node/{terminal-adapter-BQSIF5bf.d.cts → terminal-adapter-Db-snPJ3.d.cts} +1 -1
  35. package/dist/node/{validate-CNHUULQE.js → validate-EN3M4FUR.js} +1 -1
  36. package/dist/node/{verify-KLJRXVJS.js → verify-7VZRP2WS.js} +2 -2
  37. package/docs/BOT_UPDATE_POLICY.md +125 -0
  38. package/docs/DOGFOODING_CHECKLIST.md +254 -0
  39. package/docs/DOGFOODING_INDEX.md +169 -0
  40. package/docs/DOGFOODING_QUICK_START.md +140 -0
  41. package/docs/KNO_ENG_EXTRACTION_PLAN.md +577 -0
  42. package/docs/PLURES_TOOLS_INVENTORY.md +170 -0
  43. package/docs/README.md +12 -0
  44. package/docs/TESTING_BOT_WORKFLOWS.md +154 -0
  45. package/docs/conversations/INTEGRATION_POINTS.md +719 -0
  46. package/docs/conversations/README.md +168 -0
  47. package/docs/core/extending-praxis-core.md +604 -0
  48. package/docs/core/praxis-core-api.md +385 -0
  49. package/docs/decision-ledger/contract-index.json +2 -2
  50. package/docs/decision-ledger/decisions/2026-02-01-monorepo-organization.md +130 -0
  51. package/docs/examples/DOGFOODING_WORKFLOW_EXAMPLE.md +295 -0
  52. package/docs/examples/README.md +41 -0
  53. package/docs/workflows/pr-overlap-guard.md +50 -0
  54. package/package.json +7 -2
  55. package/src/__tests__/chronicle.test.ts +512 -0
  56. package/src/__tests__/conversations.test.ts +312 -0
  57. package/src/__tests__/edge-cases.test.ts +1 -1
  58. package/src/__tests__/engine-dx.test.ts +355 -0
  59. package/src/cli/commands/conversations.ts +252 -0
  60. package/src/cli/index.ts +73 -0
  61. package/src/conversations/README.md +230 -0
  62. package/src/conversations/candidate.schema.json +123 -0
  63. package/src/conversations/candidates.ts +114 -0
  64. package/src/conversations/capture.ts +56 -0
  65. package/src/conversations/classify.ts +110 -0
  66. package/src/conversations/conversation.schema.json +106 -0
  67. package/src/conversations/emitters/fs.ts +65 -0
  68. package/src/conversations/emitters/github.ts +115 -0
  69. package/src/conversations/gate.ts +102 -0
  70. package/src/conversations/index.ts +28 -0
  71. package/src/conversations/normalize.ts +51 -0
  72. package/src/conversations/redact.ts +57 -0
  73. package/src/conversations/types.ts +96 -0
  74. package/src/core/chronicle/chronicle.ts +227 -0
  75. package/src/core/chronicle/context.ts +80 -0
  76. package/src/core/chronicle/index.ts +53 -0
  77. package/src/core/chronicle/mcp.ts +135 -0
  78. package/src/core/chronicle/types.ts +61 -0
  79. package/src/core/engine.ts +99 -1
  80. package/src/core/pluresdb/index.ts +22 -0
  81. package/src/core/pluresdb/store.ts +162 -5
  82. package/src/core/rules.ts +12 -0
  83. package/src/dsl/index.ts +6 -0
  84. package/src/index.ts +18 -0
  85. package/src/integrations/pluresdb.ts +22 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Normalization module for praxis-conversations
3
+ * Handles normalization of conversation content (deterministic)
4
+ */
5
+
6
+ import type { Conversation } from './types.js';
7
+
8
+ /**
9
+ * Normalize whitespace in text
10
+ */
11
+ function normalizeWhitespace(text: string): string {
12
+ return text
13
+ .replace(/\r\n/g, '\n') // Normalize line endings
14
+ .replace(/\t/g, ' ') // Replace tabs with spaces
15
+ .replace(/\n{3,}/g, '\n\n') // Collapse multiple newlines
16
+ .trim();
17
+ }
18
+
19
+ /**
20
+ * Normalize code blocks
21
+ */
22
+ function normalizeCodeBlocks(text: string): string {
23
+ // Ensure code blocks have consistent formatting
24
+ return text.replace(/```(\w+)?\n/g, (_match, lang) => {
25
+ return lang ? `\`\`\`${lang.toLowerCase()}\n` : '```\n';
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Normalize a single conversation turn
31
+ */
32
+ function normalizeTurn(content: string): string {
33
+ let normalized = content;
34
+ normalized = normalizeWhitespace(normalized);
35
+ normalized = normalizeCodeBlocks(normalized);
36
+ return normalized;
37
+ }
38
+
39
+ /**
40
+ * Normalize a conversation
41
+ */
42
+ export function normalizeConversation(conversation: Conversation): Conversation {
43
+ return {
44
+ ...conversation,
45
+ turns: conversation.turns.map(turn => ({
46
+ ...turn,
47
+ content: normalizeTurn(turn.content),
48
+ })),
49
+ normalized: true,
50
+ };
51
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Redaction module for praxis-conversations
3
+ * Handles PII redaction from conversations (deterministic patterns only)
4
+ */
5
+
6
+ import type { Conversation } from './types.js';
7
+
8
+ /**
9
+ * Deterministic PII patterns to redact
10
+ * Note: These patterns prioritize safety over precision
11
+ * - IP pattern: matches sequences like XXX.XXX.XXX.XXX (may match invalid IPs)
12
+ * - Card pattern: matches 16-digit sequences (does not validate with Luhn algorithm)
13
+ */
14
+ const PII_PATTERNS = [
15
+ // Email addresses
16
+ { pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, replacement: '[EMAIL_REDACTED]' },
17
+ // Phone numbers (simple patterns)
18
+ { pattern: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, replacement: '[PHONE_REDACTED]' },
19
+ // Credit card numbers (basic pattern - intentionally broad for safety)
20
+ { pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, replacement: '[CARD_REDACTED]' },
21
+ // SSN pattern
22
+ { pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replacement: '[SSN_REDACTED]' },
23
+ // IP addresses (basic pattern - may match invalid IPs like 999.999.999.999)
24
+ { pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, replacement: '[IP_REDACTED]' },
25
+ ];
26
+
27
+ /**
28
+ * Redact PII from a text string using deterministic patterns
29
+ */
30
+ export function redactText(text: string): string {
31
+ let redacted = text;
32
+ for (const { pattern, replacement } of PII_PATTERNS) {
33
+ redacted = redacted.replace(pattern, replacement);
34
+ }
35
+ return redacted;
36
+ }
37
+
38
+ /**
39
+ * Redact PII from a conversation
40
+ */
41
+ export function redactConversation(conversation: Conversation): Conversation {
42
+ return {
43
+ ...conversation,
44
+ turns: conversation.turns.map(turn => ({
45
+ ...turn,
46
+ content: redactText(turn.content),
47
+ })),
48
+ metadata: {
49
+ ...conversation.metadata,
50
+ // Redact userId if it looks like an email
51
+ userId: conversation.metadata.userId
52
+ ? redactText(conversation.metadata.userId)
53
+ : undefined,
54
+ },
55
+ redacted: true,
56
+ };
57
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * TypeScript types for the praxis-conversations subsystem
3
+ */
4
+
5
+ export interface ConversationTurn {
6
+ role: 'user' | 'assistant' | 'system';
7
+ content: string;
8
+ timestamp: string;
9
+ metadata?: Record<string, unknown>;
10
+ }
11
+
12
+ export interface ConversationMetadata {
13
+ source?: string;
14
+ userId?: string;
15
+ sessionId?: string;
16
+ tags?: string[];
17
+ }
18
+
19
+ export interface Classification {
20
+ category?: string;
21
+ confidence?: number;
22
+ tags?: string[];
23
+ }
24
+
25
+ export interface Conversation {
26
+ id: string;
27
+ timestamp: string;
28
+ turns: ConversationTurn[];
29
+ metadata: ConversationMetadata;
30
+ redacted?: boolean;
31
+ normalized?: boolean;
32
+ classified?: boolean;
33
+ classification?: Classification;
34
+ }
35
+
36
+ export interface GateResult {
37
+ name: string;
38
+ passed: boolean;
39
+ message?: string;
40
+ }
41
+
42
+ export interface GateStatus {
43
+ passed: boolean;
44
+ reason?: string;
45
+ gates?: GateResult[];
46
+ }
47
+
48
+ export interface EmissionResult {
49
+ success: boolean;
50
+ timestamp: string;
51
+ externalId?: string;
52
+ error?: string;
53
+ }
54
+
55
+ export interface CandidateMetadata {
56
+ priority?: 'low' | 'medium' | 'high' | 'critical';
57
+ labels?: string[];
58
+ assignees?: string[];
59
+ source?: {
60
+ conversationId: string;
61
+ timestamp: string;
62
+ };
63
+ }
64
+
65
+ export interface Candidate {
66
+ id: string;
67
+ conversationId: string;
68
+ type: 'github-issue' | 'github-pr' | 'documentation' | 'feature-request' | 'bug-report';
69
+ title: string;
70
+ body: string;
71
+ metadata: CandidateMetadata;
72
+ gateStatus?: GateStatus;
73
+ emitted?: boolean;
74
+ emissionResult?: EmissionResult;
75
+ }
76
+
77
+ export interface PipelineOptions {
78
+ skipRedaction?: boolean;
79
+ skipNormalization?: boolean;
80
+ skipClassification?: boolean;
81
+ }
82
+
83
+ export interface EmitterOptions {
84
+ dryRun?: boolean;
85
+ commitIntent?: boolean;
86
+ }
87
+
88
+ export interface FSEmitterOptions extends EmitterOptions {
89
+ outputDir: string;
90
+ }
91
+
92
+ export interface GitHubEmitterOptions extends EmitterOptions {
93
+ owner: string;
94
+ repo: string;
95
+ token?: string;
96
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Chronicle Interface and PluresDbChronicle Implementation
3
+ *
4
+ * Records state transitions as a causal graph in PluresDB.
5
+ * Attached to PraxisDBStore via `.withChronicle()` for zero-effort observability.
6
+ */
7
+
8
+ import type { PraxisDB } from '../pluresdb/adapter.js';
9
+ import type { ChronicleEvent, ChronicleEdge, ChronicleNode, EdgeType, TraceDirection } from './types.js';
10
+
11
+ /**
12
+ * Storage path constants for Chronicle data in PluresDB.
13
+ *
14
+ * Layout:
15
+ * - `/_praxis/chronos/nodes/{nodeId}` — ChronicleNode documents
16
+ * - `/_praxis/chronos/edges/out/{nodeId}` — outgoing ChronicleEdge arrays
17
+ * - `/_praxis/chronos/edges/in/{nodeId}` — incoming ChronicleEdge arrays
18
+ * - `/_praxis/chronos/context/{contextId}` — ordered nodeId arrays per context
19
+ * - `/_praxis/chronos/index` — global ordered nodeId array (for range queries)
20
+ */
21
+ export const CHRONICLE_PATHS = {
22
+ BASE: '/_praxis/chronos',
23
+ NODES: '/_praxis/chronos/nodes',
24
+ EDGES_OUT: '/_praxis/chronos/edges/out',
25
+ EDGES_IN: '/_praxis/chronos/edges/in',
26
+ CONTEXT: '/_praxis/chronos/context',
27
+ INDEX: '/_praxis/chronos/index',
28
+ } as const;
29
+
30
+ /**
31
+ * Chronicle interface — records state transitions as a causal graph.
32
+ *
33
+ * Automatically attached to any PraxisDBStore at runtime via `.withChronicle()`.
34
+ * Records state diffs as graph nodes with causal edges.
35
+ */
36
+ export interface Chronicle {
37
+ /**
38
+ * Record a state transition and return the created node.
39
+ */
40
+ record(event: ChronicleEvent): Promise<ChronicleNode>;
41
+
42
+ /**
43
+ * Trace causality backward or forward from a node.
44
+ *
45
+ * @param nodeId Starting node ID
46
+ * @param direction `'backward'` follows incoming edges, `'forward'` follows outgoing edges
47
+ * @param maxDepth Maximum traversal depth (prevents cycles / infinite loops)
48
+ */
49
+ trace(nodeId: string, direction: TraceDirection, maxDepth: number): Promise<ChronicleNode[]>;
50
+
51
+ /**
52
+ * Return all Chronicle nodes recorded within a timestamp range.
53
+ *
54
+ * @param start Inclusive start timestamp (ms)
55
+ * @param end Inclusive end timestamp (ms)
56
+ */
57
+ range(start: number, end: number): Promise<ChronicleNode[]>;
58
+
59
+ /**
60
+ * Return all Chronicle nodes belonging to a context (session/request).
61
+ *
62
+ * @param contextId The context identifier
63
+ */
64
+ subgraph(contextId: string): Promise<ChronicleNode[]>;
65
+ }
66
+
67
+ /**
68
+ * Monotonically-increasing counter for unique node IDs within a process.
69
+ *
70
+ * Node.js is single-threaded: the pre-increment here is atomic within a
71
+ * turn of the event loop, so this counter produces unique IDs for all
72
+ * concurrent async operations within a single process.
73
+ */
74
+ let _nodeCounter = 0;
75
+
76
+ /**
77
+ * PluresDB-backed implementation of the Chronicle interface.
78
+ *
79
+ * Stores causal graph nodes and edges in PluresDB under `/_praxis/chronos/`.
80
+ * Shares the same PluresDB instance as PraxisDBStore so the JS Chronos UI
81
+ * can read from the same data layer.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * const db = createInMemoryDB();
86
+ * const chronicle = new PluresDbChronicle(db);
87
+ *
88
+ * const store = createPraxisDBStore(db, registry).withChronicle(chronicle);
89
+ * // All storeFact / appendEvent calls are now recorded automatically.
90
+ * ```
91
+ */
92
+ export class PluresDbChronicle implements Chronicle {
93
+ private readonly db: PraxisDB;
94
+
95
+ constructor(db: PraxisDB) {
96
+ this.db = db;
97
+ }
98
+
99
+ async record(event: ChronicleEvent): Promise<ChronicleNode> {
100
+ const timestamp = Date.now();
101
+ const id = `chronos:${timestamp}-${++_nodeCounter}`;
102
+
103
+ const node: ChronicleNode = { id, timestamp, event };
104
+
105
+ // Persist the node document
106
+ await this.db.set(`${CHRONICLE_PATHS.NODES}/${id}`, node);
107
+
108
+ // Update global time-ordered index
109
+ const index = (await this.db.get<string[]>(CHRONICLE_PATHS.INDEX)) ?? [];
110
+ await this.db.set(CHRONICLE_PATHS.INDEX, [...index, id]);
111
+
112
+ // If this change was caused by a known span, create a 'causes' edge
113
+ if (event.cause) {
114
+ await this.addEdge(event.cause, id, 'causes');
115
+ }
116
+
117
+ // If this change belongs to a context, maintain per-context ordering
118
+ if (event.context) {
119
+ const contextPath = `${CHRONICLE_PATHS.CONTEXT}/${event.context}`;
120
+ const contextNodes = (await this.db.get<string[]>(contextPath)) ?? [];
121
+
122
+ // Link to the previous node in the same context with a 'follows' edge
123
+ if (contextNodes.length > 0) {
124
+ const prevId = contextNodes[contextNodes.length - 1]!;
125
+ await this.addEdge(prevId, id, 'follows');
126
+ }
127
+
128
+ await this.db.set(contextPath, [...contextNodes, id]);
129
+ }
130
+
131
+ return node;
132
+ }
133
+
134
+ async trace(nodeId: string, direction: TraceDirection, maxDepth: number): Promise<ChronicleNode[]> {
135
+ const visited = new Set<string>();
136
+ const result: ChronicleNode[] = [];
137
+ await this._traceRecursive(nodeId, direction, maxDepth, 0, visited, result);
138
+ return result;
139
+ }
140
+
141
+ async range(start: number, end: number): Promise<ChronicleNode[]> {
142
+ const index = (await this.db.get<string[]>(CHRONICLE_PATHS.INDEX)) ?? [];
143
+ const result: ChronicleNode[] = [];
144
+
145
+ for (const id of index) {
146
+ const node = await this.db.get<ChronicleNode>(`${CHRONICLE_PATHS.NODES}/${id}`);
147
+ if (node && node.timestamp >= start && node.timestamp <= end) {
148
+ result.push(node);
149
+ }
150
+ }
151
+
152
+ return result;
153
+ }
154
+
155
+ async subgraph(contextId: string): Promise<ChronicleNode[]> {
156
+ const contextPath = `${CHRONICLE_PATHS.CONTEXT}/${contextId}`;
157
+ const nodeIds = (await this.db.get<string[]>(contextPath)) ?? [];
158
+ const result: ChronicleNode[] = [];
159
+
160
+ for (const id of nodeIds) {
161
+ const node = await this.db.get<ChronicleNode>(`${CHRONICLE_PATHS.NODES}/${id}`);
162
+ if (node) {
163
+ result.push(node);
164
+ }
165
+ }
166
+
167
+ return result;
168
+ }
169
+
170
+ // ── Internal helpers ──────────────────────────────────────────────────────
171
+
172
+ private async addEdge(from: string, to: string, type: EdgeType): Promise<void> {
173
+ const edge: ChronicleEdge = { from, to, type };
174
+
175
+ const outPath = `${CHRONICLE_PATHS.EDGES_OUT}/${from}`;
176
+ const outEdges = (await this.db.get<ChronicleEdge[]>(outPath)) ?? [];
177
+ await this.db.set(outPath, [...outEdges, edge]);
178
+
179
+ const inPath = `${CHRONICLE_PATHS.EDGES_IN}/${to}`;
180
+ const inEdges = (await this.db.get<ChronicleEdge[]>(inPath)) ?? [];
181
+ await this.db.set(inPath, [...inEdges, edge]);
182
+ }
183
+
184
+ private async _traceRecursive(
185
+ nodeId: string,
186
+ direction: TraceDirection,
187
+ maxDepth: number,
188
+ depth: number,
189
+ visited: Set<string>,
190
+ result: ChronicleNode[]
191
+ ): Promise<void> {
192
+ if (depth > maxDepth || visited.has(nodeId)) {
193
+ return;
194
+ }
195
+ visited.add(nodeId);
196
+
197
+ const node = await this.db.get<ChronicleNode>(`${CHRONICLE_PATHS.NODES}/${nodeId}`);
198
+ if (node) {
199
+ result.push(node);
200
+ }
201
+
202
+ if (direction === 'backward' || direction === 'both') {
203
+ const inEdges =
204
+ (await this.db.get<ChronicleEdge[]>(`${CHRONICLE_PATHS.EDGES_IN}/${nodeId}`)) ?? [];
205
+ for (const edge of inEdges) {
206
+ await this._traceRecursive(edge.from, direction, maxDepth, depth + 1, visited, result);
207
+ }
208
+ }
209
+
210
+ if (direction === 'forward' || direction === 'both') {
211
+ const outEdges =
212
+ (await this.db.get<ChronicleEdge[]>(`${CHRONICLE_PATHS.EDGES_OUT}/${nodeId}`)) ?? [];
213
+ for (const edge of outEdges) {
214
+ await this._traceRecursive(edge.to, direction, maxDepth, depth + 1, visited, result);
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Create a PluresDB-backed Chronicle instance.
222
+ *
223
+ * @param db The PraxisDB instance to store causal graph data in
224
+ */
225
+ export function createChronicle(db: PraxisDB): PluresDbChronicle {
226
+ return new PluresDbChronicle(db);
227
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Chronicle Context
3
+ *
4
+ * Synchronous causal context propagation for Chronicle span tracking.
5
+ * Equivalent to Rust's `tracing` crate span context, adapted for TypeScript.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Run code attributed to a specific span/session
10
+ * await ChronicleContext.runAsync(
11
+ * { spanId: 'route-decision-1', contextId: 'session-abc' },
12
+ * async () => {
13
+ * await store.storeFact(fact); // attributed to route-decision-1 / session-abc
14
+ * }
15
+ * );
16
+ * ```
17
+ */
18
+ export interface ChronicleSpan {
19
+ /** The span/operation ID (becomes the `cause` field on Chronicle nodes) */
20
+ spanId?: string;
21
+ /** Session or request ID grouping related spans */
22
+ contextId?: string;
23
+ }
24
+
25
+ /**
26
+ * Stack-based synchronous causal context propagation.
27
+ *
28
+ * Uses a call-stack approach for environments without AsyncLocalStorage.
29
+ * Works correctly for synchronous and sequentially-awaited async code.
30
+ * For concurrent async flows, use `runAsync` per logical operation.
31
+ */
32
+ export class ChronicleContext {
33
+ private static readonly _stack: ChronicleSpan[] = [];
34
+
35
+ /**
36
+ * Get the current active span, if any.
37
+ */
38
+ static get current(): ChronicleSpan | undefined {
39
+ return this._stack[this._stack.length - 1];
40
+ }
41
+
42
+ /**
43
+ * Run a synchronous function within a causal span.
44
+ * The span is automatically popped when the function returns.
45
+ */
46
+ static run<T>(span: ChronicleSpan, fn: () => T): T {
47
+ this._stack.push(span);
48
+ try {
49
+ return fn();
50
+ } finally {
51
+ this._stack.pop();
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Run an async function within a causal span.
57
+ * The span is popped after the promise settles.
58
+ */
59
+ static async runAsync<T>(span: ChronicleSpan, fn: () => Promise<T>): Promise<T> {
60
+ this._stack.push(span);
61
+ try {
62
+ return await fn();
63
+ } finally {
64
+ this._stack.pop();
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Create a child span that inherits the current contextId.
70
+ *
71
+ * @param spanId ID for the new span
72
+ * @returns A new ChronicleSpan with the current contextId
73
+ */
74
+ static childSpan(spanId: string): ChronicleSpan {
75
+ return {
76
+ spanId,
77
+ contextId: this.current?.contextId,
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Chronicle Module
3
+ *
4
+ * Causal graph tracking for Praxis state transitions.
5
+ * Records every storeFact / appendEvent call as a Chronicle node in PluresDB,
6
+ * enabling full observability, auditing, and training data extraction.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { createInMemoryDB } from '@plures/praxis';
11
+ * import { createChronicle, createChronosMcpTools, ChronicleContext } from '@plures/praxis';
12
+ *
13
+ * const db = createInMemoryDB();
14
+ * const chronicle = createChronicle(db);
15
+ * const store = createPraxisDBStore(db, registry).withChronicle(chronicle);
16
+ *
17
+ * // Attribute changes to a causal span
18
+ * await ChronicleContext.runAsync(
19
+ * { spanId: 'route-msg-1', contextId: 'session-abc' },
20
+ * () => store.storeFact({ tag: 'RouteDecision', payload: { route: 'fast-path' } })
21
+ * );
22
+ *
23
+ * // Trace causality backward from any node
24
+ * const tools = createChronosMcpTools(chronicle);
25
+ * const { data } = await tools.trace({ nodeId: '...', direction: 'backward' });
26
+ * ```
27
+ */
28
+
29
+ // Types
30
+ export type {
31
+ TraceDirection,
32
+ EdgeType,
33
+ ChronicleEvent,
34
+ ChronicleNode,
35
+ ChronicleEdge,
36
+ } from './types.js';
37
+
38
+ // Context propagation
39
+ export { ChronicleContext } from './context.js';
40
+ export type { ChronicleSpan } from './context.js';
41
+
42
+ // Chronicle interface and PluresDB implementation
43
+ export type { Chronicle } from './chronicle.js';
44
+ export { PluresDbChronicle, createChronicle, CHRONICLE_PATHS } from './chronicle.js';
45
+
46
+ // MCP tools
47
+ export type {
48
+ ChronosTraceParams,
49
+ ChronosSearchParams,
50
+ McpToolResult,
51
+ ChronosMcpTools,
52
+ } from './mcp.js';
53
+ export { createChronosMcpTools } from './mcp.js';
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Chronicle MCP Tools
3
+ *
4
+ * Exposes Chronicle query functionality as MCP-compatible tool handlers.
5
+ * Register these tools with any MCP server to surface Chronos querying
6
+ * capabilities for troubleshooting, auditing, and AI training data extraction.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const chronicle = createChronicle(db);
11
+ * const tools = createChronosMcpTools(chronicle);
12
+ *
13
+ * // MCP server registration (framework-agnostic)
14
+ * server.registerTool('chronos.trace', tools.trace);
15
+ * server.registerTool('chronos.search', tools.search);
16
+ * ```
17
+ */
18
+
19
+ import type { Chronicle } from './chronicle.js';
20
+ import type { ChronicleNode, TraceDirection } from './types.js';
21
+
22
+ /**
23
+ * Parameters for the `chronos.trace` MCP tool.
24
+ */
25
+ export interface ChronosTraceParams {
26
+ /** ID of the Chronicle node to start tracing from */
27
+ nodeId: string;
28
+ /** Direction to traverse the causal graph (default: `'backward'`) */
29
+ direction?: TraceDirection;
30
+ /** Maximum traversal depth (default: 10) */
31
+ maxDepth?: number;
32
+ }
33
+
34
+ /**
35
+ * Parameters for the `chronos.search` MCP tool.
36
+ */
37
+ export interface ChronosSearchParams {
38
+ /** Search query matched against node paths, metadata, and serialised payloads */
39
+ query: string;
40
+ /** Optional context ID — restricts search to a single session/request subgraph */
41
+ contextId?: string;
42
+ /** Inclusive start timestamp in ms (default: 0) */
43
+ since?: number;
44
+ /** Inclusive end timestamp in ms (default: now) */
45
+ until?: number;
46
+ /** Maximum number of results (default: no limit) */
47
+ limit?: number;
48
+ }
49
+
50
+ /**
51
+ * Uniform result envelope for MCP tool calls.
52
+ */
53
+ export interface McpToolResult<T> {
54
+ /** Whether the tool call succeeded */
55
+ success: boolean;
56
+ /** Returned data (present on success) */
57
+ data?: T;
58
+ /** Error message (present on failure) */
59
+ error?: string;
60
+ }
61
+
62
+ /**
63
+ * Chronos MCP tools bound to a Chronicle instance.
64
+ */
65
+ export interface ChronosMcpTools {
66
+ /**
67
+ * `chronos.trace` — trace causality backward/forward from a Chronicle node.
68
+ */
69
+ trace(params: ChronosTraceParams): Promise<McpToolResult<ChronicleNode[]>>;
70
+
71
+ /**
72
+ * `chronos.search` — search Chronicle nodes by path, metadata, or payload content.
73
+ */
74
+ search(params: ChronosSearchParams): Promise<McpToolResult<ChronicleNode[]>>;
75
+ }
76
+
77
+ /**
78
+ * Create Chronos MCP tools bound to a Chronicle instance.
79
+ *
80
+ * @param chronicle Chronicle instance to query
81
+ * @returns Object with `trace` and `search` tool handlers
82
+ */
83
+ export function createChronosMcpTools(chronicle: Chronicle): ChronosMcpTools {
84
+ return {
85
+ async trace(params: ChronosTraceParams): Promise<McpToolResult<ChronicleNode[]>> {
86
+ try {
87
+ const nodes = await chronicle.trace(
88
+ params.nodeId,
89
+ params.direction ?? 'backward',
90
+ params.maxDepth ?? 10
91
+ );
92
+ return { success: true, data: nodes };
93
+ } catch (error) {
94
+ return {
95
+ success: false,
96
+ error: error instanceof Error ? error.message : String(error),
97
+ };
98
+ }
99
+ },
100
+
101
+ async search(params: ChronosSearchParams): Promise<McpToolResult<ChronicleNode[]>> {
102
+ try {
103
+ const query = params.query.toLowerCase();
104
+
105
+ let candidates: ChronicleNode[];
106
+ if (params.contextId) {
107
+ candidates = await chronicle.subgraph(params.contextId);
108
+ } else {
109
+ candidates = await chronicle.range(params.since ?? 0, params.until ?? Date.now());
110
+ }
111
+
112
+ // Full-text match against path, metadata values, and serialised after/before payloads.
113
+ // NOTE: This is a linear scan suitable for development and moderate datasets.
114
+ // For production use at scale, consider building a search index in PluresDB.
115
+ const filtered = candidates.filter((node) => {
116
+ const inPath = node.event.path.toLowerCase().includes(query);
117
+ const inMeta = Object.values(node.event.metadata).some((v) =>
118
+ v.toLowerCase().includes(query)
119
+ );
120
+ const inAfter = JSON.stringify(node.event.after ?? '').toLowerCase().includes(query);
121
+ const inBefore = JSON.stringify(node.event.before ?? '').toLowerCase().includes(query);
122
+ return inPath || inMeta || inAfter || inBefore;
123
+ });
124
+
125
+ const limited = params.limit !== undefined ? filtered.slice(0, params.limit) : filtered;
126
+ return { success: true, data: limited };
127
+ } catch (error) {
128
+ return {
129
+ success: false,
130
+ error: error instanceof Error ? error.message : String(error),
131
+ };
132
+ }
133
+ },
134
+ };
135
+ }