@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,252 @@
1
+ /**
2
+ * Praxis CLI - Conversations Commands
3
+ *
4
+ * Commands for the praxis-conversations subsystem
5
+ */
6
+
7
+ import { promises as fs } from 'node:fs';
8
+ import {
9
+ loadConversation,
10
+ captureConversation,
11
+ redactConversation,
12
+ normalizeConversation,
13
+ classifyConversation,
14
+ generateCandidate,
15
+ applyGates,
16
+ candidatePassed,
17
+ emitToFS,
18
+ emitToGitHub,
19
+ type Conversation,
20
+ type Candidate,
21
+ } from '../../conversations/index.js';
22
+
23
+ interface CaptureOptions {
24
+ input?: string;
25
+ output?: string;
26
+ }
27
+
28
+ interface PushOptions {
29
+ input: string;
30
+ output?: string;
31
+ skipRedaction?: boolean;
32
+ skipNormalization?: boolean;
33
+ }
34
+
35
+ interface ClassifyOptions {
36
+ input: string;
37
+ output?: string;
38
+ }
39
+
40
+ interface EmitOptions {
41
+ input: string;
42
+ emitter: 'fs' | 'github';
43
+ outputDir?: string;
44
+ owner?: string;
45
+ repo?: string;
46
+ token?: string;
47
+ dryRun?: boolean;
48
+ commitIntent?: boolean;
49
+ }
50
+
51
+ /**
52
+ * Capture command: Capture a conversation from input
53
+ */
54
+ export async function captureCommand(options: CaptureOptions): Promise<void> {
55
+ console.log('📝 Capturing conversation...');
56
+
57
+ let conversation: Conversation;
58
+
59
+ if (options.input) {
60
+ // Load from file
61
+ const content = await fs.readFile(options.input, 'utf-8');
62
+ conversation = loadConversation(content);
63
+ console.log(`✓ Loaded conversation from ${options.input}`);
64
+ } else {
65
+ // Create a sample conversation for demo
66
+ conversation = captureConversation({
67
+ turns: [
68
+ { role: 'user', content: 'Hello, I have a question about the feature' },
69
+ { role: 'assistant', content: 'Sure, I\'d be happy to help!' },
70
+ ],
71
+ metadata: {
72
+ source: 'cli',
73
+ },
74
+ });
75
+ console.log('✓ Created sample conversation');
76
+ }
77
+
78
+ if (options.output) {
79
+ await fs.writeFile(
80
+ options.output,
81
+ JSON.stringify(conversation, null, 2),
82
+ 'utf-8'
83
+ );
84
+ console.log(`✓ Saved to ${options.output}`);
85
+ } else {
86
+ console.log(JSON.stringify(conversation, null, 2));
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Push command: Process conversation through pipeline (capture -> redact -> normalize)
92
+ */
93
+ export async function pushCommand(options: PushOptions): Promise<void> {
94
+ console.log('🔄 Processing conversation through pipeline...');
95
+
96
+ // Load conversation
97
+ const content = await fs.readFile(options.input, 'utf-8');
98
+ let conversation = loadConversation(content);
99
+ console.log(`✓ Loaded conversation ${conversation.id}`);
100
+
101
+ // Redact
102
+ if (!options.skipRedaction) {
103
+ conversation = redactConversation(conversation);
104
+ console.log('✓ Redacted PII');
105
+ }
106
+
107
+ // Normalize
108
+ if (!options.skipNormalization) {
109
+ conversation = normalizeConversation(conversation);
110
+ console.log('✓ Normalized content');
111
+ }
112
+
113
+ // Save result
114
+ if (options.output) {
115
+ await fs.writeFile(
116
+ options.output,
117
+ JSON.stringify(conversation, null, 2),
118
+ 'utf-8'
119
+ );
120
+ console.log(`✓ Saved to ${options.output}`);
121
+ } else {
122
+ console.log(JSON.stringify(conversation, null, 2));
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Classify command: Classify a conversation and generate candidate
128
+ */
129
+ export async function classifyCommand(options: ClassifyOptions): Promise<void> {
130
+ console.log('🏷️ Classifying conversation...');
131
+
132
+ // Load conversation
133
+ const content = await fs.readFile(options.input, 'utf-8');
134
+ let conversation = loadConversation(content);
135
+ console.log(`✓ Loaded conversation ${conversation.id}`);
136
+
137
+ // Ensure conversation is processed
138
+ if (!conversation.redacted) {
139
+ conversation = redactConversation(conversation);
140
+ console.log('✓ Applied redaction');
141
+ }
142
+
143
+ if (!conversation.normalized) {
144
+ conversation = normalizeConversation(conversation);
145
+ console.log('✓ Applied normalization');
146
+ }
147
+
148
+ // Classify
149
+ conversation = classifyConversation(conversation);
150
+ console.log(`✓ Classified as: ${conversation.classification?.category} (confidence: ${conversation.classification?.confidence?.toFixed(2)})`);
151
+
152
+ // Generate candidate
153
+ const candidate = generateCandidate(conversation);
154
+ if (!candidate) {
155
+ console.error('✗ Failed to generate candidate');
156
+ process.exit(1);
157
+ }
158
+
159
+ console.log(`✓ Generated candidate: ${candidate.title}`);
160
+
161
+ // Apply gates
162
+ const gatedCandidate = applyGates(candidate);
163
+ console.log(`\n📋 Gate Results:`);
164
+ for (const gate of gatedCandidate.gateStatus?.gates || []) {
165
+ const icon = gate.passed ? '✓' : '✗';
166
+ console.log(` ${icon} ${gate.name}: ${gate.message}`);
167
+ }
168
+
169
+ const passed = candidatePassed(gatedCandidate);
170
+ console.log(`\n${passed ? '✓ All gates passed' : '✗ Some gates failed'}`);
171
+
172
+ // Save result
173
+ if (options.output) {
174
+ await fs.writeFile(
175
+ options.output,
176
+ JSON.stringify(gatedCandidate, null, 2),
177
+ 'utf-8'
178
+ );
179
+ console.log(`✓ Saved candidate to ${options.output}`);
180
+ } else {
181
+ console.log('\n' + JSON.stringify(gatedCandidate, null, 2));
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Emit command: Emit a candidate to a destination
187
+ */
188
+ export async function emitCommand(options: EmitOptions): Promise<void> {
189
+ console.log('📤 Emitting candidate...');
190
+
191
+ // Load candidate
192
+ const content = await fs.readFile(options.input, 'utf-8');
193
+ const candidate = JSON.parse(content) as Candidate;
194
+ console.log(`✓ Loaded candidate ${candidate.id}`);
195
+
196
+ // Check gates
197
+ if (!candidatePassed(candidate)) {
198
+ console.error('✗ Candidate did not pass gates, refusing to emit');
199
+ console.error(` Reason: ${candidate.gateStatus?.reason}`);
200
+ process.exit(1);
201
+ }
202
+
203
+ let result: Candidate;
204
+
205
+ if (options.emitter === 'fs') {
206
+ const outputDir = options.outputDir || './output/candidates';
207
+ result = await emitToFS(candidate, {
208
+ outputDir,
209
+ dryRun: options.dryRun,
210
+ });
211
+
212
+ if (result.emissionResult?.success) {
213
+ console.log(`✓ Emitted to filesystem: ${result.emissionResult.externalId}`);
214
+ } else {
215
+ console.error(`✗ Emission failed: ${result.emissionResult?.error}`);
216
+ process.exit(1);
217
+ }
218
+ } else if (options.emitter === 'github') {
219
+ if (!options.owner || !options.repo) {
220
+ console.error('✗ GitHub emitter requires --owner and --repo');
221
+ process.exit(1);
222
+ }
223
+
224
+ // CRITICAL: Display commit_intent gate status
225
+ if (!options.commitIntent) {
226
+ console.error('');
227
+ console.error('⛔ GATE BLOCKED: commit_intent=false');
228
+ console.error('');
229
+ console.error('The GitHub emitter is HARD GATED by the --commit-intent flag.');
230
+ console.error('This prevents accidental issue creation.');
231
+ console.error('');
232
+ console.error('To emit to GitHub, add: --commit-intent');
233
+ console.error('');
234
+ process.exit(1);
235
+ }
236
+
237
+ result = await emitToGitHub(candidate, {
238
+ owner: options.owner,
239
+ repo: options.repo,
240
+ token: options.token,
241
+ dryRun: options.dryRun,
242
+ commitIntent: options.commitIntent,
243
+ });
244
+
245
+ if (result.emissionResult?.success) {
246
+ console.log(`✓ Emitted to GitHub: ${result.emissionResult.externalId}`);
247
+ } else {
248
+ console.error(`✗ Emission failed: ${result.emissionResult?.error}`);
249
+ process.exit(1);
250
+ }
251
+ }
252
+ }
package/src/cli/index.ts CHANGED
@@ -301,4 +301,77 @@ program
301
301
  }
302
302
  });
303
303
 
304
+ // Conversations commands (praxis-conversations subsystem)
305
+ const conversationsCmd = program
306
+ .command('conversations')
307
+ .description('Conversation ingestion subsystem (capture -> redact -> normalize -> classify -> emit)');
308
+
309
+ conversationsCmd
310
+ .command('capture')
311
+ .description('Capture a conversation from input')
312
+ .option('-i, --input <file>', 'Input conversation file')
313
+ .option('-o, --output <file>', 'Output file for captured conversation')
314
+ .action(async (options) => {
315
+ try {
316
+ const { captureCommand } = await import('./commands/conversations.js');
317
+ await captureCommand(options);
318
+ } catch (error) {
319
+ console.error('Error capturing conversation:', error);
320
+ process.exit(1);
321
+ }
322
+ });
323
+
324
+ conversationsCmd
325
+ .command('push')
326
+ .description('Process conversation through pipeline (redact -> normalize)')
327
+ .requiredOption('-i, --input <file>', 'Input conversation file')
328
+ .option('-o, --output <file>', 'Output file for processed conversation')
329
+ .option('--skip-redaction', 'Skip PII redaction', false)
330
+ .option('--skip-normalization', 'Skip normalization', false)
331
+ .action(async (options) => {
332
+ try {
333
+ const { pushCommand } = await import('./commands/conversations.js');
334
+ await pushCommand(options);
335
+ } catch (error) {
336
+ console.error('Error processing conversation:', error);
337
+ process.exit(1);
338
+ }
339
+ });
340
+
341
+ conversationsCmd
342
+ .command('classify')
343
+ .description('Classify conversation and generate candidate')
344
+ .requiredOption('-i, --input <file>', 'Input conversation file')
345
+ .option('-o, --output <file>', 'Output file for candidate')
346
+ .action(async (options) => {
347
+ try {
348
+ const { classifyCommand } = await import('./commands/conversations.js');
349
+ await classifyCommand(options);
350
+ } catch (error) {
351
+ console.error('Error classifying conversation:', error);
352
+ process.exit(1);
353
+ }
354
+ });
355
+
356
+ conversationsCmd
357
+ .command('emit')
358
+ .description('Emit candidate to destination (fs or github)')
359
+ .requiredOption('-i, --input <file>', 'Input candidate file')
360
+ .requiredOption('-e, --emitter <type>', 'Emitter type (fs, github)')
361
+ .option('--output-dir <dir>', 'Output directory (for fs emitter)', './output/candidates')
362
+ .option('--owner <owner>', 'GitHub repository owner (for github emitter)')
363
+ .option('--repo <repo>', 'GitHub repository name (for github emitter)')
364
+ .option('--token <token>', 'GitHub token (for github emitter)')
365
+ .option('--dry-run', 'Dry run mode (no actual emission)', false)
366
+ .option('--commit-intent', 'REQUIRED: Explicit commit intent for GitHub emission', false)
367
+ .action(async (options) => {
368
+ try {
369
+ const { emitCommand } = await import('./commands/conversations.js');
370
+ await emitCommand(options);
371
+ } catch (error) {
372
+ console.error('Error emitting candidate:', error);
373
+ process.exit(1);
374
+ }
375
+ });
376
+
304
377
  program.parse();
@@ -0,0 +1,230 @@
1
+ # Praxis Conversations Subsystem
2
+
3
+ The praxis-conversations subsystem provides a deterministic-first conversation ingestion pipeline for capturing, processing, and emitting conversation data.
4
+
5
+ ## Pipeline
6
+
7
+ The subsystem follows a deterministic pipeline:
8
+
9
+ ```
10
+ capture → redact → normalize → classify → candidates → gate → emit
11
+ ```
12
+
13
+ ### Pipeline Stages
14
+
15
+ 1. **Capture**: Capture conversations from various sources (CLI, API, files)
16
+ 2. **Redact**: Remove PII (email, phone, IP addresses) using deterministic patterns
17
+ 3. **Normalize**: Normalize whitespace, code blocks, and formatting
18
+ 4. **Classify**: Classify conversations using keyword-based rules (no LLM)
19
+ 5. **Candidates**: Generate emission candidates (GitHub issues, docs, etc.)
20
+ 6. **Gate**: Apply quality gates before emission
21
+ 7. **Emit**: Emit to destinations (filesystem, GitHub)
22
+
23
+ ## Schemas
24
+
25
+ ### Conversation Schema
26
+
27
+ See `conversation.schema.json` for the full schema.
28
+
29
+ ```json
30
+ {
31
+ "id": "uuid",
32
+ "timestamp": "ISO 8601",
33
+ "turns": [
34
+ {
35
+ "role": "user|assistant|system",
36
+ "content": "text",
37
+ "timestamp": "ISO 8601"
38
+ }
39
+ ],
40
+ "metadata": {
41
+ "source": "github-copilot|cli|web",
42
+ "userId": "string",
43
+ "sessionId": "string",
44
+ "tags": ["tag1", "tag2"]
45
+ },
46
+ "redacted": false,
47
+ "normalized": false,
48
+ "classified": false,
49
+ "classification": {
50
+ "category": "bug-report|feature-request|question|...",
51
+ "confidence": 0.85,
52
+ "tags": ["tag1", "tag2"]
53
+ }
54
+ }
55
+ ```
56
+
57
+ ### Candidate Schema
58
+
59
+ See `candidate.schema.json` for the full schema.
60
+
61
+ ```json
62
+ {
63
+ "id": "uuid",
64
+ "conversationId": "uuid",
65
+ "type": "github-issue|github-pr|documentation|...",
66
+ "title": "Short title",
67
+ "body": "Full body content",
68
+ "metadata": {
69
+ "priority": "low|medium|high|critical",
70
+ "labels": ["bug", "priority:high"],
71
+ "assignees": ["username"]
72
+ },
73
+ "gateStatus": {
74
+ "passed": true,
75
+ "gates": [...]
76
+ },
77
+ "emitted": false
78
+ }
79
+ ```
80
+
81
+ ## CLI Usage
82
+
83
+ ### Capture a Conversation
84
+
85
+ ```bash
86
+ # Capture from file
87
+ praxis conversations capture -i conversation.json -o captured.json
88
+
89
+ # Create sample conversation
90
+ praxis conversations capture -o sample.json
91
+ ```
92
+
93
+ ### Process Through Pipeline
94
+
95
+ ```bash
96
+ # Redact and normalize
97
+ praxis conversations push -i conversation.json -o processed.json
98
+
99
+ # Skip redaction
100
+ praxis conversations push -i conversation.json --skip-redaction
101
+ ```
102
+
103
+ ### Classify and Generate Candidate
104
+
105
+ ```bash
106
+ # Classify conversation and generate candidate
107
+ praxis conversations classify -i conversation.json -o candidate.json
108
+ ```
109
+
110
+ ### Emit to Destination
111
+
112
+ ```bash
113
+ # Emit to filesystem
114
+ praxis conversations emit -i candidate.json -e fs --output-dir ./output
115
+
116
+ # Emit to GitHub (dry run)
117
+ praxis conversations emit -i candidate.json -e github \
118
+ --owner myorg --repo myrepo --dry-run
119
+
120
+ # Emit to GitHub (REQUIRES --commit-intent)
121
+ praxis conversations emit -i candidate.json -e github \
122
+ --owner myorg --repo myrepo --token $GITHUB_TOKEN --commit-intent
123
+ ```
124
+
125
+ ## GitHub Emitter Gate
126
+
127
+ **CRITICAL**: The GitHub emitter is **HARD GATED** by the `--commit-intent` flag.
128
+
129
+ This prevents accidental issue creation. You **MUST** explicitly pass `--commit-intent` to emit to GitHub:
130
+
131
+ ```bash
132
+ praxis conversations emit -i candidate.json -e github \
133
+ --owner myorg --repo myrepo --commit-intent
134
+ ```
135
+
136
+ Without `--commit-intent`, the command will fail with:
137
+
138
+ ```
139
+ ⛔ GATE BLOCKED: commit_intent=false
140
+
141
+ The GitHub emitter is HARD GATED by the --commit-intent flag.
142
+ This prevents accidental issue creation.
143
+
144
+ To emit to GitHub, add: --commit-intent
145
+ ```
146
+
147
+ ## Programmatic Usage
148
+
149
+ ```typescript
150
+ import {
151
+ captureConversation,
152
+ redactConversation,
153
+ normalizeConversation,
154
+ classifyConversation,
155
+ generateCandidate,
156
+ applyGates,
157
+ candidatePassed,
158
+ emitToFS,
159
+ emitToGitHub,
160
+ } from '@plures/praxis/conversations';
161
+
162
+ // Capture
163
+ const conversation = captureConversation({
164
+ turns: [
165
+ { role: 'user', content: 'I found a bug...' },
166
+ { role: 'assistant', content: 'Thanks for reporting!' },
167
+ ],
168
+ metadata: { source: 'cli' },
169
+ });
170
+
171
+ // Process
172
+ let processed = redactConversation(conversation);
173
+ processed = normalizeConversation(processed);
174
+ processed = classifyConversation(processed);
175
+
176
+ // Generate candidate
177
+ const candidate = generateCandidate(processed);
178
+
179
+ // Gate
180
+ const gated = applyGates(candidate);
181
+ if (candidatePassed(gated)) {
182
+ // Emit
183
+ const result = await emitToFS(gated, {
184
+ outputDir: './output/candidates',
185
+ });
186
+
187
+ console.log('Emitted:', result.emissionResult?.externalId);
188
+ }
189
+ ```
190
+
191
+ ## Classification Categories
192
+
193
+ The classifier uses deterministic keyword matching to identify:
194
+
195
+ - `bug-report`: bugs, errors, crashes
196
+ - `feature-request`: features, enhancements
197
+ - `question`: questions, help requests
198
+ - `documentation`: docs, guides, examples
199
+ - `performance`: performance, optimization
200
+
201
+ ## Quality Gates
202
+
203
+ Before emission, candidates pass through these gates:
204
+
205
+ 1. **minimum-length**: Content must be >= 50 characters
206
+ 2. **valid-title**: Title must be 10-200 characters
207
+ 3. **not-duplicate**: Not a duplicate (stub, would check existing issues)
208
+ 4. **has-metadata**: Must have labels and priority
209
+
210
+ ## Testing
211
+
212
+ Run tests:
213
+
214
+ ```bash
215
+ npm test src/__tests__/conversations.test.ts
216
+ ```
217
+
218
+ Test fixtures are in `test/fixtures/conversations/`.
219
+
220
+ ## Examples
221
+
222
+ See `test/fixtures/conversations/` for example conversations:
223
+
224
+ - `bug-report.json`: Bug report conversation
225
+ - `feature-request.json`: Feature request conversation
226
+ - `question.json`: Question conversation
227
+
228
+ ## License
229
+
230
+ MIT
@@ -0,0 +1,123 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://praxis.dev/schemas/candidate.json",
4
+ "title": "Candidate",
5
+ "description": "Schema for a conversation candidate for emission (GitHub issue, documentation, etc.)",
6
+ "type": "object",
7
+ "required": ["id", "conversationId", "type", "title", "body", "metadata"],
8
+ "properties": {
9
+ "id": {
10
+ "type": "string",
11
+ "description": "Unique identifier for the candidate"
12
+ },
13
+ "conversationId": {
14
+ "type": "string",
15
+ "description": "Reference to the source conversation ID"
16
+ },
17
+ "type": {
18
+ "type": "string",
19
+ "enum": ["github-issue", "github-pr", "documentation", "feature-request", "bug-report"],
20
+ "description": "Type of candidate to emit"
21
+ },
22
+ "title": {
23
+ "type": "string",
24
+ "description": "Title or subject for the candidate"
25
+ },
26
+ "body": {
27
+ "type": "string",
28
+ "description": "Main content body for the candidate"
29
+ },
30
+ "metadata": {
31
+ "type": "object",
32
+ "description": "Candidate metadata",
33
+ "properties": {
34
+ "priority": {
35
+ "type": "string",
36
+ "enum": ["low", "medium", "high", "critical"],
37
+ "description": "Priority level"
38
+ },
39
+ "labels": {
40
+ "type": "array",
41
+ "items": { "type": "string" },
42
+ "description": "Labels to apply (for GitHub issues/PRs)"
43
+ },
44
+ "assignees": {
45
+ "type": "array",
46
+ "items": { "type": "string" },
47
+ "description": "Assignees (for GitHub issues/PRs)"
48
+ },
49
+ "source": {
50
+ "type": "object",
51
+ "description": "Source information",
52
+ "properties": {
53
+ "conversationId": {
54
+ "type": "string"
55
+ },
56
+ "timestamp": {
57
+ "type": "string",
58
+ "format": "date-time"
59
+ }
60
+ }
61
+ }
62
+ }
63
+ },
64
+ "gateStatus": {
65
+ "type": "object",
66
+ "description": "Gating decision results",
67
+ "properties": {
68
+ "passed": {
69
+ "type": "boolean",
70
+ "description": "Whether the candidate passed gating"
71
+ },
72
+ "reason": {
73
+ "type": "string",
74
+ "description": "Reason for gating decision"
75
+ },
76
+ "gates": {
77
+ "type": "array",
78
+ "items": {
79
+ "type": "object",
80
+ "properties": {
81
+ "name": {
82
+ "type": "string"
83
+ },
84
+ "passed": {
85
+ "type": "boolean"
86
+ },
87
+ "message": {
88
+ "type": "string"
89
+ }
90
+ }
91
+ },
92
+ "description": "Individual gate results"
93
+ }
94
+ }
95
+ },
96
+ "emitted": {
97
+ "type": "boolean",
98
+ "description": "Whether the candidate has been emitted",
99
+ "default": false
100
+ },
101
+ "emissionResult": {
102
+ "type": "object",
103
+ "description": "Result of emission attempt",
104
+ "properties": {
105
+ "success": {
106
+ "type": "boolean"
107
+ },
108
+ "timestamp": {
109
+ "type": "string",
110
+ "format": "date-time"
111
+ },
112
+ "externalId": {
113
+ "type": "string",
114
+ "description": "External ID (e.g., GitHub issue number)"
115
+ },
116
+ "error": {
117
+ "type": "string",
118
+ "description": "Error message if emission failed"
119
+ }
120
+ }
121
+ }
122
+ }
123
+ }