@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,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
|
+
}
|