@plures/praxis 1.2.12 → 1.2.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -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
package/dist/node/cli/index.js
CHANGED
|
@@ -689,7 +689,7 @@ cloudCmd.command("usage").description("View cloud usage metrics").action(async (
|
|
|
689
689
|
});
|
|
690
690
|
program.command("verify <type>").description("Verify project implementation (e.g., implementation)").option("-d, --detailed", "Show detailed analysis").action(async (type, options) => {
|
|
691
691
|
try {
|
|
692
|
-
const { verify } = await import("../verify-
|
|
692
|
+
const { verify } = await import("../verify-7VZRP2WS.js");
|
|
693
693
|
await verify(type, options);
|
|
694
694
|
} catch (error) {
|
|
695
695
|
console.error("Error verifying:", error);
|
|
@@ -698,7 +698,7 @@ program.command("verify <type>").description("Verify project implementation (e.g
|
|
|
698
698
|
});
|
|
699
699
|
program.command("validate").description("Validate contract coverage for rules and constraints").option("--output <format>", "Output format (console, json, sarif)", "console").option("--strict", "Exit with error if contracts are missing", false).option("--registry <path>", "Path to registry module").option("--tests", "Check for tests for each rule/constraint", true).option("--spec", "Check for specs for each rule/constraint", true).option("--emit-facts", "Emit ContractMissing facts JSON payload", false).option("--gap-output <file>", "Write contract-gap payload to file").option("--ledger <dir>", "Write logic ledger snapshots to directory").option("--author <name>", "Author name for ledger entries", "system").action(async (options) => {
|
|
700
700
|
try {
|
|
701
|
-
const { validateCommand } = await import("../validate-
|
|
701
|
+
const { validateCommand } = await import("../validate-EN3M4FUR.js");
|
|
702
702
|
await validateCommand(options);
|
|
703
703
|
} catch (error) {
|
|
704
704
|
console.error("Error validating contracts:", error);
|
|
@@ -714,4 +714,41 @@ program.command("reverse").description("Reverse engineer contracts from existing
|
|
|
714
714
|
process.exit(1);
|
|
715
715
|
}
|
|
716
716
|
});
|
|
717
|
+
var conversationsCmd = program.command("conversations").description("Conversation ingestion subsystem (capture -> redact -> normalize -> classify -> emit)");
|
|
718
|
+
conversationsCmd.command("capture").description("Capture a conversation from input").option("-i, --input <file>", "Input conversation file").option("-o, --output <file>", "Output file for captured conversation").action(async (options) => {
|
|
719
|
+
try {
|
|
720
|
+
const { captureCommand } = await import("../conversations-KQBXTP3N.js");
|
|
721
|
+
await captureCommand(options);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
console.error("Error capturing conversation:", error);
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
conversationsCmd.command("push").description("Process conversation through pipeline (redact -> normalize)").requiredOption("-i, --input <file>", "Input conversation file").option("-o, --output <file>", "Output file for processed conversation").option("--skip-redaction", "Skip PII redaction", false).option("--skip-normalization", "Skip normalization", false).action(async (options) => {
|
|
728
|
+
try {
|
|
729
|
+
const { pushCommand } = await import("../conversations-KQBXTP3N.js");
|
|
730
|
+
await pushCommand(options);
|
|
731
|
+
} catch (error) {
|
|
732
|
+
console.error("Error processing conversation:", error);
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
conversationsCmd.command("classify").description("Classify conversation and generate candidate").requiredOption("-i, --input <file>", "Input conversation file").option("-o, --output <file>", "Output file for candidate").action(async (options) => {
|
|
737
|
+
try {
|
|
738
|
+
const { classifyCommand } = await import("../conversations-KQBXTP3N.js");
|
|
739
|
+
await classifyCommand(options);
|
|
740
|
+
} catch (error) {
|
|
741
|
+
console.error("Error classifying conversation:", error);
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
conversationsCmd.command("emit").description("Emit candidate to destination (fs or github)").requiredOption("-i, --input <file>", "Input candidate file").requiredOption("-e, --emitter <type>", "Emitter type (fs, github)").option("--output-dir <dir>", "Output directory (for fs emitter)", "./output/candidates").option("--owner <owner>", "GitHub repository owner (for github emitter)").option("--repo <repo>", "GitHub repository name (for github emitter)").option("--token <token>", "GitHub token (for github emitter)").option("--dry-run", "Dry run mode (no actual emission)", false).option("--commit-intent", "REQUIRED: Explicit commit intent for GitHub emission", false).action(async (options) => {
|
|
746
|
+
try {
|
|
747
|
+
const { emitCommand } = await import("../conversations-KQBXTP3N.js");
|
|
748
|
+
await emitCommand(options);
|
|
749
|
+
} catch (error) {
|
|
750
|
+
console.error("Error emitting candidate:", error);
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
717
754
|
program.parse();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as TerminalAdapter } from '../terminal-adapter-
|
|
2
|
-
export { c as createTerminalAdapter } from '../terminal-adapter-
|
|
1
|
+
import { T as TerminalAdapter } from '../terminal-adapter-Db-snPJ3.cjs';
|
|
2
|
+
export { c as createTerminalAdapter } from '../terminal-adapter-Db-snPJ3.cjs';
|
|
3
3
|
import '../schema.cjs';
|
|
4
4
|
import '@plures/pluresdb/local-first';
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as TerminalAdapter } from '../terminal-adapter-
|
|
2
|
-
export { c as createTerminalAdapter } from '../terminal-adapter-
|
|
1
|
+
import { T as TerminalAdapter } from '../terminal-adapter-CvIvgTo4.js';
|
|
2
|
+
export { c as createTerminalAdapter } from '../terminal-adapter-CvIvgTo4.js';
|
|
3
3
|
import '../schema.js';
|
|
4
4
|
import '@plures/pluresdb/local-first';
|
|
5
5
|
|
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import "./chunk-QGM4M3NI.js";
|
|
2
|
+
|
|
3
|
+
// src/cli/commands/conversations.ts
|
|
4
|
+
import { promises as fs2 } from "fs";
|
|
5
|
+
|
|
6
|
+
// src/conversations/capture.ts
|
|
7
|
+
import { randomUUID } from "crypto";
|
|
8
|
+
function captureConversation(input) {
|
|
9
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10
|
+
return {
|
|
11
|
+
id: randomUUID(),
|
|
12
|
+
timestamp: now,
|
|
13
|
+
turns: input.turns.map((turn) => ({
|
|
14
|
+
role: turn.role,
|
|
15
|
+
content: turn.content,
|
|
16
|
+
timestamp: turn.timestamp || now,
|
|
17
|
+
metadata: {}
|
|
18
|
+
})),
|
|
19
|
+
metadata: {
|
|
20
|
+
source: input.metadata?.source || "unknown",
|
|
21
|
+
userId: input.metadata?.userId,
|
|
22
|
+
sessionId: input.metadata?.sessionId,
|
|
23
|
+
tags: input.metadata?.tags || []
|
|
24
|
+
},
|
|
25
|
+
redacted: false,
|
|
26
|
+
normalized: false,
|
|
27
|
+
classified: false
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function loadConversation(json) {
|
|
31
|
+
const data = JSON.parse(json);
|
|
32
|
+
if (!data.id || !data.timestamp || !data.turns || !data.metadata) {
|
|
33
|
+
throw new Error("Invalid conversation JSON: missing required fields");
|
|
34
|
+
}
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/conversations/redact.ts
|
|
39
|
+
var PII_PATTERNS = [
|
|
40
|
+
// Email addresses
|
|
41
|
+
{ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, replacement: "[EMAIL_REDACTED]" },
|
|
42
|
+
// Phone numbers (simple patterns)
|
|
43
|
+
{ pattern: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, replacement: "[PHONE_REDACTED]" },
|
|
44
|
+
// Credit card numbers (basic pattern - intentionally broad for safety)
|
|
45
|
+
{ pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, replacement: "[CARD_REDACTED]" },
|
|
46
|
+
// SSN pattern
|
|
47
|
+
{ pattern: /\b\d{3}-\d{2}-\d{4}\b/g, replacement: "[SSN_REDACTED]" },
|
|
48
|
+
// IP addresses (basic pattern - may match invalid IPs like 999.999.999.999)
|
|
49
|
+
{ pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, replacement: "[IP_REDACTED]" }
|
|
50
|
+
];
|
|
51
|
+
function redactText(text) {
|
|
52
|
+
let redacted = text;
|
|
53
|
+
for (const { pattern, replacement } of PII_PATTERNS) {
|
|
54
|
+
redacted = redacted.replace(pattern, replacement);
|
|
55
|
+
}
|
|
56
|
+
return redacted;
|
|
57
|
+
}
|
|
58
|
+
function redactConversation(conversation) {
|
|
59
|
+
return {
|
|
60
|
+
...conversation,
|
|
61
|
+
turns: conversation.turns.map((turn) => ({
|
|
62
|
+
...turn,
|
|
63
|
+
content: redactText(turn.content)
|
|
64
|
+
})),
|
|
65
|
+
metadata: {
|
|
66
|
+
...conversation.metadata,
|
|
67
|
+
// Redact userId if it looks like an email
|
|
68
|
+
userId: conversation.metadata.userId ? redactText(conversation.metadata.userId) : void 0
|
|
69
|
+
},
|
|
70
|
+
redacted: true
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/conversations/normalize.ts
|
|
75
|
+
function normalizeWhitespace(text) {
|
|
76
|
+
return text.replace(/\r\n/g, "\n").replace(/\t/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
77
|
+
}
|
|
78
|
+
function normalizeCodeBlocks(text) {
|
|
79
|
+
return text.replace(/```(\w+)?\n/g, (_match, lang) => {
|
|
80
|
+
return lang ? `\`\`\`${lang.toLowerCase()}
|
|
81
|
+
` : "```\n";
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function normalizeTurn(content) {
|
|
85
|
+
let normalized = content;
|
|
86
|
+
normalized = normalizeWhitespace(normalized);
|
|
87
|
+
normalized = normalizeCodeBlocks(normalized);
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
function normalizeConversation(conversation) {
|
|
91
|
+
return {
|
|
92
|
+
...conversation,
|
|
93
|
+
turns: conversation.turns.map((turn) => ({
|
|
94
|
+
...turn,
|
|
95
|
+
content: normalizeTurn(turn.content)
|
|
96
|
+
})),
|
|
97
|
+
normalized: true
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/conversations/classify.ts
|
|
102
|
+
var CLASSIFICATION_RULES = [
|
|
103
|
+
{
|
|
104
|
+
category: "bug-report",
|
|
105
|
+
keywords: ["bug", "error", "crash", "broken", "issue", "problem", "fail", "exception"],
|
|
106
|
+
weight: 1
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
category: "feature-request",
|
|
110
|
+
keywords: ["feature", "enhancement", "add", "support", "implement", "would like", "could you"],
|
|
111
|
+
weight: 1
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
category: "question",
|
|
115
|
+
keywords: ["how", "what", "why", "when", "where", "?", "help", "question"],
|
|
116
|
+
weight: 0.8
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
category: "documentation",
|
|
120
|
+
keywords: ["docs", "documentation", "readme", "guide", "tutorial", "example"],
|
|
121
|
+
weight: 0.9
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
category: "performance",
|
|
125
|
+
keywords: ["slow", "performance", "optimize", "speed", "memory", "cpu"],
|
|
126
|
+
weight: 1
|
|
127
|
+
}
|
|
128
|
+
];
|
|
129
|
+
function extractKeywords(text) {
|
|
130
|
+
return text.toLowerCase().split(/\s+/).map((word) => word.replace(/[^\w]/g, "")).filter((word) => word.length > 2);
|
|
131
|
+
}
|
|
132
|
+
function calculateScores(conversation) {
|
|
133
|
+
const scores = {};
|
|
134
|
+
const allContent = conversation.turns.map((t) => t.content).join(" ");
|
|
135
|
+
const keywords = extractKeywords(allContent);
|
|
136
|
+
const keywordSet = new Set(keywords);
|
|
137
|
+
for (const rule of CLASSIFICATION_RULES) {
|
|
138
|
+
let matchCount = 0;
|
|
139
|
+
for (const keyword of rule.keywords) {
|
|
140
|
+
if (keywordSet.has(keyword.toLowerCase())) {
|
|
141
|
+
matchCount++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (matchCount > 0) {
|
|
145
|
+
scores[rule.category] = matchCount / rule.keywords.length * rule.weight;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return scores;
|
|
149
|
+
}
|
|
150
|
+
function classifyConversation(conversation) {
|
|
151
|
+
const scores = calculateScores(conversation);
|
|
152
|
+
let bestCategory = "unknown";
|
|
153
|
+
let bestScore = 0;
|
|
154
|
+
for (const [category, score] of Object.entries(scores)) {
|
|
155
|
+
if (score > bestScore) {
|
|
156
|
+
bestScore = score;
|
|
157
|
+
bestCategory = category;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const classification = {
|
|
161
|
+
category: bestCategory,
|
|
162
|
+
confidence: Math.min(bestScore, 1),
|
|
163
|
+
tags: Object.entries(scores).filter(([_, score]) => score > 0.3).map(([category]) => category)
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
...conversation,
|
|
167
|
+
classification,
|
|
168
|
+
classified: true
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/conversations/candidates.ts
|
|
173
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
174
|
+
function generateCandidate(conversation) {
|
|
175
|
+
if (!conversation.classified || !conversation.classification) {
|
|
176
|
+
throw new Error("Conversation must be classified before generating candidates");
|
|
177
|
+
}
|
|
178
|
+
const { classification } = conversation;
|
|
179
|
+
let type = "documentation";
|
|
180
|
+
if (classification.category === "bug-report") {
|
|
181
|
+
type = "github-issue";
|
|
182
|
+
} else if (classification.category === "feature-request") {
|
|
183
|
+
type = "github-issue";
|
|
184
|
+
} else if (classification.category === "documentation") {
|
|
185
|
+
type = "documentation";
|
|
186
|
+
}
|
|
187
|
+
const firstUserTurn = conversation.turns.find((t) => t.role === "user");
|
|
188
|
+
if (!firstUserTurn) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const title = generateTitle(firstUserTurn.content, classification.category);
|
|
192
|
+
const body = generateBody(conversation);
|
|
193
|
+
let priority = "medium";
|
|
194
|
+
if (classification.category === "bug-report" && (classification.confidence ?? 0) > 0.7) {
|
|
195
|
+
priority = "high";
|
|
196
|
+
}
|
|
197
|
+
const labels = [...new Set([classification.category, ...classification.tags || []].filter((label) => label !== void 0))];
|
|
198
|
+
return {
|
|
199
|
+
id: randomUUID2(),
|
|
200
|
+
conversationId: conversation.id,
|
|
201
|
+
type,
|
|
202
|
+
title,
|
|
203
|
+
body,
|
|
204
|
+
metadata: {
|
|
205
|
+
priority,
|
|
206
|
+
labels,
|
|
207
|
+
source: {
|
|
208
|
+
conversationId: conversation.id,
|
|
209
|
+
timestamp: conversation.timestamp
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
emitted: false
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function generateTitle(content, category) {
|
|
216
|
+
const firstLine = content.split("\n")[0].trim();
|
|
217
|
+
const firstSentence = firstLine.split(/[.!?]/)[0].trim();
|
|
218
|
+
let title = firstSentence.substring(0, 80);
|
|
219
|
+
if (firstSentence.length > 80) {
|
|
220
|
+
title += "...";
|
|
221
|
+
}
|
|
222
|
+
if (category && category !== "unknown") {
|
|
223
|
+
const prefix = category.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
224
|
+
title = `[${prefix}] ${title}`;
|
|
225
|
+
}
|
|
226
|
+
return title;
|
|
227
|
+
}
|
|
228
|
+
function generateBody(conversation) {
|
|
229
|
+
const parts = [];
|
|
230
|
+
parts.push("## Conversation Summary\n");
|
|
231
|
+
for (const turn of conversation.turns) {
|
|
232
|
+
const roleName = turn.role.charAt(0).toUpperCase() + turn.role.slice(1);
|
|
233
|
+
parts.push(`**${roleName}:**
|
|
234
|
+
${turn.content}
|
|
235
|
+
`);
|
|
236
|
+
}
|
|
237
|
+
if (conversation.classification) {
|
|
238
|
+
parts.push(`
|
|
239
|
+
## Classification
|
|
240
|
+
`);
|
|
241
|
+
parts.push(`- Category: ${conversation.classification.category}`);
|
|
242
|
+
parts.push(`- Confidence: ${(conversation.classification.confidence || 0).toFixed(2)}`);
|
|
243
|
+
if (conversation.classification.tags && conversation.classification.tags.length > 0) {
|
|
244
|
+
parts.push(`- Tags: ${conversation.classification.tags.join(", ")}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
parts.push(`
|
|
248
|
+
---
|
|
249
|
+
*Generated from conversation ${conversation.id}*`);
|
|
250
|
+
return parts.join("\n");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/conversations/gate.ts
|
|
254
|
+
function gateMinimumLength(candidate) {
|
|
255
|
+
const minLength = 50;
|
|
256
|
+
const passed = candidate.body.length >= minLength;
|
|
257
|
+
return {
|
|
258
|
+
name: "minimum-length",
|
|
259
|
+
passed,
|
|
260
|
+
message: passed ? "Content meets minimum length requirement" : `Content too short (${candidate.body.length} < ${minLength} chars)`
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function gateValidTitle(candidate) {
|
|
264
|
+
const passed = candidate.title.length > 10 && candidate.title.length < 200;
|
|
265
|
+
return {
|
|
266
|
+
name: "valid-title",
|
|
267
|
+
passed,
|
|
268
|
+
message: passed ? "Title is valid" : "Title length must be between 10 and 200 characters"
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function gateNotDuplicate(_candidate) {
|
|
272
|
+
return {
|
|
273
|
+
name: "not-duplicate",
|
|
274
|
+
passed: true,
|
|
275
|
+
message: "Duplicate check passed (stub)"
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function gateHasMetadata(candidate) {
|
|
279
|
+
const hasLabels = !!(candidate.metadata.labels && candidate.metadata.labels.length > 0);
|
|
280
|
+
const hasPriority = !!candidate.metadata.priority;
|
|
281
|
+
const passed = hasLabels && hasPriority;
|
|
282
|
+
return {
|
|
283
|
+
name: "has-metadata",
|
|
284
|
+
passed,
|
|
285
|
+
message: passed ? "Candidate has required metadata" : "Missing labels or priority in metadata"
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function applyGates(candidate) {
|
|
289
|
+
const gates = [
|
|
290
|
+
gateMinimumLength(candidate),
|
|
291
|
+
gateValidTitle(candidate),
|
|
292
|
+
gateNotDuplicate(candidate),
|
|
293
|
+
gateHasMetadata(candidate)
|
|
294
|
+
];
|
|
295
|
+
const allPassed = gates.every((g) => g.passed);
|
|
296
|
+
const failedGates = gates.filter((g) => !g.passed);
|
|
297
|
+
const gateStatus = {
|
|
298
|
+
passed: allPassed,
|
|
299
|
+
reason: allPassed ? "All gates passed" : `Failed gates: ${failedGates.map((g) => g.name).join(", ")}`,
|
|
300
|
+
gates
|
|
301
|
+
};
|
|
302
|
+
return {
|
|
303
|
+
...candidate,
|
|
304
|
+
gateStatus
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function candidatePassed(candidate) {
|
|
308
|
+
return candidate.gateStatus?.passed || false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/conversations/emitters/fs.ts
|
|
312
|
+
import { promises as fs } from "fs";
|
|
313
|
+
import path from "path";
|
|
314
|
+
async function emitToFS(candidate, options) {
|
|
315
|
+
const { outputDir, dryRun = false } = options;
|
|
316
|
+
if (dryRun) {
|
|
317
|
+
console.log(`[DRY RUN] Would emit candidate ${candidate.id} to ${outputDir}`);
|
|
318
|
+
return {
|
|
319
|
+
...candidate,
|
|
320
|
+
emitted: true,
|
|
321
|
+
emissionResult: {
|
|
322
|
+
success: true,
|
|
323
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
324
|
+
externalId: `fs://${outputDir}/${candidate.id}.json`
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
330
|
+
const filename = `${candidate.id}.json`;
|
|
331
|
+
const filepath = path.join(outputDir, filename);
|
|
332
|
+
await fs.writeFile(filepath, JSON.stringify(candidate, null, 2), "utf-8");
|
|
333
|
+
const result = {
|
|
334
|
+
success: true,
|
|
335
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
336
|
+
externalId: `fs://${filepath}`
|
|
337
|
+
};
|
|
338
|
+
return {
|
|
339
|
+
...candidate,
|
|
340
|
+
emitted: true,
|
|
341
|
+
emissionResult: result
|
|
342
|
+
};
|
|
343
|
+
} catch (error) {
|
|
344
|
+
const result = {
|
|
345
|
+
success: false,
|
|
346
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
347
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
348
|
+
};
|
|
349
|
+
return {
|
|
350
|
+
...candidate,
|
|
351
|
+
emitted: false,
|
|
352
|
+
emissionResult: result
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/conversations/emitters/github.ts
|
|
358
|
+
async function emitToGitHub(candidate, options) {
|
|
359
|
+
const { owner, repo, token, dryRun = false, commitIntent = false } = options;
|
|
360
|
+
if (!commitIntent) {
|
|
361
|
+
const result = {
|
|
362
|
+
success: false,
|
|
363
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
364
|
+
error: "GATE FAILURE: commit_intent must be explicitly set to true for GitHub emission"
|
|
365
|
+
};
|
|
366
|
+
console.error(`[GATE BLOCKED] ${result.error}`);
|
|
367
|
+
return {
|
|
368
|
+
...candidate,
|
|
369
|
+
emitted: false,
|
|
370
|
+
emissionResult: result
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (dryRun) {
|
|
374
|
+
console.log(`[DRY RUN] Would create GitHub issue in ${owner}/${repo}`);
|
|
375
|
+
console.log(`Title: ${candidate.title}`);
|
|
376
|
+
console.log(`Labels: ${candidate.metadata.labels?.join(", ") || "none"}`);
|
|
377
|
+
return {
|
|
378
|
+
...candidate,
|
|
379
|
+
emitted: true,
|
|
380
|
+
emissionResult: {
|
|
381
|
+
success: true,
|
|
382
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
383
|
+
externalId: `github://${owner}/${repo}/issues/DRY_RUN`
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
if (!owner || !repo) {
|
|
389
|
+
throw new Error("GitHub owner and repo are required");
|
|
390
|
+
}
|
|
391
|
+
if (!token) {
|
|
392
|
+
throw new Error("GitHub token is required for emission");
|
|
393
|
+
}
|
|
394
|
+
const issueData = {
|
|
395
|
+
title: candidate.title,
|
|
396
|
+
body: candidate.body,
|
|
397
|
+
labels: candidate.metadata.labels || [],
|
|
398
|
+
assignees: candidate.metadata.assignees || []
|
|
399
|
+
};
|
|
400
|
+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues`, {
|
|
401
|
+
method: "POST",
|
|
402
|
+
headers: {
|
|
403
|
+
"Authorization": `token ${token}`,
|
|
404
|
+
"Accept": "application/vnd.github.v3+json",
|
|
405
|
+
"Content-Type": "application/json"
|
|
406
|
+
},
|
|
407
|
+
body: JSON.stringify(issueData)
|
|
408
|
+
});
|
|
409
|
+
if (!response.ok) {
|
|
410
|
+
const errorData = await response.json();
|
|
411
|
+
throw new Error(`GitHub API error: ${errorData.message || response.statusText}`);
|
|
412
|
+
}
|
|
413
|
+
const issue = await response.json();
|
|
414
|
+
const result = {
|
|
415
|
+
success: true,
|
|
416
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
417
|
+
externalId: `github://${owner}/${repo}/issues/${issue.number}`
|
|
418
|
+
};
|
|
419
|
+
console.log(`\u2713 Created GitHub issue #${issue.number}: ${issue.html_url}`);
|
|
420
|
+
return {
|
|
421
|
+
...candidate,
|
|
422
|
+
emitted: true,
|
|
423
|
+
emissionResult: result
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
const result = {
|
|
427
|
+
success: false,
|
|
428
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
429
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
430
|
+
};
|
|
431
|
+
console.error(`\u2717 Failed to create GitHub issue: ${result.error}`);
|
|
432
|
+
return {
|
|
433
|
+
...candidate,
|
|
434
|
+
emitted: false,
|
|
435
|
+
emissionResult: result
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/cli/commands/conversations.ts
|
|
441
|
+
async function captureCommand(options) {
|
|
442
|
+
console.log("\u{1F4DD} Capturing conversation...");
|
|
443
|
+
let conversation;
|
|
444
|
+
if (options.input) {
|
|
445
|
+
const content = await fs2.readFile(options.input, "utf-8");
|
|
446
|
+
conversation = loadConversation(content);
|
|
447
|
+
console.log(`\u2713 Loaded conversation from ${options.input}`);
|
|
448
|
+
} else {
|
|
449
|
+
conversation = captureConversation({
|
|
450
|
+
turns: [
|
|
451
|
+
{ role: "user", content: "Hello, I have a question about the feature" },
|
|
452
|
+
{ role: "assistant", content: "Sure, I'd be happy to help!" }
|
|
453
|
+
],
|
|
454
|
+
metadata: {
|
|
455
|
+
source: "cli"
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
console.log("\u2713 Created sample conversation");
|
|
459
|
+
}
|
|
460
|
+
if (options.output) {
|
|
461
|
+
await fs2.writeFile(
|
|
462
|
+
options.output,
|
|
463
|
+
JSON.stringify(conversation, null, 2),
|
|
464
|
+
"utf-8"
|
|
465
|
+
);
|
|
466
|
+
console.log(`\u2713 Saved to ${options.output}`);
|
|
467
|
+
} else {
|
|
468
|
+
console.log(JSON.stringify(conversation, null, 2));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function pushCommand(options) {
|
|
472
|
+
console.log("\u{1F504} Processing conversation through pipeline...");
|
|
473
|
+
const content = await fs2.readFile(options.input, "utf-8");
|
|
474
|
+
let conversation = loadConversation(content);
|
|
475
|
+
console.log(`\u2713 Loaded conversation ${conversation.id}`);
|
|
476
|
+
if (!options.skipRedaction) {
|
|
477
|
+
conversation = redactConversation(conversation);
|
|
478
|
+
console.log("\u2713 Redacted PII");
|
|
479
|
+
}
|
|
480
|
+
if (!options.skipNormalization) {
|
|
481
|
+
conversation = normalizeConversation(conversation);
|
|
482
|
+
console.log("\u2713 Normalized content");
|
|
483
|
+
}
|
|
484
|
+
if (options.output) {
|
|
485
|
+
await fs2.writeFile(
|
|
486
|
+
options.output,
|
|
487
|
+
JSON.stringify(conversation, null, 2),
|
|
488
|
+
"utf-8"
|
|
489
|
+
);
|
|
490
|
+
console.log(`\u2713 Saved to ${options.output}`);
|
|
491
|
+
} else {
|
|
492
|
+
console.log(JSON.stringify(conversation, null, 2));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async function classifyCommand(options) {
|
|
496
|
+
console.log("\u{1F3F7}\uFE0F Classifying conversation...");
|
|
497
|
+
const content = await fs2.readFile(options.input, "utf-8");
|
|
498
|
+
let conversation = loadConversation(content);
|
|
499
|
+
console.log(`\u2713 Loaded conversation ${conversation.id}`);
|
|
500
|
+
if (!conversation.redacted) {
|
|
501
|
+
conversation = redactConversation(conversation);
|
|
502
|
+
console.log("\u2713 Applied redaction");
|
|
503
|
+
}
|
|
504
|
+
if (!conversation.normalized) {
|
|
505
|
+
conversation = normalizeConversation(conversation);
|
|
506
|
+
console.log("\u2713 Applied normalization");
|
|
507
|
+
}
|
|
508
|
+
conversation = classifyConversation(conversation);
|
|
509
|
+
console.log(`\u2713 Classified as: ${conversation.classification?.category} (confidence: ${conversation.classification?.confidence?.toFixed(2)})`);
|
|
510
|
+
const candidate = generateCandidate(conversation);
|
|
511
|
+
if (!candidate) {
|
|
512
|
+
console.error("\u2717 Failed to generate candidate");
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
console.log(`\u2713 Generated candidate: ${candidate.title}`);
|
|
516
|
+
const gatedCandidate = applyGates(candidate);
|
|
517
|
+
console.log(`
|
|
518
|
+
\u{1F4CB} Gate Results:`);
|
|
519
|
+
for (const gate of gatedCandidate.gateStatus?.gates || []) {
|
|
520
|
+
const icon = gate.passed ? "\u2713" : "\u2717";
|
|
521
|
+
console.log(` ${icon} ${gate.name}: ${gate.message}`);
|
|
522
|
+
}
|
|
523
|
+
const passed = candidatePassed(gatedCandidate);
|
|
524
|
+
console.log(`
|
|
525
|
+
${passed ? "\u2713 All gates passed" : "\u2717 Some gates failed"}`);
|
|
526
|
+
if (options.output) {
|
|
527
|
+
await fs2.writeFile(
|
|
528
|
+
options.output,
|
|
529
|
+
JSON.stringify(gatedCandidate, null, 2),
|
|
530
|
+
"utf-8"
|
|
531
|
+
);
|
|
532
|
+
console.log(`\u2713 Saved candidate to ${options.output}`);
|
|
533
|
+
} else {
|
|
534
|
+
console.log("\n" + JSON.stringify(gatedCandidate, null, 2));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async function emitCommand(options) {
|
|
538
|
+
console.log("\u{1F4E4} Emitting candidate...");
|
|
539
|
+
const content = await fs2.readFile(options.input, "utf-8");
|
|
540
|
+
const candidate = JSON.parse(content);
|
|
541
|
+
console.log(`\u2713 Loaded candidate ${candidate.id}`);
|
|
542
|
+
if (!candidatePassed(candidate)) {
|
|
543
|
+
console.error("\u2717 Candidate did not pass gates, refusing to emit");
|
|
544
|
+
console.error(` Reason: ${candidate.gateStatus?.reason}`);
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
let result;
|
|
548
|
+
if (options.emitter === "fs") {
|
|
549
|
+
const outputDir = options.outputDir || "./output/candidates";
|
|
550
|
+
result = await emitToFS(candidate, {
|
|
551
|
+
outputDir,
|
|
552
|
+
dryRun: options.dryRun
|
|
553
|
+
});
|
|
554
|
+
if (result.emissionResult?.success) {
|
|
555
|
+
console.log(`\u2713 Emitted to filesystem: ${result.emissionResult.externalId}`);
|
|
556
|
+
} else {
|
|
557
|
+
console.error(`\u2717 Emission failed: ${result.emissionResult?.error}`);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
} else if (options.emitter === "github") {
|
|
561
|
+
if (!options.owner || !options.repo) {
|
|
562
|
+
console.error("\u2717 GitHub emitter requires --owner and --repo");
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
if (!options.commitIntent) {
|
|
566
|
+
console.error("");
|
|
567
|
+
console.error("\u26D4 GATE BLOCKED: commit_intent=false");
|
|
568
|
+
console.error("");
|
|
569
|
+
console.error("The GitHub emitter is HARD GATED by the --commit-intent flag.");
|
|
570
|
+
console.error("This prevents accidental issue creation.");
|
|
571
|
+
console.error("");
|
|
572
|
+
console.error("To emit to GitHub, add: --commit-intent");
|
|
573
|
+
console.error("");
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
result = await emitToGitHub(candidate, {
|
|
577
|
+
owner: options.owner,
|
|
578
|
+
repo: options.repo,
|
|
579
|
+
token: options.token,
|
|
580
|
+
dryRun: options.dryRun,
|
|
581
|
+
commitIntent: options.commitIntent
|
|
582
|
+
});
|
|
583
|
+
if (result.emissionResult?.success) {
|
|
584
|
+
console.log(`\u2713 Emitted to GitHub: ${result.emissionResult.externalId}`);
|
|
585
|
+
} else {
|
|
586
|
+
console.error(`\u2717 Emission failed: ${result.emissionResult?.error}`);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
export {
|
|
592
|
+
captureCommand,
|
|
593
|
+
classifyCommand,
|
|
594
|
+
emitCommand,
|
|
595
|
+
pushCommand
|
|
596
|
+
};
|