@balpal4495/quorum 2.0.0 → 3.0.0
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 +422 -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,230 @@
|
|
|
1
|
+
export function buildBriefPrompt(params: {
|
|
2
|
+
chronicleContext: string
|
|
3
|
+
behaviorContext: string
|
|
4
|
+
area?: string
|
|
5
|
+
}): string {
|
|
6
|
+
return `Produce a Compass Brief — a summary of current product direction.
|
|
7
|
+
|
|
8
|
+
${params.area ? `Focus area: ${params.area}\n` : ""}
|
|
9
|
+
|
|
10
|
+
## Chronicle evidence (approved project memory)
|
|
11
|
+
${params.chronicleContext}
|
|
12
|
+
|
|
13
|
+
## Current product behaviour
|
|
14
|
+
${params.behaviorContext}
|
|
15
|
+
|
|
16
|
+
Return ONLY valid JSON with this exact schema (no markdown fences, no explanation):
|
|
17
|
+
{
|
|
18
|
+
"product_direction": "<one clear sentence describing where the product is pointed>",
|
|
19
|
+
"known_from_chronicle": ["<fact from Chronicle>"],
|
|
20
|
+
"known_from_behavior": ["<fact from code/docs/tests>"],
|
|
21
|
+
"inferred": ["<inference — not stated directly in evidence>"],
|
|
22
|
+
"assumptions": ["<assumption being made>"],
|
|
23
|
+
"unknowns": ["<what is not known — especially user behaviour>"],
|
|
24
|
+
"missing_evidence": ["<what evidence would improve this brief>"],
|
|
25
|
+
"recommended_next_step": "<specific quorum command or action>",
|
|
26
|
+
"confidence": <number 0–1>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Rules:
|
|
30
|
+
- known_from_chronicle must cite specific decisions, not paraphrase
|
|
31
|
+
- unknowns must include "No analytics or support evidence connected" if no user data provided
|
|
32
|
+
- confidence reflects how strongly the direction can be stated from available evidence`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildPathwaysPrompt(params: {
|
|
36
|
+
goal: string
|
|
37
|
+
horizon?: string
|
|
38
|
+
appetite?: string
|
|
39
|
+
chronicleContext: string
|
|
40
|
+
behaviorContext: string
|
|
41
|
+
area?: string
|
|
42
|
+
limit?: number
|
|
43
|
+
}): string {
|
|
44
|
+
return `Generate ${params.limit ?? 5} product pathways toward the following goal.
|
|
45
|
+
|
|
46
|
+
Goal: ${params.goal}
|
|
47
|
+
${params.horizon ? `Horizon: ${params.horizon}` : ""}
|
|
48
|
+
${params.appetite ? `Appetite: ${params.appetite}` : ""}
|
|
49
|
+
${params.area ? `Focus area: ${params.area}` : ""}
|
|
50
|
+
|
|
51
|
+
## Chronicle evidence (approved project memory)
|
|
52
|
+
${params.chronicleContext}
|
|
53
|
+
|
|
54
|
+
## Current product behaviour
|
|
55
|
+
${params.behaviorContext}
|
|
56
|
+
|
|
57
|
+
Return ONLY valid JSON with this exact schema (no markdown fences, no explanation):
|
|
58
|
+
{
|
|
59
|
+
"pathways": [
|
|
60
|
+
{
|
|
61
|
+
"id": "<slug-id>",
|
|
62
|
+
"kind": "product_pathway",
|
|
63
|
+
"title": "<pathway title>",
|
|
64
|
+
"goal": "<goal this pathway serves>",
|
|
65
|
+
"target_user": "<who this is for>",
|
|
66
|
+
"problem": "<user problem being solved>",
|
|
67
|
+
"current_behaviors": ["<existing behaviour this builds on>"],
|
|
68
|
+
"opportunity": "<what gap or need this addresses>",
|
|
69
|
+
"why_now": "<why this makes sense at this stage>",
|
|
70
|
+
"smallest_useful_version": "<minimum version that validates the idea>",
|
|
71
|
+
"phases": [
|
|
72
|
+
{
|
|
73
|
+
"name": "<phase name>",
|
|
74
|
+
"outcome": "<what this phase delivers>",
|
|
75
|
+
"user_value": "<value to the user>",
|
|
76
|
+
"build_notes": ["<optional implementation note>"],
|
|
77
|
+
"dependencies": ["<dependency>"],
|
|
78
|
+
"risks": ["<risk>"]
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"dependencies": ["<dependency>"],
|
|
82
|
+
"risks": ["<risk>"],
|
|
83
|
+
"assumptions": ["<assumption — must be present>"],
|
|
84
|
+
"open_questions": ["<unanswered question>"],
|
|
85
|
+
"evidence": [
|
|
86
|
+
{
|
|
87
|
+
"id": "<evidence id>",
|
|
88
|
+
"kind": "chronicle|docs|cli|code|inference|assumption",
|
|
89
|
+
"source": "<source path or label>",
|
|
90
|
+
"summary": "<what this evidence says>",
|
|
91
|
+
"confidence": <0–1>
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"scores": {
|
|
95
|
+
"strategic_fit": <0–1>,
|
|
96
|
+
"user_problem_clarity": <0–1>,
|
|
97
|
+
"evidence_strength": <0–1>,
|
|
98
|
+
"leverage": <0–1>,
|
|
99
|
+
"feasibility": <0–1>,
|
|
100
|
+
"time_to_signal": <0–1>,
|
|
101
|
+
"reversibility": <0–1>,
|
|
102
|
+
"complexity_penalty": <0–1>,
|
|
103
|
+
"dependency_penalty": <0–1>,
|
|
104
|
+
"contradiction_penalty": <0–1>,
|
|
105
|
+
"evidence_gap_penalty": <0–1>,
|
|
106
|
+
"total": <0–100>
|
|
107
|
+
},
|
|
108
|
+
"confidence": <0–1>,
|
|
109
|
+
"time_to_signal": "<e.g. '1-2 sessions', '2 weeks'>",
|
|
110
|
+
"reversibility": "high|medium|low",
|
|
111
|
+
"suggested_next_step": "<specific actionable next step>"
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Rules:
|
|
117
|
+
- Every pathway must cite at least one evidence reference
|
|
118
|
+
- Reject generic feature ideas — every pathway must build on current behaviour or approved memory
|
|
119
|
+
- Compute scores.total using: strategic_fit*20 + user_problem_clarity*15 + evidence_strength*20 + leverage*10 + feasibility*15 + time_to_signal*10 + reversibility*10 - complexity_penalty*10 - dependency_penalty*8 - contradiction_penalty*15 - evidence_gap_penalty*12
|
|
120
|
+
- Clamp total to 0–100
|
|
121
|
+
- Assumptions must always be present (minimum 1)
|
|
122
|
+
- unknowns must note if no user evidence is connected
|
|
123
|
+
- Sort by scores.total descending`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildBetsPrompt(params: {
|
|
127
|
+
horizon?: string
|
|
128
|
+
goal?: string
|
|
129
|
+
appetite?: string
|
|
130
|
+
chronicleContext: string
|
|
131
|
+
behaviorContext: string
|
|
132
|
+
}): string {
|
|
133
|
+
return `Generate 2–3 strategic product bets.
|
|
134
|
+
|
|
135
|
+
${params.horizon ? `Horizon: ${params.horizon}` : ""}
|
|
136
|
+
${params.goal ? `Goal: ${params.goal}` : ""}
|
|
137
|
+
${params.appetite ? `Appetite: ${params.appetite}` : ""}
|
|
138
|
+
|
|
139
|
+
## Chronicle evidence
|
|
140
|
+
${params.chronicleContext}
|
|
141
|
+
|
|
142
|
+
## Current product behaviour
|
|
143
|
+
${params.behaviorContext}
|
|
144
|
+
|
|
145
|
+
Return ONLY valid JSON with this exact schema (no markdown fences):
|
|
146
|
+
{
|
|
147
|
+
"bets": [
|
|
148
|
+
{
|
|
149
|
+
"id": "<slug-id>",
|
|
150
|
+
"kind": "product_bet",
|
|
151
|
+
"title": "<bet title>",
|
|
152
|
+
"thesis": "<falsifiable product hypothesis — one sentence>",
|
|
153
|
+
"why_now": "<why this moment>",
|
|
154
|
+
"target_user": "<who>",
|
|
155
|
+
"upside": "<best case>",
|
|
156
|
+
"downside": "<realistic downside>",
|
|
157
|
+
"assumptions": ["<assumption — minimum 2>"],
|
|
158
|
+
"validation_signals": ["<signal that confirms the thesis>"],
|
|
159
|
+
"invalidation_signals": ["<signal that refutes the thesis>"],
|
|
160
|
+
"kill_criteria": ["<when to stop>"],
|
|
161
|
+
"first_experiment": "<smallest first test>",
|
|
162
|
+
"build_path": ["<phase 1>", "<phase 2>"],
|
|
163
|
+
"evidence": [{ "id": "<id>", "kind": "chronicle|inference|docs|cli", "source": "<source>", "summary": "<summary>", "confidence": <0–1> }],
|
|
164
|
+
"scores": {
|
|
165
|
+
"strategic_fit": <0–1>, "user_problem_clarity": <0–1>, "evidence_strength": <0–1>,
|
|
166
|
+
"leverage": <0–1>, "feasibility": <0–1>, "time_to_signal": <0–1>, "reversibility": <0–1>,
|
|
167
|
+
"complexity_penalty": <0–1>, "dependency_penalty": <0–1>, "contradiction_penalty": <0–1>,
|
|
168
|
+
"evidence_gap_penalty": <0–1>, "total": <0–100>
|
|
169
|
+
},
|
|
170
|
+
"confidence": <0–1>,
|
|
171
|
+
"time_to_signal": "<timeframe>",
|
|
172
|
+
"reversibility": "high|medium|low",
|
|
173
|
+
"appetite": "small|medium|large"
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
Rules:
|
|
179
|
+
- A bet is a falsifiable hypothesis, not a feature list
|
|
180
|
+
- Kill criteria must be present
|
|
181
|
+
- Invalidation signals must be present
|
|
182
|
+
- If no user evidence is available, evidence_strength should be ≤ 0.4`
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function buildScorePrompt(params: {
|
|
186
|
+
idea: string
|
|
187
|
+
context?: string
|
|
188
|
+
chronicleContext: string
|
|
189
|
+
behaviorContext: string
|
|
190
|
+
}): string {
|
|
191
|
+
return `Evaluate this product idea.
|
|
192
|
+
|
|
193
|
+
Idea: ${params.idea}
|
|
194
|
+
${params.context ? `Context: ${params.context}` : ""}
|
|
195
|
+
|
|
196
|
+
## Chronicle evidence
|
|
197
|
+
${params.chronicleContext}
|
|
198
|
+
|
|
199
|
+
## Current product behaviour
|
|
200
|
+
${params.behaviorContext}
|
|
201
|
+
|
|
202
|
+
Return ONLY valid JSON with this exact schema (no markdown fences):
|
|
203
|
+
{
|
|
204
|
+
"idea": "${params.idea}",
|
|
205
|
+
"summary": "<one sentence summary of what this idea is>",
|
|
206
|
+
"recommendation": "pursue|pursue-small-test|investigate-more|defer|avoid",
|
|
207
|
+
"scores": {
|
|
208
|
+
"strategic_fit": <0–1>, "user_problem_clarity": <0–1>, "evidence_strength": <0–1>,
|
|
209
|
+
"leverage": <0–1>, "feasibility": <0–1>, "time_to_signal": <0–1>, "reversibility": <0–1>,
|
|
210
|
+
"complexity_penalty": <0–1>, "dependency_penalty": <0–1>, "contradiction_penalty": <0–1>,
|
|
211
|
+
"evidence_gap_penalty": <0–1>, "total": <0–100>
|
|
212
|
+
},
|
|
213
|
+
"evidence": [{ "id": "<id>", "kind": "<kind>", "source": "<source>", "summary": "<summary>", "confidence": <0–1> }],
|
|
214
|
+
"supporting_reasons": ["<reason this scores well>"],
|
|
215
|
+
"risks": ["<risk>"],
|
|
216
|
+
"assumptions": ["<assumption>"],
|
|
217
|
+
"open_questions": ["<question>"],
|
|
218
|
+
"suggested_next_step": "<specific next action>"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Score total = strategic_fit*20 + user_problem_clarity*15 + evidence_strength*20 + leverage*10 + feasibility*15 + time_to_signal*10 + reversibility*10 - complexity_penalty*10 - dependency_penalty*8 - contradiction_penalty*15 - evidence_gap_penalty*12. Clamp 0–100.
|
|
222
|
+
|
|
223
|
+
Penalise:
|
|
224
|
+
- weak or missing evidence
|
|
225
|
+
- conflicts with Chronicle decisions
|
|
226
|
+
- high infrastructure/hosting/auth burden
|
|
227
|
+
- unclear user problem
|
|
228
|
+
- long time-to-signal
|
|
229
|
+
- low reversibility`
|
|
230
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const COMPASS_SYSTEM_PROMPT = `You are Quorum Compass, the product-direction module for an AI-assisted software team.
|
|
2
|
+
|
|
3
|
+
Your job is to help decide where the product should go next.
|
|
4
|
+
|
|
5
|
+
You are not a generic brainstormer.
|
|
6
|
+
You must ground every recommendation in provided evidence.
|
|
7
|
+
|
|
8
|
+
Evidence may come from:
|
|
9
|
+
- Chronicle memory (human-approved past decisions)
|
|
10
|
+
- current code behaviour
|
|
11
|
+
- docs
|
|
12
|
+
- tests
|
|
13
|
+
- package metadata
|
|
14
|
+
- CLI commands
|
|
15
|
+
|
|
16
|
+
Rules:
|
|
17
|
+
1. Separate known facts from inferences and assumptions.
|
|
18
|
+
2. Never claim user demand unless user evidence (analytics, support, issues) is provided.
|
|
19
|
+
3. Prefer small, reversible next moves unless asked for big bets.
|
|
20
|
+
4. Identify contradictions with Chronicle or current product behaviour.
|
|
21
|
+
5. Include assumptions, invalidation signals, and open questions.
|
|
22
|
+
6. Do not recommend implementation details beyond product-level guidance.
|
|
23
|
+
7. Return only valid JSON matching the requested schema.
|
|
24
|
+
8. When no analytics/support data is connected, always state: "No direct user signal connected."`
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { OracleClient } from "../shared/types"
|
|
2
|
+
import type {
|
|
3
|
+
CompassProposalInput, CompassProposalResult,
|
|
4
|
+
CompassOutcomeInput, CompassOutcomeResultPayload,
|
|
5
|
+
ProductPathway, ProductBet, ProductPathwayPhase,
|
|
6
|
+
} from "./types"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Stage a Compass-generated artifact as a Chronicle proposal.
|
|
10
|
+
*
|
|
11
|
+
* Writes to .chronicle/proposals/ ONLY via oracle.propose().
|
|
12
|
+
* Never calls oracle.commit(). Human must approve.
|
|
13
|
+
*/
|
|
14
|
+
export async function stageProposal(
|
|
15
|
+
input: CompassProposalInput,
|
|
16
|
+
oracle: OracleClient,
|
|
17
|
+
): Promise<CompassProposalResult> {
|
|
18
|
+
const payload = input.payload as ProductPathway | ProductBet
|
|
19
|
+
|
|
20
|
+
const decision = buildDecision(input)
|
|
21
|
+
const key_insight = decision.slice(0, 200)
|
|
22
|
+
|
|
23
|
+
const entry = {
|
|
24
|
+
key_insight,
|
|
25
|
+
decision,
|
|
26
|
+
schema_version: 2 as const,
|
|
27
|
+
topic: buildTopic(input),
|
|
28
|
+
scope: buildScope(input),
|
|
29
|
+
affected_areas: buildAffectedAreas(input),
|
|
30
|
+
status: "open" as const,
|
|
31
|
+
confidence: "confidence" in payload ? payload.confidence : 0.7,
|
|
32
|
+
source_module: "compass",
|
|
33
|
+
evidence_cited: [],
|
|
34
|
+
alternatives_considered: "assumptions" in payload ? [] : [],
|
|
35
|
+
rejected_reason: [],
|
|
36
|
+
validation_plan: buildValidationPlan(input),
|
|
37
|
+
review_after: reviewAfterDate(),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = await oracle.propose(entry)
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
proposal_id: result.proposalId,
|
|
44
|
+
message: `Staged Chronicle proposal ${result.proposalId.slice(0, 8)} — run 'quorum commit --list' to review.`,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Stage an outcome update for a prior product bet or pathway.
|
|
50
|
+
* Creates a new proposal that supersedes the original entry.
|
|
51
|
+
*/
|
|
52
|
+
export async function stageOutcome(
|
|
53
|
+
input: CompassOutcomeInput,
|
|
54
|
+
oracle: OracleClient,
|
|
55
|
+
): Promise<CompassOutcomeResultPayload> {
|
|
56
|
+
const resultLabel: Record<string, string> = {
|
|
57
|
+
"validated": "has been validated",
|
|
58
|
+
"partially-validated": "has been partially validated",
|
|
59
|
+
"invalidated": "has been invalidated",
|
|
60
|
+
"unclear": "outcome is unclear — insufficient signal",
|
|
61
|
+
"superseded": "has been superseded by a newer approach",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const label = resultLabel[input.result] ?? input.result
|
|
65
|
+
const decision = `Product bet/pathway ${input.entry_id.slice(0, 8)} ${label}.${input.note ? " " + input.note : ""}`
|
|
66
|
+
|
|
67
|
+
const entry = {
|
|
68
|
+
key_insight: decision.slice(0, 200),
|
|
69
|
+
decision,
|
|
70
|
+
schema_version: 2 as const,
|
|
71
|
+
topic: `product/outcome/${input.entry_id.slice(0, 8)}`,
|
|
72
|
+
scope: ["product", "compass", "outcome"],
|
|
73
|
+
affected_areas: [],
|
|
74
|
+
status: "validated" as const,
|
|
75
|
+
confidence: input.result === "validated" ? 0.9
|
|
76
|
+
: input.result === "partially-validated" ? 0.7
|
|
77
|
+
: input.result === "invalidated" ? 0.6
|
|
78
|
+
: 0.5,
|
|
79
|
+
source_module: "compass",
|
|
80
|
+
evidence_cited: [input.entry_id],
|
|
81
|
+
alternatives_considered: [],
|
|
82
|
+
rejected_reason: [],
|
|
83
|
+
validation_plan: [],
|
|
84
|
+
post_merge_result: (input.result === "validated" ? "successful"
|
|
85
|
+
: input.result === "invalidated" ? "rolled-back"
|
|
86
|
+
: input.result === "partially-validated" ? "partial"
|
|
87
|
+
: undefined) as "successful" | "bug" | "partial" | "rolled-back" | undefined,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = await oracle.propose(entry)
|
|
91
|
+
return {
|
|
92
|
+
proposal_id: result.proposalId,
|
|
93
|
+
message: `Staged outcome proposal ${result.proposalId.slice(0, 8)} — run 'quorum commit --list' to review.`,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
function buildDecision(input: CompassProposalInput): string {
|
|
100
|
+
const p = input.payload as ProductPathway | ProductBet
|
|
101
|
+
if (input.artifact_kind === "product_pathway" && "smallest_useful_version" in p) {
|
|
102
|
+
return `Compass identified '${(p as ProductPathway).title}' as a product pathway: ${(p as ProductPathway).smallest_useful_version}`
|
|
103
|
+
}
|
|
104
|
+
if (input.artifact_kind === "product_bet" && "thesis" in p) {
|
|
105
|
+
return `Product bet: ${(p as ProductBet).thesis}`
|
|
106
|
+
}
|
|
107
|
+
if ("title" in p) {
|
|
108
|
+
return `Compass ${input.artifact_kind.replace("_", " ")}: ${(p as { title: string }).title}`
|
|
109
|
+
}
|
|
110
|
+
return `Compass generated ${input.artifact_kind} artifact.`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildTopic(input: CompassProposalInput): string {
|
|
114
|
+
const p = input.payload
|
|
115
|
+
if ("title" in p) return `product/${input.artifact_kind.replace("product_", "")}/${slugify((p as { title: string }).title)}`
|
|
116
|
+
return `product/${input.artifact_kind}`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildScope(input: CompassProposalInput): string[] {
|
|
120
|
+
const base = ["product", "compass", input.artifact_kind.replace("product_", "")]
|
|
121
|
+
const p = input.payload
|
|
122
|
+
if ("goal" in p && (p as ProductPathway).goal) base.push(slugify((p as ProductPathway).goal).slice(0, 20))
|
|
123
|
+
return base
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildAffectedAreas(input: CompassProposalInput): string[] {
|
|
127
|
+
const p = input.payload
|
|
128
|
+
if ("phases" in p) {
|
|
129
|
+
return (p as ProductPathway).phases
|
|
130
|
+
.flatMap((ph: ProductPathwayPhase) => ph.dependencies)
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.slice(0, 5)
|
|
133
|
+
}
|
|
134
|
+
return []
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildValidationPlan(input: CompassProposalInput): string[] {
|
|
138
|
+
const p = input.payload
|
|
139
|
+
if ("validation_signals" in p) return (p as ProductBet).validation_signals.slice(0, 3)
|
|
140
|
+
if ("suggested_next_step" in p) return [(p as ProductPathway).suggested_next_step]
|
|
141
|
+
return []
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function reviewAfterDate(): string {
|
|
145
|
+
const d = new Date()
|
|
146
|
+
d.setDate(d.getDate() + 45)
|
|
147
|
+
return d.toISOString().slice(0, 10)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function slugify(s: string): string {
|
|
151
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 40)
|
|
152
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
export const CompassEvidenceRefSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
kind: z.enum([
|
|
6
|
+
"chronicle", "advisor", "sentinel", "code", "docs", "tests",
|
|
7
|
+
"config", "cli", "package", "issue", "analytics", "support",
|
|
8
|
+
"inference", "assumption", "unknown",
|
|
9
|
+
]),
|
|
10
|
+
source: z.string(),
|
|
11
|
+
path: z.string().optional(),
|
|
12
|
+
line: z.number().optional(),
|
|
13
|
+
entry_id: z.string().optional(),
|
|
14
|
+
summary: z.string().min(1),
|
|
15
|
+
quote: z.string().optional(),
|
|
16
|
+
confidence: z.number().min(0).max(1),
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export const ProductScoreBreakdownSchema = z.object({
|
|
20
|
+
strategic_fit: z.number().min(0).max(1),
|
|
21
|
+
user_problem_clarity: z.number().min(0).max(1),
|
|
22
|
+
evidence_strength: z.number().min(0).max(1),
|
|
23
|
+
leverage: z.number().min(0).max(1),
|
|
24
|
+
feasibility: z.number().min(0).max(1),
|
|
25
|
+
time_to_signal: z.number().min(0).max(1),
|
|
26
|
+
reversibility: z.number().min(0).max(1),
|
|
27
|
+
complexity_penalty: z.number().min(0).max(1),
|
|
28
|
+
dependency_penalty: z.number().min(0).max(1),
|
|
29
|
+
contradiction_penalty: z.number().min(0).max(1),
|
|
30
|
+
evidence_gap_penalty: z.number().min(0).max(1),
|
|
31
|
+
total: z.number().min(0).max(100),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export const ProductPathwayPhaseSchema = z.object({
|
|
35
|
+
name: z.string().min(1),
|
|
36
|
+
outcome: z.string().min(1),
|
|
37
|
+
user_value: z.string().min(1),
|
|
38
|
+
build_notes: z.array(z.string()).optional(),
|
|
39
|
+
dependencies: z.array(z.string()),
|
|
40
|
+
risks: z.array(z.string()),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export const ProductPathwaySchema = z.object({
|
|
44
|
+
id: z.string(),
|
|
45
|
+
kind: z.literal("product_pathway"),
|
|
46
|
+
title: z.string().min(1),
|
|
47
|
+
goal: z.string().min(1),
|
|
48
|
+
target_user: z.string().optional(),
|
|
49
|
+
problem: z.string().optional(),
|
|
50
|
+
current_behaviors: z.array(z.string()).min(1),
|
|
51
|
+
opportunity: z.string().min(1),
|
|
52
|
+
why_now: z.string().min(1),
|
|
53
|
+
smallest_useful_version: z.string().min(1),
|
|
54
|
+
phases: z.array(ProductPathwayPhaseSchema),
|
|
55
|
+
dependencies: z.array(z.string()),
|
|
56
|
+
risks: z.array(z.string()),
|
|
57
|
+
assumptions: z.array(z.string()).min(1),
|
|
58
|
+
open_questions: z.array(z.string()),
|
|
59
|
+
evidence: z.array(CompassEvidenceRefSchema).min(1),
|
|
60
|
+
scores: ProductScoreBreakdownSchema,
|
|
61
|
+
confidence: z.number().min(0).max(1),
|
|
62
|
+
time_to_signal: z.string().min(1),
|
|
63
|
+
reversibility: z.enum(["high", "medium", "low"]),
|
|
64
|
+
suggested_next_step: z.string().min(1),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
export const ProductBetSchema = z.object({
|
|
68
|
+
id: z.string(),
|
|
69
|
+
kind: z.literal("product_bet"),
|
|
70
|
+
title: z.string().min(1),
|
|
71
|
+
thesis: z.string().min(1),
|
|
72
|
+
why_now: z.string().min(1),
|
|
73
|
+
target_user: z.string().optional(),
|
|
74
|
+
upside: z.string().min(1),
|
|
75
|
+
downside: z.string().min(1),
|
|
76
|
+
assumptions: z.array(z.string()).min(1),
|
|
77
|
+
validation_signals: z.array(z.string()).min(1),
|
|
78
|
+
invalidation_signals: z.array(z.string()).min(1),
|
|
79
|
+
kill_criteria: z.array(z.string()).min(1),
|
|
80
|
+
first_experiment: z.string().min(1),
|
|
81
|
+
build_path: z.array(z.string()),
|
|
82
|
+
evidence: z.array(CompassEvidenceRefSchema).min(1),
|
|
83
|
+
scores: ProductScoreBreakdownSchema,
|
|
84
|
+
confidence: z.number().min(0).max(1),
|
|
85
|
+
time_to_signal: z.string().min(1),
|
|
86
|
+
reversibility: z.enum(["high", "medium", "low"]),
|
|
87
|
+
appetite: z.enum(["small", "medium", "large"]),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
export const ProductIdeaScoreSchema = z.object({
|
|
91
|
+
idea: z.string().min(1),
|
|
92
|
+
summary: z.string().min(1),
|
|
93
|
+
recommendation: z.enum(["pursue", "pursue-small-test", "investigate-more", "defer", "avoid"]),
|
|
94
|
+
scores: ProductScoreBreakdownSchema,
|
|
95
|
+
evidence: z.array(CompassEvidenceRefSchema),
|
|
96
|
+
supporting_reasons: z.array(z.string()),
|
|
97
|
+
risks: z.array(z.string()),
|
|
98
|
+
assumptions: z.array(z.string()),
|
|
99
|
+
open_questions: z.array(z.string()),
|
|
100
|
+
suggested_next_step: z.string().min(1),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
export const CompassBriefLLMSchema = z.object({
|
|
104
|
+
product_direction: z.string().min(1),
|
|
105
|
+
known_from_chronicle: z.array(z.string()),
|
|
106
|
+
known_from_behavior: z.array(z.string()),
|
|
107
|
+
inferred: z.array(z.string()),
|
|
108
|
+
assumptions: z.array(z.string()),
|
|
109
|
+
unknowns: z.array(z.string()),
|
|
110
|
+
missing_evidence: z.array(z.string()),
|
|
111
|
+
recommended_next_step: z.string().min(1),
|
|
112
|
+
confidence: z.number().min(0).max(1),
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
export const PathwaysLLMSchema = z.object({
|
|
116
|
+
pathways: z.array(ProductPathwaySchema),
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
export const BetsLLMSchema = z.object({
|
|
120
|
+
bets: z.array(ProductBetSchema),
|
|
121
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ProductScoreBreakdown } from "./types"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compute a directional product score from raw dimension values.
|
|
5
|
+
*
|
|
6
|
+
* Each dimension is 0–1. Penalty dimensions reduce the total.
|
|
7
|
+
* Final score is clamped to 0–100.
|
|
8
|
+
*
|
|
9
|
+
* Interpretation:
|
|
10
|
+
* 85–100 Very strong; build or spec next
|
|
11
|
+
* 70–84 Strong; pursue a small test
|
|
12
|
+
* 55–69 Plausible; investigate more
|
|
13
|
+
* 40–54 Weak; defer unless strategic
|
|
14
|
+
* 0–39 Avoid for now
|
|
15
|
+
*/
|
|
16
|
+
export function computeScore(dims: Omit<ProductScoreBreakdown, "total">): ProductScoreBreakdown {
|
|
17
|
+
const raw =
|
|
18
|
+
dims.strategic_fit * 20 +
|
|
19
|
+
dims.user_problem_clarity * 15 +
|
|
20
|
+
dims.evidence_strength * 20 +
|
|
21
|
+
dims.leverage * 10 +
|
|
22
|
+
dims.feasibility * 15 +
|
|
23
|
+
dims.time_to_signal * 10 +
|
|
24
|
+
dims.reversibility * 10 -
|
|
25
|
+
dims.complexity_penalty * 10 -
|
|
26
|
+
dims.dependency_penalty * 8 -
|
|
27
|
+
dims.contradiction_penalty * 15 -
|
|
28
|
+
dims.evidence_gap_penalty * 12
|
|
29
|
+
|
|
30
|
+
const total = Math.max(0, Math.min(100, Math.round(raw)))
|
|
31
|
+
return { ...dims, total }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function scoreToRecommendation(
|
|
35
|
+
total: number,
|
|
36
|
+
): "pursue" | "pursue-small-test" | "investigate-more" | "defer" | "avoid" {
|
|
37
|
+
if (total >= 85) return "pursue"
|
|
38
|
+
if (total >= 70) return "pursue-small-test"
|
|
39
|
+
if (total >= 55) return "investigate-more"
|
|
40
|
+
if (total >= 40) return "defer"
|
|
41
|
+
return "avoid"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function scoreToLabel(total: number): string {
|
|
45
|
+
if (total >= 85) return "Very strong"
|
|
46
|
+
if (total >= 70) return "Strong"
|
|
47
|
+
if (total >= 55) return "Plausible"
|
|
48
|
+
if (total >= 40) return "Weak"
|
|
49
|
+
return "Avoid"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build a human-readable explanation for a score.
|
|
54
|
+
*/
|
|
55
|
+
export function explainScore(scores: ProductScoreBreakdown): { strengths: string[]; weaknesses: string[] } {
|
|
56
|
+
const strengths: string[] = []
|
|
57
|
+
const weaknesses: string[] = []
|
|
58
|
+
|
|
59
|
+
if (scores.strategic_fit >= 0.7) strengths.push("Strong strategic fit with current product direction")
|
|
60
|
+
if (scores.evidence_strength >= 0.7) strengths.push("Well-supported by Chronicle or code evidence")
|
|
61
|
+
if (scores.feasibility >= 0.7) strengths.push("Technically feasible given current architecture")
|
|
62
|
+
if (scores.reversibility >= 0.7) strengths.push("Highly reversible — easy to change course")
|
|
63
|
+
if (scores.leverage >= 0.7) strengths.push("High leverage — small build unlocks significant value")
|
|
64
|
+
if (scores.time_to_signal >= 0.7) strengths.push("Fast time-to-signal — team learns quickly")
|
|
65
|
+
if (scores.user_problem_clarity >= 0.7) strengths.push("Clear user problem and target")
|
|
66
|
+
|
|
67
|
+
if (scores.evidence_strength < 0.4) weaknesses.push("Weak evidence — mostly inference or assumption")
|
|
68
|
+
if (scores.evidence_gap_penalty > 0.5) weaknesses.push("Significant evidence gaps — needs more investigation")
|
|
69
|
+
if (scores.contradiction_penalty > 0.3) weaknesses.push("May conflict with prior Chronicle decisions")
|
|
70
|
+
if (scores.dependency_penalty > 0.4) weaknesses.push("High dependency burden — external services or platform work required")
|
|
71
|
+
if (scores.complexity_penalty > 0.4) weaknesses.push("Adds significant complexity — UI, infrastructure, or support burden")
|
|
72
|
+
if (scores.time_to_signal < 0.4) weaknesses.push("Long time-to-signal — hard to validate quickly")
|
|
73
|
+
if (scores.strategic_fit < 0.4) weaknesses.push("Weak strategic fit with current product direction")
|
|
74
|
+
if (scores.user_problem_clarity < 0.4) weaknesses.push("User problem or target not yet clearly defined")
|
|
75
|
+
|
|
76
|
+
return { strengths, weaknesses }
|
|
77
|
+
}
|