@balpal4495/quorum 2.0.0 → 3.0.1
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/.github/copilot-instructions.md +29 -6
- package/README.md +69 -2
- package/bin/commands/compass.js +942 -0
- package/bin/commands/init.js +13 -7
- package/bin/commands/migrate-v2.js +136 -0
- package/bin/commands/sentinel.js +1 -1
- package/bin/commands/sync.js +97 -0
- package/bin/quorum.js +35 -0
- package/bin/templates/CLAUDE.md +101 -0
- package/modules/README.md +57 -10
- package/modules/compass/behavior.ts +161 -0
- package/modules/compass/create.ts +365 -0
- package/modules/compass/evidence/collect.ts +109 -0
- package/modules/compass/index.ts +7 -0
- package/modules/compass/prompts/index.ts +230 -0
- package/modules/compass/prompts/system.ts +24 -0
- package/modules/compass/propose.ts +152 -0
- package/modules/compass/schemas.ts +121 -0
- package/modules/compass/score.ts +77 -0
- package/modules/compass/sources/index.ts +413 -0
- package/modules/compass/types.ts +431 -0
- package/modules/setup.ts +33 -0
- package/package.json +19 -11
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { randomUUID } from "crypto"
|
|
2
|
+
import type {
|
|
3
|
+
ProductBehavior, ProductBehaviorGap, ProductBehaviorContradiction, BehaviorMap,
|
|
4
|
+
BehaviorMapInput, ProductSourceFinding, CompassEvidenceRef,
|
|
5
|
+
} from "./types"
|
|
6
|
+
|
|
7
|
+
// ── Deterministic behaviour mapping from source findings ─────────────────────
|
|
8
|
+
|
|
9
|
+
export function mapBehaviorsFromFindings(
|
|
10
|
+
findings: ProductSourceFinding[],
|
|
11
|
+
input: BehaviorMapInput = {},
|
|
12
|
+
): BehaviorMap {
|
|
13
|
+
const behaviors: ProductBehavior[] = []
|
|
14
|
+
const gaps: ProductBehaviorGap[] = []
|
|
15
|
+
const contradictions: ProductBehaviorContradiction[] = []
|
|
16
|
+
|
|
17
|
+
// Group CLI commands into behaviours
|
|
18
|
+
const cliFindings = findings.filter(f => f.kind === "cli")
|
|
19
|
+
for (const f of cliFindings) {
|
|
20
|
+
behaviors.push({
|
|
21
|
+
id: `behavior-cli-${f.id}`,
|
|
22
|
+
area: inferArea(f),
|
|
23
|
+
name: f.title,
|
|
24
|
+
description: f.summary,
|
|
25
|
+
current_behavior: f.summary,
|
|
26
|
+
evidence: [findingToRef(f)],
|
|
27
|
+
basis: ["implemented"],
|
|
28
|
+
confidence: f.confidence,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Extract documented user flows from docs findings
|
|
33
|
+
const docsFindings = findings.filter(f => f.kind === "docs" && f.tags.includes("cli"))
|
|
34
|
+
for (const f of docsFindings) {
|
|
35
|
+
// Only add if not already covered by a CLI finding
|
|
36
|
+
const alreadyPresent = behaviors.some(b =>
|
|
37
|
+
b.current_behavior.toLowerCase().includes(extractCommand(f.summary).toLowerCase()) &&
|
|
38
|
+
extractCommand(f.summary).length > 3,
|
|
39
|
+
)
|
|
40
|
+
if (!alreadyPresent && extractCommand(f.summary)) {
|
|
41
|
+
behaviors.push({
|
|
42
|
+
id: `behavior-docs-${f.id}`,
|
|
43
|
+
area: inferArea(f),
|
|
44
|
+
name: `Documented: ${f.title}`,
|
|
45
|
+
description: f.summary,
|
|
46
|
+
current_behavior: f.summary,
|
|
47
|
+
evidence: [findingToRef(f)],
|
|
48
|
+
basis: ["documented"],
|
|
49
|
+
confidence: f.confidence * 0.9,
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Cross-reference: documented claims without implementation
|
|
55
|
+
const docsHeadings = findings.filter(f => f.kind === "docs" && !f.tags.includes("cli"))
|
|
56
|
+
const implementedAreas = new Set(behaviors.map(b => b.area))
|
|
57
|
+
|
|
58
|
+
// Detect gaps: central product promises with no CLI surface
|
|
59
|
+
const EXPECTED_AREAS = ["onboarding", "chronicle", "advisor", "review"]
|
|
60
|
+
for (const expected of EXPECTED_AREAS) {
|
|
61
|
+
const hasBehavior = behaviors.some(b => b.area === expected || b.name.toLowerCase().includes(expected))
|
|
62
|
+
if (!hasBehavior) {
|
|
63
|
+
const docRef = docsHeadings.find(f => f.summary.toLowerCase().includes(expected))
|
|
64
|
+
gaps.push({
|
|
65
|
+
id: `gap-${expected}`,
|
|
66
|
+
area: expected,
|
|
67
|
+
gap: `No first-class CLI command found for '${expected}'.`,
|
|
68
|
+
why_it_matters: `'${expected}' appears in product docs but has no dedicated CLI surface.`,
|
|
69
|
+
evidence: docRef ? [findingToRef(docRef)] : [],
|
|
70
|
+
confidence: 0.7,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Gap: no product-direction module (Compass itself)
|
|
76
|
+
const hasCompass = behaviors.some(b => b.name.toLowerCase().includes("compass"))
|
|
77
|
+
if (!hasCompass) {
|
|
78
|
+
gaps.push({
|
|
79
|
+
id: "gap-product-direction",
|
|
80
|
+
area: "product direction",
|
|
81
|
+
gap: "No product behaviour mapping or direction module currently exists.",
|
|
82
|
+
why_it_matters: "Quorum helps agents avoid repeating engineering mistakes, but has no module to help avoid repeating product-direction mistakes.",
|
|
83
|
+
evidence: [],
|
|
84
|
+
confidence: 0.93,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Filter by area if provided
|
|
89
|
+
const filteredBehaviors = input.area
|
|
90
|
+
? behaviors.filter(b =>
|
|
91
|
+
b.area.toLowerCase().includes(input.area!.toLowerCase()) ||
|
|
92
|
+
b.name.toLowerCase().includes(input.area!.toLowerCase()),
|
|
93
|
+
)
|
|
94
|
+
: behaviors
|
|
95
|
+
|
|
96
|
+
const overallConfidence = filteredBehaviors.length === 0
|
|
97
|
+
? 0.5
|
|
98
|
+
: filteredBehaviors.reduce((s, b) => s + b.confidence, 0) / filteredBehaviors.length
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
generated_at: new Date().toISOString(),
|
|
102
|
+
area: input.area,
|
|
103
|
+
behaviors: filteredBehaviors,
|
|
104
|
+
gaps: input.area
|
|
105
|
+
? gaps.filter(g => g.area.toLowerCase().includes(input.area!.toLowerCase()))
|
|
106
|
+
: gaps,
|
|
107
|
+
contradictions,
|
|
108
|
+
confidence: Math.round(overallConfidence * 100) / 100,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Extract documented behaviors for LLM context ─────────────────────────────
|
|
113
|
+
|
|
114
|
+
export function summarizeBehaviorMap(map: BehaviorMap): string {
|
|
115
|
+
const lines: string[] = []
|
|
116
|
+
|
|
117
|
+
if (map.behaviors.length > 0) {
|
|
118
|
+
lines.push("## Current behaviours")
|
|
119
|
+
for (const b of map.behaviors.slice(0, 20)) {
|
|
120
|
+
lines.push(` ✓ ${b.current_behavior.slice(0, 100)}`)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (map.gaps.length > 0) {
|
|
125
|
+
lines.push("\n## Gaps")
|
|
126
|
+
for (const g of map.gaps) {
|
|
127
|
+
lines.push(` ? ${g.gap}`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return lines.join("\n")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function findingToRef(f: ProductSourceFinding): CompassEvidenceRef {
|
|
137
|
+
return {
|
|
138
|
+
id: f.id,
|
|
139
|
+
kind: f.kind,
|
|
140
|
+
source: f.source,
|
|
141
|
+
path: f.path,
|
|
142
|
+
summary: f.summary,
|
|
143
|
+
confidence: f.confidence,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function inferArea(f: ProductSourceFinding): string {
|
|
148
|
+
if (f.tags.includes("onboarding") || f.tags.includes("init")) return "onboarding"
|
|
149
|
+
if (f.tags.includes("chronicle") || f.tags.includes("commit") || f.tags.includes("proposal")) return "chronicle review"
|
|
150
|
+
if (f.tags.includes("advisor")) return "memory retrieval"
|
|
151
|
+
if (f.tags.includes("sentinel")) return "coverage"
|
|
152
|
+
if (f.tags.includes("compass")) return "product direction"
|
|
153
|
+
if (f.tags.includes("auth")) return "auth"
|
|
154
|
+
if (f.tags.includes("cli")) return "cli"
|
|
155
|
+
return "general"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function extractCommand(text: string): string {
|
|
159
|
+
const match = text.match(/`(quorum [^`]+)`/)
|
|
160
|
+
return match?.[1] ?? ""
|
|
161
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { randomUUID } from "crypto"
|
|
2
|
+
import type { LLMProvider } from "../shared/types"
|
|
3
|
+
import type {
|
|
4
|
+
Compass, CreateCompassOptions,
|
|
5
|
+
CompassBrief, CompassBriefInput,
|
|
6
|
+
BehaviorMap, BehaviorMapInput,
|
|
7
|
+
BehaviorAnswer, BehaviorQuestionInput,
|
|
8
|
+
ProductOpportunity, OpportunitiesInput,
|
|
9
|
+
ProductPathway, PathwaysInput,
|
|
10
|
+
ProductBet, BigBetsInput,
|
|
11
|
+
ProductIdeaScore, ScoreIdeaInput,
|
|
12
|
+
ProductBrief, ProductBriefInput,
|
|
13
|
+
CompassProposalInput, CompassProposalResult,
|
|
14
|
+
CompassOutcomeInput, CompassOutcomeResultPayload,
|
|
15
|
+
} from "./types"
|
|
16
|
+
import { defaultSources } from "./sources/index"
|
|
17
|
+
import { collectBearings, collectTerrain, formatBearingsForPrompt, formatTerrainForPrompt } from "./evidence/collect"
|
|
18
|
+
import { mapBehaviorsFromFindings, summarizeBehaviorMap } from "./behavior"
|
|
19
|
+
import { computeScore, scoreToRecommendation, explainScore } from "./score"
|
|
20
|
+
import { stageProposal, stageOutcome } from "./propose"
|
|
21
|
+
import { COMPASS_SYSTEM_PROMPT } from "./prompts/system"
|
|
22
|
+
import {
|
|
23
|
+
buildBriefPrompt, buildPathwaysPrompt,
|
|
24
|
+
buildBetsPrompt, buildScorePrompt,
|
|
25
|
+
} from "./prompts/index"
|
|
26
|
+
import {
|
|
27
|
+
CompassBriefLLMSchema, PathwaysLLMSchema,
|
|
28
|
+
BetsLLMSchema, ProductIdeaScoreSchema,
|
|
29
|
+
} from "./schemas"
|
|
30
|
+
|
|
31
|
+
function parseLLMJson(raw: string): unknown {
|
|
32
|
+
const cleaned = raw.replace(/^```(?:json)?\s*/m, "").replace(/\s*```$/m, "").trim()
|
|
33
|
+
return JSON.parse(cleaned)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function callLLM(
|
|
37
|
+
llm: LLMProvider | undefined,
|
|
38
|
+
userPrompt: string,
|
|
39
|
+
model?: string,
|
|
40
|
+
): Promise<string> {
|
|
41
|
+
if (!llm) throw new Error("Compass: LLM provider is required for this command. Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or pass llm to setup().")
|
|
42
|
+
return llm(
|
|
43
|
+
[
|
|
44
|
+
{ role: "system" as const, content: COMPASS_SYSTEM_PROMPT },
|
|
45
|
+
{ role: "user" as const, content: userPrompt },
|
|
46
|
+
],
|
|
47
|
+
model,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createCompass(options: CreateCompassOptions): Compass {
|
|
52
|
+
const {
|
|
53
|
+
oracle,
|
|
54
|
+
llm,
|
|
55
|
+
rootDir = process.cwd(),
|
|
56
|
+
chronicleDir = ".chronicle",
|
|
57
|
+
sources = defaultSources(),
|
|
58
|
+
models = {},
|
|
59
|
+
minimumEvidence = "balanced",
|
|
60
|
+
} = options
|
|
61
|
+
|
|
62
|
+
// ── Shared context helpers ─────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
async function getContext(area?: string) {
|
|
65
|
+
const [bearings, terrain] = await Promise.all([
|
|
66
|
+
collectBearings(oracle, area),
|
|
67
|
+
collectTerrain(sources, rootDir, area),
|
|
68
|
+
])
|
|
69
|
+
const chronicleContext = formatBearingsForPrompt(bearings)
|
|
70
|
+
const behaviorContext = formatTerrainForPrompt(terrain.findings)
|
|
71
|
+
const behaviorMap = mapBehaviorsFromFindings(terrain.findings, { area })
|
|
72
|
+
return { bearings, terrain, chronicleContext, behaviorContext, behaviorMap }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── brief ──────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
async function brief(input?: CompassBriefInput): Promise<CompassBrief> {
|
|
78
|
+
const { chronicleContext, behaviorContext, terrain, behaviorMap } = await getContext(input?.area)
|
|
79
|
+
|
|
80
|
+
if (!llm) {
|
|
81
|
+
// Deterministic fallback
|
|
82
|
+
return {
|
|
83
|
+
generated_at: new Date().toISOString(),
|
|
84
|
+
area: input?.area,
|
|
85
|
+
product_direction: "Unable to synthesize direction — no LLM configured. See Chronicle and behaviour map for raw evidence.",
|
|
86
|
+
known_from_chronicle: [],
|
|
87
|
+
known_from_behavior: behaviorMap.behaviors.slice(0, 5).map(b => b.current_behavior),
|
|
88
|
+
inferred: [],
|
|
89
|
+
assumptions: [],
|
|
90
|
+
unknowns: ["LLM not configured — full synthesis unavailable."],
|
|
91
|
+
opportunities: [],
|
|
92
|
+
missing_evidence: ["LLM provider required for synthesis"],
|
|
93
|
+
recommended_next_step: "Run: quorum advisor brief",
|
|
94
|
+
confidence: 0.4,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const raw = await callLLM(llm, buildBriefPrompt({ chronicleContext, behaviorContext, area: input?.area }), models.brief)
|
|
99
|
+
let parsed: unknown
|
|
100
|
+
try { parsed = parseLLMJson(raw) } catch {
|
|
101
|
+
throw new Error(`Compass brief: LLM returned non-JSON. Raw (first 300): ${raw.slice(0, 300)}`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const result = CompassBriefLLMSchema.safeParse(parsed)
|
|
105
|
+
if (!result.success) throw new Error(`Compass brief: LLM output failed validation: ${JSON.stringify(result.error.issues)}`)
|
|
106
|
+
|
|
107
|
+
const d = result.data
|
|
108
|
+
// Build opportunity items from behavior map gaps
|
|
109
|
+
const opportunities: ProductOpportunity[] = behaviorMap.gaps.slice(0, 3).map((g, i) => ({
|
|
110
|
+
id: `opp-gap-${i}`,
|
|
111
|
+
title: g.gap,
|
|
112
|
+
area: g.area,
|
|
113
|
+
why_it_matters: g.why_it_matters,
|
|
114
|
+
evidence_strength: "inferred" as const,
|
|
115
|
+
suggested_next_step: "Run: quorum compass map",
|
|
116
|
+
evidence: g.evidence,
|
|
117
|
+
confidence: g.confidence,
|
|
118
|
+
}))
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
generated_at: new Date().toISOString(),
|
|
122
|
+
area: input?.area,
|
|
123
|
+
...d,
|
|
124
|
+
opportunities,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── mapBehaviors ────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
async function mapBehaviors(input?: BehaviorMapInput): Promise<BehaviorMap> {
|
|
131
|
+
const terrain = await collectTerrain(sources, rootDir, input?.area)
|
|
132
|
+
return mapBehaviorsFromFindings(terrain.findings, input)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── behavior (single question) ──────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
async function behavior(input: BehaviorQuestionInput): Promise<BehaviorAnswer> {
|
|
138
|
+
const terrain = await collectTerrain(sources, rootDir, input.area)
|
|
139
|
+
const map = mapBehaviorsFromFindings(terrain.findings, { area: input.area })
|
|
140
|
+
|
|
141
|
+
const relevant = terrain.findings.filter(f =>
|
|
142
|
+
f.summary.toLowerCase().includes(input.question.toLowerCase().split(" ").slice(0, 3).join(" ")) ||
|
|
143
|
+
f.tags.some(t => input.question.toLowerCase().includes(t)),
|
|
144
|
+
).slice(0, 10)
|
|
145
|
+
|
|
146
|
+
if (!llm || input.deterministic) {
|
|
147
|
+
// Deterministic answer from behaviour map
|
|
148
|
+
const what_exists = map.behaviors.slice(0, 6).map(b => b.current_behavior)
|
|
149
|
+
const what_missing = map.gaps.slice(0, 4).map(g => g.gap)
|
|
150
|
+
return {
|
|
151
|
+
question: input.question,
|
|
152
|
+
what_exists,
|
|
153
|
+
what_appears_missing: what_missing,
|
|
154
|
+
product_implication: map.gaps.length > 0
|
|
155
|
+
? `The area has ${map.behaviors.length} documented behaviours but ${map.gaps.length} notable gaps.`
|
|
156
|
+
: `The area appears well-covered with ${map.behaviors.length} documented behaviours.`,
|
|
157
|
+
evidence: relevant.map(f => ({ id: f.id, kind: f.kind, source: f.source, path: f.path, summary: f.summary, confidence: f.confidence })),
|
|
158
|
+
confidence: map.confidence,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// LLM-backed synthesis
|
|
163
|
+
const contextLines = [
|
|
164
|
+
"## Behaviours found",
|
|
165
|
+
...map.behaviors.slice(0, 10).map(b => ` ✓ ${b.current_behavior}`),
|
|
166
|
+
"## Gaps found",
|
|
167
|
+
...map.gaps.slice(0, 5).map(g => ` ? ${g.gap}`),
|
|
168
|
+
].join("\n")
|
|
169
|
+
|
|
170
|
+
const prompt = `Answer this product-behaviour question by combining repo evidence with known behaviours.
|
|
171
|
+
|
|
172
|
+
Question: ${input.question}
|
|
173
|
+
|
|
174
|
+
${contextLines}
|
|
175
|
+
|
|
176
|
+
Return JSON:
|
|
177
|
+
{
|
|
178
|
+
"question": "${input.question}",
|
|
179
|
+
"what_exists": ["<existing behaviour>"],
|
|
180
|
+
"what_appears_missing": ["<missing capability>"],
|
|
181
|
+
"product_implication": "<one sentence product implication>",
|
|
182
|
+
"evidence": [{ "id": "<id>", "kind": "<kind>", "source": "<source>", "summary": "<summary>", "confidence": <0–1> }],
|
|
183
|
+
"confidence": <0–1>
|
|
184
|
+
}`
|
|
185
|
+
|
|
186
|
+
const raw = await callLLM(llm, prompt, models.brief)
|
|
187
|
+
try {
|
|
188
|
+
const parsed = parseLLMJson(raw) as BehaviorAnswer
|
|
189
|
+
return { ...parsed, question: input.question }
|
|
190
|
+
} catch {
|
|
191
|
+
// Fall back to deterministic
|
|
192
|
+
return behavior({ ...input, deterministic: true })
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── opportunities ───────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
async function opportunities(input?: OpportunitiesInput): Promise<ProductOpportunity[]> {
|
|
199
|
+
const terrain = await collectTerrain(sources, rootDir, input?.area)
|
|
200
|
+
const map = mapBehaviorsFromFindings(terrain.findings, { area: input?.area })
|
|
201
|
+
|
|
202
|
+
const opps: ProductOpportunity[] = map.gaps.map((g, i) => ({
|
|
203
|
+
id: `opp-${i}`,
|
|
204
|
+
title: g.gap,
|
|
205
|
+
area: g.area,
|
|
206
|
+
why_it_matters: g.why_it_matters,
|
|
207
|
+
evidence_strength: g.confidence >= 0.7 ? "strong" as const
|
|
208
|
+
: g.confidence >= 0.5 ? "medium" as const
|
|
209
|
+
: "inferred" as const,
|
|
210
|
+
suggested_next_step: `quorum compass pathways --goal "${g.gap.slice(0, 50)}"`,
|
|
211
|
+
evidence: g.evidence,
|
|
212
|
+
confidence: g.confidence,
|
|
213
|
+
}))
|
|
214
|
+
|
|
215
|
+
const limited = input?.limit ? opps.slice(0, input.limit) : opps
|
|
216
|
+
return input?.goal
|
|
217
|
+
? limited.filter(o => o.title.toLowerCase().includes(input.goal!.toLowerCase()) || o.area.toLowerCase().includes(input.goal!.toLowerCase()))
|
|
218
|
+
: limited
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── pathways ────────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
async function pathways(input: PathwaysInput): Promise<ProductPathway[]> {
|
|
224
|
+
const { chronicleContext, behaviorContext } = await getContext(input.area)
|
|
225
|
+
const raw = await callLLM(
|
|
226
|
+
llm,
|
|
227
|
+
buildPathwaysPrompt({ ...input, chronicleContext, behaviorContext, limit: input.limit ?? 5 }),
|
|
228
|
+
models.pathways,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
let parsed: unknown
|
|
232
|
+
try { parsed = parseLLMJson(raw) } catch {
|
|
233
|
+
throw new Error(`Compass pathways: LLM returned non-JSON. Raw (first 300): ${raw.slice(0, 300)}`)
|
|
234
|
+
}
|
|
235
|
+
const result = PathwaysLLMSchema.safeParse(parsed)
|
|
236
|
+
if (!result.success) throw new Error(`Compass pathways: LLM output failed validation: ${JSON.stringify(result.error.issues)}`)
|
|
237
|
+
|
|
238
|
+
// Recompute scores deterministically
|
|
239
|
+
return result.data.pathways.map(p => ({
|
|
240
|
+
...p,
|
|
241
|
+
scores: computeScore(p.scores),
|
|
242
|
+
}))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── bigBets ─────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
async function bigBets(input: BigBetsInput): Promise<ProductBet[]> {
|
|
248
|
+
const { chronicleContext, behaviorContext } = await getContext()
|
|
249
|
+
const raw = await callLLM(
|
|
250
|
+
llm,
|
|
251
|
+
buildBetsPrompt({ ...input, chronicleContext, behaviorContext }),
|
|
252
|
+
models.bets,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
let parsed: unknown
|
|
256
|
+
try { parsed = parseLLMJson(raw) } catch {
|
|
257
|
+
throw new Error(`Compass bets: LLM returned non-JSON. Raw (first 300): ${raw.slice(0, 300)}`)
|
|
258
|
+
}
|
|
259
|
+
const result = BetsLLMSchema.safeParse(parsed)
|
|
260
|
+
if (!result.success) throw new Error(`Compass bets: LLM output failed validation: ${JSON.stringify(result.error.issues)}`)
|
|
261
|
+
|
|
262
|
+
return result.data.bets.map(b => ({
|
|
263
|
+
...b,
|
|
264
|
+
scores: computeScore(b.scores),
|
|
265
|
+
}))
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── scoreIdea ───────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
async function scoreIdea(input: ScoreIdeaInput): Promise<ProductIdeaScore> {
|
|
271
|
+
const { chronicleContext, behaviorContext } = await getContext()
|
|
272
|
+
const raw = await callLLM(
|
|
273
|
+
llm,
|
|
274
|
+
buildScorePrompt({ ...input, chronicleContext, behaviorContext }),
|
|
275
|
+
models.score,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
let parsed: unknown
|
|
279
|
+
try { parsed = parseLLMJson(raw) } catch {
|
|
280
|
+
throw new Error(`Compass score: LLM returned non-JSON. Raw (first 300): ${raw.slice(0, 300)}`)
|
|
281
|
+
}
|
|
282
|
+
const result = ProductIdeaScoreSchema.safeParse(parsed)
|
|
283
|
+
if (!result.success) throw new Error(`Compass score: LLM output failed validation: ${JSON.stringify(result.error.issues)}`)
|
|
284
|
+
|
|
285
|
+
const d = result.data
|
|
286
|
+
return {
|
|
287
|
+
...d,
|
|
288
|
+
scores: computeScore(d.scores),
|
|
289
|
+
recommendation: scoreToRecommendation(d.scores.total),
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── productBrief ─────────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
async function productBrief(input: ProductBriefInput): Promise<ProductBrief> {
|
|
296
|
+
const { chronicleContext, behaviorContext, terrain } = await getContext()
|
|
297
|
+
|
|
298
|
+
const prompt = `Generate a product brief for implementation planning.
|
|
299
|
+
|
|
300
|
+
Title: ${input.title}
|
|
301
|
+
${input.idea ? `Idea: ${input.idea}` : ""}
|
|
302
|
+
${input.context ? `Context: ${input.context}` : ""}
|
|
303
|
+
|
|
304
|
+
## Chronicle evidence
|
|
305
|
+
${chronicleContext}
|
|
306
|
+
|
|
307
|
+
## Current behaviour
|
|
308
|
+
${behaviorContext}
|
|
309
|
+
|
|
310
|
+
Return JSON:
|
|
311
|
+
{
|
|
312
|
+
"title": "${input.title}",
|
|
313
|
+
"problem": "<problem being solved>",
|
|
314
|
+
"target_user": "<who>",
|
|
315
|
+
"current_behavior": ["<relevant current behaviour>"],
|
|
316
|
+
"product_opportunity": "<gap or need>",
|
|
317
|
+
"recommended_solution": "<recommended approach>",
|
|
318
|
+
"smallest_useful_version": "<minimum useful version>",
|
|
319
|
+
"non_goals": ["<explicit non-goal>"],
|
|
320
|
+
"user_flow": ["<step in user flow>"],
|
|
321
|
+
"implementation_notes": ["<implementation note>"],
|
|
322
|
+
"dependencies": ["<dependency>"],
|
|
323
|
+
"risks": ["<risk>"],
|
|
324
|
+
"open_questions": ["<open question>"],
|
|
325
|
+
"assumptions": ["<assumption>"],
|
|
326
|
+
"validation_signals": ["<signal>"],
|
|
327
|
+
"invalidation_signals": ["<signal>"],
|
|
328
|
+
"evidence": [{ "id": "<id>", "kind": "<kind>", "source": "<source>", "summary": "<summary>", "confidence": <0–1> }],
|
|
329
|
+
"suggested_quorum_checks": ["<quorum check command>"]
|
|
330
|
+
}`
|
|
331
|
+
|
|
332
|
+
const raw = await callLLM(llm, prompt, models.brief)
|
|
333
|
+
try {
|
|
334
|
+
const parsed = parseLLMJson(raw) as ProductBrief
|
|
335
|
+
return parsed
|
|
336
|
+
} catch {
|
|
337
|
+
throw new Error(`Compass productBrief: LLM returned non-JSON. Raw (first 300): ${raw.slice(0, 300)}`)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── propose ──────────────────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
async function propose(input: CompassProposalInput): Promise<CompassProposalResult> {
|
|
344
|
+
return stageProposal(input, oracle)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── outcome ──────────────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
async function outcome(input: CompassOutcomeInput): Promise<CompassOutcomeResultPayload> {
|
|
350
|
+
return stageOutcome(input, oracle)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
brief,
|
|
355
|
+
mapBehaviors,
|
|
356
|
+
behavior,
|
|
357
|
+
opportunities,
|
|
358
|
+
pathways,
|
|
359
|
+
bigBets,
|
|
360
|
+
scoreIdea,
|
|
361
|
+
productBrief,
|
|
362
|
+
propose,
|
|
363
|
+
outcome,
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { OracleClient, OracleResult } from "../../shared/types"
|
|
2
|
+
import type { CompassEvidenceRef, ProductBearing, ProductSource, ProductSourceFinding } from "../types"
|
|
3
|
+
import { randomUUID } from "crypto"
|
|
4
|
+
|
|
5
|
+
// ── Bearings from Chronicle ───────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export async function collectBearings(
|
|
8
|
+
oracle: OracleClient,
|
|
9
|
+
area?: string,
|
|
10
|
+
): Promise<ProductBearing[]> {
|
|
11
|
+
const queries = [
|
|
12
|
+
area ?? "product direction goals decisions",
|
|
13
|
+
"rejected approaches refuted alternatives",
|
|
14
|
+
"constraints scope out-of-scope",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
const seen = new Set<string>()
|
|
18
|
+
const bearings: ProductBearing[] = []
|
|
19
|
+
|
|
20
|
+
for (const q of queries) {
|
|
21
|
+
let results: OracleResult[]
|
|
22
|
+
try { results = await oracle.query(q, { limit: 8 }) } catch { continue }
|
|
23
|
+
|
|
24
|
+
for (const entry of results) {
|
|
25
|
+
if (seen.has(entry.id)) continue
|
|
26
|
+
seen.add(entry.id)
|
|
27
|
+
|
|
28
|
+
const text = entry.decision ?? entry.key_insight
|
|
29
|
+
bearings.push({
|
|
30
|
+
id: `bearing-${entry.id.slice(0, 8)}`,
|
|
31
|
+
title: entry.topic ?? text.slice(0, 60),
|
|
32
|
+
summary: text,
|
|
33
|
+
area: entry.scope?.[0],
|
|
34
|
+
evidence: [
|
|
35
|
+
{
|
|
36
|
+
id: randomUUID().slice(0, 8),
|
|
37
|
+
kind: "chronicle",
|
|
38
|
+
source: ".chronicle/committed",
|
|
39
|
+
entry_id: entry.id,
|
|
40
|
+
summary: text,
|
|
41
|
+
confidence: entry.confidence,
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
confidence: entry.confidence,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return bearings
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Terrain from source scanners ──────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export interface TerrainResult {
|
|
55
|
+
findings: ProductSourceFinding[]
|
|
56
|
+
evidenceRefs: CompassEvidenceRef[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function collectTerrain(
|
|
60
|
+
sources: ProductSource[],
|
|
61
|
+
rootDir: string,
|
|
62
|
+
area?: string,
|
|
63
|
+
): Promise<TerrainResult> {
|
|
64
|
+
const allFindings: ProductSourceFinding[] = []
|
|
65
|
+
|
|
66
|
+
for (const source of sources) {
|
|
67
|
+
const found = await source.scan({ rootDir, area })
|
|
68
|
+
allFindings.push(...found)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const evidenceRefs: CompassEvidenceRef[] = allFindings.map(f => ({
|
|
72
|
+
id: f.id,
|
|
73
|
+
kind: f.kind,
|
|
74
|
+
source: f.source,
|
|
75
|
+
path: f.path,
|
|
76
|
+
line: f.line,
|
|
77
|
+
summary: f.summary,
|
|
78
|
+
confidence: f.confidence,
|
|
79
|
+
}))
|
|
80
|
+
|
|
81
|
+
return { findings: allFindings, evidenceRefs }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Format helpers for LLM prompts ───────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export function formatBearingsForPrompt(bearings: ProductBearing[]): string {
|
|
87
|
+
if (bearings.length === 0) return "No Chronicle entries found."
|
|
88
|
+
return bearings
|
|
89
|
+
.map(b => `[${b.id}] (confidence: ${b.confidence.toFixed(2)}) ${b.summary}`)
|
|
90
|
+
.join("\n")
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function formatTerrainForPrompt(findings: ProductSourceFinding[], limit = 40): string {
|
|
94
|
+
if (findings.length === 0) return "No product behaviour found in sources."
|
|
95
|
+
|
|
96
|
+
// Group by kind
|
|
97
|
+
const groups: Record<string, ProductSourceFinding[]> = {}
|
|
98
|
+
for (const f of findings.slice(0, limit)) {
|
|
99
|
+
groups[f.kind] = groups[f.kind] ?? []
|
|
100
|
+
groups[f.kind].push(f)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return Object.entries(groups)
|
|
104
|
+
.map(([kind, items]) => {
|
|
105
|
+
const lines = items.slice(0, 10).map(i => ` - ${i.title}: ${i.summary.slice(0, 100)}`)
|
|
106
|
+
return `${kind.toUpperCase()}:\n${lines.join("\n")}`
|
|
107
|
+
})
|
|
108
|
+
.join("\n\n")
|
|
109
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { createCompass } from "./create"
|
|
2
|
+
export { defaultSources, docsSource, packageSource, cliSource, repoSource, testsSource, configSource } from "./sources/index"
|
|
3
|
+
export { mapBehaviorsFromFindings, summarizeBehaviorMap } from "./behavior"
|
|
4
|
+
export { computeScore, scoreToRecommendation, scoreToLabel, explainScore } from "./score"
|
|
5
|
+
export { collectBearings, collectTerrain, formatBearingsForPrompt, formatTerrainForPrompt } from "./evidence/collect"
|
|
6
|
+
export { stageProposal, stageOutcome } from "./propose"
|
|
7
|
+
export * from "./types"
|