@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,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Candidates module for praxis-conversations
|
|
3
|
+
* Generates emission candidates from classified conversations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Conversation, Candidate, CandidateMetadata } from './types.js';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generate a candidate from a conversation
|
|
11
|
+
*/
|
|
12
|
+
export function generateCandidate(conversation: Conversation): Candidate | null {
|
|
13
|
+
if (!conversation.classified || !conversation.classification) {
|
|
14
|
+
throw new Error('Conversation must be classified before generating candidates');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { classification } = conversation;
|
|
18
|
+
|
|
19
|
+
// Determine candidate type based on classification
|
|
20
|
+
let type: Candidate['type'] = 'documentation';
|
|
21
|
+
if (classification.category === 'bug-report') {
|
|
22
|
+
type = 'github-issue';
|
|
23
|
+
} else if (classification.category === 'feature-request') {
|
|
24
|
+
type = 'github-issue';
|
|
25
|
+
} else if (classification.category === 'documentation') {
|
|
26
|
+
type = 'documentation';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Generate title from first user turn
|
|
30
|
+
const firstUserTurn = conversation.turns.find(t => t.role === 'user');
|
|
31
|
+
if (!firstUserTurn) {
|
|
32
|
+
return null; // No user input to generate candidate from
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const title = generateTitle(firstUserTurn.content, classification.category);
|
|
36
|
+
const body = generateBody(conversation);
|
|
37
|
+
|
|
38
|
+
// Determine priority
|
|
39
|
+
let priority: CandidateMetadata['priority'] = 'medium';
|
|
40
|
+
if (classification.category === 'bug-report' && (classification.confidence ?? 0) > 0.7) {
|
|
41
|
+
priority = 'high';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Deduplicate labels using Set for O(n) performance
|
|
45
|
+
const labels = [...new Set([classification.category, ...(classification.tags || [])].filter((label): label is string => label !== undefined))];
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
id: randomUUID(),
|
|
49
|
+
conversationId: conversation.id,
|
|
50
|
+
type,
|
|
51
|
+
title,
|
|
52
|
+
body,
|
|
53
|
+
metadata: {
|
|
54
|
+
priority,
|
|
55
|
+
labels,
|
|
56
|
+
source: {
|
|
57
|
+
conversationId: conversation.id,
|
|
58
|
+
timestamp: conversation.timestamp,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
emitted: false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate a title from content
|
|
67
|
+
*/
|
|
68
|
+
function generateTitle(content: string, category?: string): string {
|
|
69
|
+
// Take first line or first sentence, max 80 chars
|
|
70
|
+
const firstLine = content.split('\n')[0].trim();
|
|
71
|
+
const firstSentence = firstLine.split(/[.!?]/)[0].trim();
|
|
72
|
+
|
|
73
|
+
let title = firstSentence.substring(0, 80);
|
|
74
|
+
if (firstSentence.length > 80) {
|
|
75
|
+
title += '...';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add category prefix if available
|
|
79
|
+
if (category && category !== 'unknown') {
|
|
80
|
+
const prefix = category.split('-').map(w => w[0].toUpperCase() + w.slice(1)).join(' ');
|
|
81
|
+
title = `[${prefix}] ${title}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return title;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Generate body from conversation
|
|
89
|
+
*/
|
|
90
|
+
function generateBody(conversation: Conversation): string {
|
|
91
|
+
const parts: string[] = [];
|
|
92
|
+
|
|
93
|
+
// Add conversation summary
|
|
94
|
+
parts.push('## Conversation Summary\n');
|
|
95
|
+
|
|
96
|
+
for (const turn of conversation.turns) {
|
|
97
|
+
const roleName = turn.role.charAt(0).toUpperCase() + turn.role.slice(1);
|
|
98
|
+
parts.push(`**${roleName}:**\n${turn.content}\n`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Add metadata
|
|
102
|
+
if (conversation.classification) {
|
|
103
|
+
parts.push(`\n## Classification\n`);
|
|
104
|
+
parts.push(`- Category: ${conversation.classification.category}`);
|
|
105
|
+
parts.push(`- Confidence: ${(conversation.classification.confidence || 0).toFixed(2)}`);
|
|
106
|
+
if (conversation.classification.tags && conversation.classification.tags.length > 0) {
|
|
107
|
+
parts.push(`- Tags: ${conversation.classification.tags.join(', ')}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
parts.push(`\n---\n*Generated from conversation ${conversation.id}*`);
|
|
112
|
+
|
|
113
|
+
return parts.join('\n');
|
|
114
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture module for praxis-conversations
|
|
3
|
+
* Handles capturing conversations from various sources
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Conversation } from './types.js';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Capture a conversation from raw input
|
|
11
|
+
*/
|
|
12
|
+
export function captureConversation(input: {
|
|
13
|
+
turns: Array<{ role: string; content: string; timestamp?: string }>;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
}): Conversation {
|
|
16
|
+
const now = new Date().toISOString();
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
id: randomUUID(),
|
|
20
|
+
timestamp: now,
|
|
21
|
+
turns: input.turns.map(turn => ({
|
|
22
|
+
role: turn.role as 'user' | 'assistant' | 'system',
|
|
23
|
+
content: turn.content,
|
|
24
|
+
timestamp: turn.timestamp || now,
|
|
25
|
+
metadata: {},
|
|
26
|
+
})),
|
|
27
|
+
metadata: {
|
|
28
|
+
source: (input.metadata?.source as string) || 'unknown',
|
|
29
|
+
userId: input.metadata?.userId as string,
|
|
30
|
+
sessionId: input.metadata?.sessionId as string,
|
|
31
|
+
tags: (input.metadata?.tags as string[]) || [],
|
|
32
|
+
},
|
|
33
|
+
redacted: false,
|
|
34
|
+
normalized: false,
|
|
35
|
+
classified: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load a conversation from JSON
|
|
41
|
+
*/
|
|
42
|
+
export function loadConversation(json: string): Conversation {
|
|
43
|
+
const data = JSON.parse(json);
|
|
44
|
+
// Basic validation
|
|
45
|
+
if (!data.id || !data.timestamp || !data.turns || !data.metadata) {
|
|
46
|
+
throw new Error('Invalid conversation JSON: missing required fields');
|
|
47
|
+
}
|
|
48
|
+
return data as Conversation;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Serialize a conversation to JSON
|
|
53
|
+
*/
|
|
54
|
+
export function serializeConversation(conversation: Conversation): string {
|
|
55
|
+
return JSON.stringify(conversation, null, 2);
|
|
56
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classification module for praxis-conversations
|
|
3
|
+
* Handles deterministic classification of conversations (no LLM)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Conversation, Classification } from './types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deterministic keyword-based classification rules
|
|
10
|
+
*/
|
|
11
|
+
const CLASSIFICATION_RULES = [
|
|
12
|
+
{
|
|
13
|
+
category: 'bug-report',
|
|
14
|
+
keywords: ['bug', 'error', 'crash', 'broken', 'issue', 'problem', 'fail', 'exception'],
|
|
15
|
+
weight: 1.0,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
category: 'feature-request',
|
|
19
|
+
keywords: ['feature', 'enhancement', 'add', 'support', 'implement', 'would like', 'could you'],
|
|
20
|
+
weight: 1.0,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
category: 'question',
|
|
24
|
+
keywords: ['how', 'what', 'why', 'when', 'where', '?', 'help', 'question'],
|
|
25
|
+
weight: 0.8,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
category: 'documentation',
|
|
29
|
+
keywords: ['docs', 'documentation', 'readme', 'guide', 'tutorial', 'example'],
|
|
30
|
+
weight: 0.9,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
category: 'performance',
|
|
34
|
+
keywords: ['slow', 'performance', 'optimize', 'speed', 'memory', 'cpu'],
|
|
35
|
+
weight: 1.0,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract keywords from text (simple tokenization)
|
|
41
|
+
*/
|
|
42
|
+
function extractKeywords(text: string): string[] {
|
|
43
|
+
return text
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.split(/\s+/)
|
|
46
|
+
.map(word => word.replace(/[^\w]/g, ''))
|
|
47
|
+
.filter(word => word.length > 2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Calculate classification scores for a conversation
|
|
52
|
+
*/
|
|
53
|
+
function calculateScores(conversation: Conversation): Record<string, number> {
|
|
54
|
+
const scores: Record<string, number> = {};
|
|
55
|
+
|
|
56
|
+
// Combine all turn content
|
|
57
|
+
const allContent = conversation.turns
|
|
58
|
+
.map(t => t.content)
|
|
59
|
+
.join(' ');
|
|
60
|
+
|
|
61
|
+
const keywords = extractKeywords(allContent);
|
|
62
|
+
const keywordSet = new Set(keywords);
|
|
63
|
+
|
|
64
|
+
for (const rule of CLASSIFICATION_RULES) {
|
|
65
|
+
let matchCount = 0;
|
|
66
|
+
for (const keyword of rule.keywords) {
|
|
67
|
+
if (keywordSet.has(keyword.toLowerCase())) {
|
|
68
|
+
matchCount++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (matchCount > 0) {
|
|
73
|
+
scores[rule.category] = (matchCount / rule.keywords.length) * rule.weight;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return scores;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Classify a conversation using deterministic keyword matching
|
|
82
|
+
*/
|
|
83
|
+
export function classifyConversation(conversation: Conversation): Conversation {
|
|
84
|
+
const scores = calculateScores(conversation);
|
|
85
|
+
|
|
86
|
+
// Find the highest scoring category
|
|
87
|
+
let bestCategory = 'unknown';
|
|
88
|
+
let bestScore = 0;
|
|
89
|
+
|
|
90
|
+
for (const [category, score] of Object.entries(scores)) {
|
|
91
|
+
if (score > bestScore) {
|
|
92
|
+
bestScore = score;
|
|
93
|
+
bestCategory = category;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const classification: Classification = {
|
|
98
|
+
category: bestCategory,
|
|
99
|
+
confidence: Math.min(bestScore, 1.0),
|
|
100
|
+
tags: Object.entries(scores)
|
|
101
|
+
.filter(([_, score]) => score > 0.3)
|
|
102
|
+
.map(([category]) => category),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
...conversation,
|
|
107
|
+
classification,
|
|
108
|
+
classified: true,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://praxis.dev/schemas/conversation.json",
|
|
4
|
+
"title": "Conversation",
|
|
5
|
+
"description": "Schema for a conversation in the praxis-conversations ingestion subsystem",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["id", "timestamp", "turns", "metadata"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"id": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Unique identifier for the conversation"
|
|
12
|
+
},
|
|
13
|
+
"timestamp": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"format": "date-time",
|
|
16
|
+
"description": "ISO 8601 timestamp of conversation start"
|
|
17
|
+
},
|
|
18
|
+
"turns": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"description": "Array of conversation turns",
|
|
21
|
+
"items": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"required": ["role", "content", "timestamp"],
|
|
24
|
+
"properties": {
|
|
25
|
+
"role": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["user", "assistant", "system"],
|
|
28
|
+
"description": "Role of the speaker"
|
|
29
|
+
},
|
|
30
|
+
"content": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Content of the turn"
|
|
33
|
+
},
|
|
34
|
+
"timestamp": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"format": "date-time",
|
|
37
|
+
"description": "ISO 8601 timestamp of the turn"
|
|
38
|
+
},
|
|
39
|
+
"metadata": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"description": "Optional metadata for the turn"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"metadata": {
|
|
47
|
+
"type": "object",
|
|
48
|
+
"description": "Conversation metadata",
|
|
49
|
+
"properties": {
|
|
50
|
+
"source": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"description": "Source of the conversation (e.g., 'github-copilot', 'cli', 'web')"
|
|
53
|
+
},
|
|
54
|
+
"userId": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "User identifier (may be redacted)"
|
|
57
|
+
},
|
|
58
|
+
"sessionId": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Session identifier"
|
|
61
|
+
},
|
|
62
|
+
"tags": {
|
|
63
|
+
"type": "array",
|
|
64
|
+
"items": { "type": "string" },
|
|
65
|
+
"description": "Tags for categorization"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"redacted": {
|
|
70
|
+
"type": "boolean",
|
|
71
|
+
"description": "Whether PII has been redacted",
|
|
72
|
+
"default": false
|
|
73
|
+
},
|
|
74
|
+
"normalized": {
|
|
75
|
+
"type": "boolean",
|
|
76
|
+
"description": "Whether the conversation has been normalized",
|
|
77
|
+
"default": false
|
|
78
|
+
},
|
|
79
|
+
"classified": {
|
|
80
|
+
"type": "boolean",
|
|
81
|
+
"description": "Whether the conversation has been classified",
|
|
82
|
+
"default": false
|
|
83
|
+
},
|
|
84
|
+
"classification": {
|
|
85
|
+
"type": "object",
|
|
86
|
+
"description": "Classification results",
|
|
87
|
+
"properties": {
|
|
88
|
+
"category": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"description": "Primary category (e.g., 'feature-request', 'bug-report', 'question')"
|
|
91
|
+
},
|
|
92
|
+
"confidence": {
|
|
93
|
+
"type": "number",
|
|
94
|
+
"minimum": 0,
|
|
95
|
+
"maximum": 1,
|
|
96
|
+
"description": "Confidence score for classification"
|
|
97
|
+
},
|
|
98
|
+
"tags": {
|
|
99
|
+
"type": "array",
|
|
100
|
+
"items": { "type": "string" },
|
|
101
|
+
"description": "Additional classification tags"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem emitter for praxis-conversations
|
|
3
|
+
* Emits candidates to the local filesystem
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Candidate, FSEmitterOptions, EmissionResult } from '../types.js';
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Emit a candidate to the filesystem
|
|
12
|
+
*/
|
|
13
|
+
export async function emitToFS(
|
|
14
|
+
candidate: Candidate,
|
|
15
|
+
options: FSEmitterOptions
|
|
16
|
+
): Promise<Candidate> {
|
|
17
|
+
const { outputDir, dryRun = false } = options;
|
|
18
|
+
|
|
19
|
+
if (dryRun) {
|
|
20
|
+
console.log(`[DRY RUN] Would emit candidate ${candidate.id} to ${outputDir}`);
|
|
21
|
+
return {
|
|
22
|
+
...candidate,
|
|
23
|
+
emitted: true,
|
|
24
|
+
emissionResult: {
|
|
25
|
+
success: true,
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
externalId: `fs://${outputDir}/${candidate.id}.json`,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Ensure output directory exists
|
|
34
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
// Write candidate to file
|
|
37
|
+
const filename = `${candidate.id}.json`;
|
|
38
|
+
const filepath = path.join(outputDir, filename);
|
|
39
|
+
await fs.writeFile(filepath, JSON.stringify(candidate, null, 2), 'utf-8');
|
|
40
|
+
|
|
41
|
+
const result: EmissionResult = {
|
|
42
|
+
success: true,
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
externalId: `fs://${filepath}`,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...candidate,
|
|
49
|
+
emitted: true,
|
|
50
|
+
emissionResult: result,
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const result: EmissionResult = {
|
|
54
|
+
success: false,
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
...candidate,
|
|
61
|
+
emitted: false,
|
|
62
|
+
emissionResult: result,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub emitter for praxis-conversations
|
|
3
|
+
* Emits candidates to GitHub as issues (HARD GATED by commit_intent=true)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Candidate, GitHubEmitterOptions, EmissionResult } from '../types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* CRITICAL: GitHub emitter is HARD GATED by commit_intent=true
|
|
10
|
+
* This ensures no accidental issue creation
|
|
11
|
+
*/
|
|
12
|
+
export async function emitToGitHub(
|
|
13
|
+
candidate: Candidate,
|
|
14
|
+
options: GitHubEmitterOptions
|
|
15
|
+
): Promise<Candidate> {
|
|
16
|
+
const { owner, repo, token, dryRun = false, commitIntent = false } = options;
|
|
17
|
+
|
|
18
|
+
// HARD GATE: Must have commit_intent=true
|
|
19
|
+
if (!commitIntent) {
|
|
20
|
+
const result: EmissionResult = {
|
|
21
|
+
success: false,
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
error: 'GATE FAILURE: commit_intent must be explicitly set to true for GitHub emission',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
console.error(`[GATE BLOCKED] ${result.error}`);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
...candidate,
|
|
30
|
+
emitted: false,
|
|
31
|
+
emissionResult: result,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (dryRun) {
|
|
36
|
+
console.log(`[DRY RUN] Would create GitHub issue in ${owner}/${repo}`);
|
|
37
|
+
console.log(`Title: ${candidate.title}`);
|
|
38
|
+
console.log(`Labels: ${candidate.metadata.labels?.join(', ') || 'none'}`);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
...candidate,
|
|
42
|
+
emitted: true,
|
|
43
|
+
emissionResult: {
|
|
44
|
+
success: true,
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
externalId: `github://${owner}/${repo}/issues/DRY_RUN`,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Validate required options
|
|
53
|
+
if (!owner || !repo) {
|
|
54
|
+
throw new Error('GitHub owner and repo are required');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!token) {
|
|
58
|
+
throw new Error('GitHub token is required for emission');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Prepare issue data
|
|
62
|
+
const issueData = {
|
|
63
|
+
title: candidate.title,
|
|
64
|
+
body: candidate.body,
|
|
65
|
+
labels: candidate.metadata.labels || [],
|
|
66
|
+
assignees: candidate.metadata.assignees || [],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Make GitHub API call
|
|
70
|
+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: {
|
|
73
|
+
'Authorization': `token ${token}`,
|
|
74
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
},
|
|
77
|
+
body: JSON.stringify(issueData),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!response.ok) {
|
|
81
|
+
const errorData = await response.json();
|
|
82
|
+
throw new Error(`GitHub API error: ${errorData.message || response.statusText}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const issue = await response.json() as { number: number; html_url: string };
|
|
86
|
+
|
|
87
|
+
const result: EmissionResult = {
|
|
88
|
+
success: true,
|
|
89
|
+
timestamp: new Date().toISOString(),
|
|
90
|
+
externalId: `github://${owner}/${repo}/issues/${issue.number}`,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
console.log(`✓ Created GitHub issue #${issue.number}: ${issue.html_url}`);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
...candidate,
|
|
97
|
+
emitted: true,
|
|
98
|
+
emissionResult: result,
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const result: EmissionResult = {
|
|
102
|
+
success: false,
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
console.error(`✗ Failed to create GitHub issue: ${result.error}`);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
...candidate,
|
|
111
|
+
emitted: false,
|
|
112
|
+
emissionResult: result,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gating module for praxis-conversations
|
|
3
|
+
* Applies deterministic gates to candidates before emission
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Candidate, GateStatus, GateResult } from './types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Gate: Minimum content length
|
|
10
|
+
*/
|
|
11
|
+
function gateMinimumLength(candidate: Candidate): GateResult {
|
|
12
|
+
const minLength = 50;
|
|
13
|
+
const passed = candidate.body.length >= minLength;
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
name: 'minimum-length',
|
|
17
|
+
passed,
|
|
18
|
+
message: passed
|
|
19
|
+
? 'Content meets minimum length requirement'
|
|
20
|
+
: `Content too short (${candidate.body.length} < ${minLength} chars)`,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gate: Has valid title
|
|
26
|
+
*/
|
|
27
|
+
function gateValidTitle(candidate: Candidate): GateResult {
|
|
28
|
+
const passed = candidate.title.length > 10 && candidate.title.length < 200;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: 'valid-title',
|
|
32
|
+
passed,
|
|
33
|
+
message: passed
|
|
34
|
+
? 'Title is valid'
|
|
35
|
+
: 'Title length must be between 10 and 200 characters',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gate: Not duplicate (simple check based on title)
|
|
41
|
+
* In production, this would check against existing issues
|
|
42
|
+
*/
|
|
43
|
+
function gateNotDuplicate(_candidate: Candidate): GateResult {
|
|
44
|
+
// For now, always pass - would implement duplicate detection in production
|
|
45
|
+
return {
|
|
46
|
+
name: 'not-duplicate',
|
|
47
|
+
passed: true,
|
|
48
|
+
message: 'Duplicate check passed (stub)',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Gate: Has metadata
|
|
54
|
+
*/
|
|
55
|
+
function gateHasMetadata(candidate: Candidate): GateResult {
|
|
56
|
+
const hasLabels = !!(candidate.metadata.labels && candidate.metadata.labels.length > 0);
|
|
57
|
+
const hasPriority = !!candidate.metadata.priority;
|
|
58
|
+
const passed: boolean = hasLabels && hasPriority;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
name: 'has-metadata',
|
|
62
|
+
passed,
|
|
63
|
+
message: passed
|
|
64
|
+
? 'Candidate has required metadata'
|
|
65
|
+
: 'Missing labels or priority in metadata',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply all gates to a candidate
|
|
71
|
+
*/
|
|
72
|
+
export function applyGates(candidate: Candidate): Candidate {
|
|
73
|
+
const gates: GateResult[] = [
|
|
74
|
+
gateMinimumLength(candidate),
|
|
75
|
+
gateValidTitle(candidate),
|
|
76
|
+
gateNotDuplicate(candidate),
|
|
77
|
+
gateHasMetadata(candidate),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const allPassed = gates.every(g => g.passed);
|
|
81
|
+
const failedGates = gates.filter(g => !g.passed);
|
|
82
|
+
|
|
83
|
+
const gateStatus: GateStatus = {
|
|
84
|
+
passed: allPassed,
|
|
85
|
+
reason: allPassed
|
|
86
|
+
? 'All gates passed'
|
|
87
|
+
: `Failed gates: ${failedGates.map(g => g.name).join(', ')}`,
|
|
88
|
+
gates,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
...candidate,
|
|
93
|
+
gateStatus,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a candidate passed all gates
|
|
99
|
+
*/
|
|
100
|
+
export function candidatePassed(candidate: Candidate): boolean {
|
|
101
|
+
return candidate.gateStatus?.passed || false;
|
|
102
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Praxis Conversations Subsystem
|
|
3
|
+
*
|
|
4
|
+
* Deterministic-first conversation ingestion pipeline:
|
|
5
|
+
* capture -> redact -> normalize -> classify -> candidates -> gate -> emit
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from './types.js';
|
|
9
|
+
export * from './capture.js';
|
|
10
|
+
export * from './redact.js';
|
|
11
|
+
export * from './normalize.js';
|
|
12
|
+
export * from './classify.js';
|
|
13
|
+
export * from './candidates.js';
|
|
14
|
+
export * from './gate.js';
|
|
15
|
+
|
|
16
|
+
// Emitters
|
|
17
|
+
export * from './emitters/fs.js';
|
|
18
|
+
export * from './emitters/github.js';
|
|
19
|
+
|
|
20
|
+
// Re-export main pipeline functions
|
|
21
|
+
export { captureConversation, loadConversation, serializeConversation } from './capture.js';
|
|
22
|
+
export { redactConversation, redactText } from './redact.js';
|
|
23
|
+
export { normalizeConversation } from './normalize.js';
|
|
24
|
+
export { classifyConversation } from './classify.js';
|
|
25
|
+
export { generateCandidate } from './candidates.js';
|
|
26
|
+
export { applyGates, candidatePassed } from './gate.js';
|
|
27
|
+
export { emitToFS } from './emitters/fs.js';
|
|
28
|
+
export { emitToGitHub } from './emitters/github.js';
|