@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.
Files changed (85) hide show
  1. package/README.md +44 -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,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';