impact-compass 0.2.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/DOCUMENTATION.md +47 -0
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/SKILLS.md +52 -0
- package/assets/top-banner-readme.png +0 -0
- package/example-coffee.json +15 -0
- package/example-input.json +15 -0
- package/example-output.json +352 -0
- package/example-react.json +15 -0
- package/package.json +52 -0
- package/src/cli.ts +219 -0
- package/src/domain/evidence.test.ts +93 -0
- package/src/domain/evidence.ts +99 -0
- package/src/domain/queryBundle.test.ts +67 -0
- package/src/domain/queryBundle.ts +116 -0
- package/src/domain/scoring.test.ts +184 -0
- package/src/domain/scoring.ts +322 -0
- package/src/services/comparison.test.ts +89 -0
- package/src/services/comparison.ts +84 -0
- package/src/services/demoReport.test.ts +37 -0
- package/src/services/demoReport.ts +32 -0
- package/src/services/publicEvidenceReport.test.ts +66 -0
- package/src/services/publicEvidenceReport.ts +82 -0
- package/src/services/queryDerivedReport.test.ts +68 -0
- package/src/services/queryDerivedReport.ts +227 -0
- package/src/services/reportBuilder.test.ts +90 -0
- package/src/services/reportBuilder.ts +219 -0
- package/src/services/reportInsights.ts +23 -0
- package/src/services/reportStorage.test.ts +77 -0
- package/src/services/reportStorage.ts +113 -0
- package/src/services/reportTypes.ts +49 -0
- package/src/services/sources/extendedAdapters.ts +218 -0
- package/src/services/sources/githubSource.test.ts +48 -0
- package/src/services/sources/githubSource.ts +63 -0
- package/src/services/sources/hackerNewsSource.test.ts +80 -0
- package/src/services/sources/hackerNewsSource.ts +117 -0
- package/src/services/sources/itunesSource.ts +62 -0
- package/src/services/sources/npmSource.ts +65 -0
- package/src/services/sources/redditSource.ts +68 -0
- package/src/services/sources/sourceAdapter.ts +12 -0
- package/src/services/sources/stackExchangeSource.ts +62 -0
- package/src/services/sources/wikipediaSource.ts +62 -0
- package/src/services/therapySeed.ts +183 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
calculateEvidenceIntegrity,
|
|
3
|
+
filterEvidenceForBundle,
|
|
4
|
+
type EvidenceItem,
|
|
5
|
+
} from "../domain/evidence";
|
|
6
|
+
import { evaluateQueryQuality, type QueryBundle } from "../domain/queryBundle";
|
|
7
|
+
import {
|
|
8
|
+
calculateCompassScore,
|
|
9
|
+
calculateScoreRange,
|
|
10
|
+
type PillarScores,
|
|
11
|
+
summarizeIntegrity,
|
|
12
|
+
} from "../domain/scoring";
|
|
13
|
+
import type {
|
|
14
|
+
CompassReportModel,
|
|
15
|
+
FormulaReadout,
|
|
16
|
+
IdeaBrief,
|
|
17
|
+
PillarSummary,
|
|
18
|
+
} from "./reportTypes";
|
|
19
|
+
|
|
20
|
+
export type BuildCompassReportInput = {
|
|
21
|
+
idea: IdeaBrief;
|
|
22
|
+
queryBundle: QueryBundle;
|
|
23
|
+
evidence: EvidenceItem[];
|
|
24
|
+
pillarScores: PillarScores;
|
|
25
|
+
uncertainty?: number;
|
|
26
|
+
methodologyVersion?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const pillarLabels: Record<keyof PillarScores, string> = {
|
|
30
|
+
demand: "Demand",
|
|
31
|
+
pain: "Pain",
|
|
32
|
+
momentum: "Momentum",
|
|
33
|
+
competitionFit: "Competition Fit",
|
|
34
|
+
activity: "Activity",
|
|
35
|
+
channelFit: "Channel Fit",
|
|
36
|
+
evidenceQuality: "Evidence Quality",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const formulas: Omit<FormulaReadout, "score">[] = [
|
|
40
|
+
{
|
|
41
|
+
pillar: "Demand",
|
|
42
|
+
formula: "0.45 volume + 0.25 unique authors + 0.20 questions + 0.10 engagement",
|
|
43
|
+
formulaLatex:
|
|
44
|
+
"0.45V + 0.25A_u + 0.20Q_i + 0.10E_p",
|
|
45
|
+
inputs: ["mention volume", "unique authors", "question intent", "engagement percentile"],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
pillar: "Pain",
|
|
49
|
+
formula:
|
|
50
|
+
"0.35 pain density + 0.25 workaround density + 0.20 alternative density + 0.20 discussion depth",
|
|
51
|
+
formulaLatex:
|
|
52
|
+
"0.35D_p + 0.25D_w + 0.20D_a + 0.20D_d",
|
|
53
|
+
inputs: ["pain phrases", "workarounds", "alternative seeking", "discussion depth"],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
pillar: "Momentum",
|
|
57
|
+
formula: "0.40 short growth + 0.30 medium growth + 0.20 sustained growth - 0.10 spike penalty",
|
|
58
|
+
formulaLatex:
|
|
59
|
+
"0.40G_{30} + 0.30G_{90} + 0.20G_s - 0.10P_{spike}",
|
|
60
|
+
inputs: ["30-day rate", "90-day rate", "sustained trend", "spike penalty"],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
pillar: "Competition Fit",
|
|
64
|
+
formula: "100 * exp(-((supply percentile - 60)^2) / (2 * 25^2))",
|
|
65
|
+
formulaLatex:
|
|
66
|
+
"100e^{-\\frac{(P_s - 60)^2}{2 \\cdot 25^2}}",
|
|
67
|
+
inputs: ["competitor count", "launch count", "repo/package supply", "saturation penalty"],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
pillar: "Activity",
|
|
71
|
+
formula: "weighted available activity signals; non-relevant missing metrics excluded",
|
|
72
|
+
formulaLatex:
|
|
73
|
+
"\\frac{\\sum w_i s_i}{\\sum w_i}",
|
|
74
|
+
inputs: ["repo activity", "package activity", "launch recency", "discussion freshness"],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
pillar: "Channel Fit",
|
|
78
|
+
formula: "0.35 concentration + 0.25 community count + 0.25 engagement + 0.15 lens match",
|
|
79
|
+
formulaLatex:
|
|
80
|
+
"0.35C_s + 0.25C_n + 0.25E_c + 0.15L_m",
|
|
81
|
+
inputs: ["source concentration", "community count", "top channel engagement", "lens match"],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
pillar: "Evidence Quality",
|
|
85
|
+
formula:
|
|
86
|
+
"0.25 source diversity + 0.20 sample size + 0.20 precision + 0.15 ambiguity inverse + 0.10 duplicate inverse + 0.10 recency coverage",
|
|
87
|
+
formulaLatex:
|
|
88
|
+
"0.25D_s + 0.20N + 0.20R_p + 0.15A_i + 0.10D_i + 0.10R_c",
|
|
89
|
+
inputs: ["source diversity", "sample size", "precision", "ambiguity", "duplicates", "recency"],
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
function firstTerm(terms: string[], fallback: string) {
|
|
94
|
+
return terms[0] ?? fallback;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function createPillarNotes(input: BuildCompassReportInput): Record<keyof PillarScores, string> {
|
|
98
|
+
const problem = firstTerm(input.queryBundle.problemKeywords, input.idea.problem);
|
|
99
|
+
const solution = firstTerm(input.queryBundle.solutionKeywords, input.idea.name);
|
|
100
|
+
const audience = firstTerm(input.queryBundle.audienceKeywords, input.idea.targetUser);
|
|
101
|
+
const competitor = firstTerm(
|
|
102
|
+
input.queryBundle.competitorKeywords,
|
|
103
|
+
`${input.idea.name} alternatives`,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
demand: `Demand reads public-search targets for "${problem}" across free sources.`,
|
|
108
|
+
pain: `Pain reads workaround and complaint language around "${problem}".`,
|
|
109
|
+
momentum: `Momentum reads recent public activity around "${solution}".`,
|
|
110
|
+
competitionFit: `Competition Fit compares visible alternatives such as "${competitor}".`,
|
|
111
|
+
activity: `Activity reads builder and package signals around "${solution}".`,
|
|
112
|
+
channelFit: `Channel Fit checks whether "${audience}" has reachable public communities.`,
|
|
113
|
+
evidenceQuality: "Evidence Quality rewards source diversity, exclusions, and query specificity.",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createPillars(input: BuildCompassReportInput): PillarSummary[] {
|
|
118
|
+
const pillarNotes = createPillarNotes(input);
|
|
119
|
+
|
|
120
|
+
return (Object.keys(input.pillarScores) as Array<keyof PillarScores>).map((key) => ({
|
|
121
|
+
key,
|
|
122
|
+
label: pillarLabels[key],
|
|
123
|
+
score: input.pillarScores[key],
|
|
124
|
+
note: pillarNotes[key],
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function findPillar(
|
|
129
|
+
pillars: PillarSummary[],
|
|
130
|
+
compare: (a: PillarSummary, b: PillarSummary) => PillarSummary,
|
|
131
|
+
) {
|
|
132
|
+
return pillars.reduce(compare);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createFormulaReadouts(pillarScores: PillarScores): FormulaReadout[] {
|
|
136
|
+
const scoreByLabel = Object.fromEntries(
|
|
137
|
+
Object.entries(pillarLabels).map(([key, label]) => [
|
|
138
|
+
label,
|
|
139
|
+
pillarScores[key as keyof PillarScores],
|
|
140
|
+
]),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return formulas.map((formula) => ({
|
|
144
|
+
...formula,
|
|
145
|
+
score: scoreByLabel[formula.pillar],
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function generateInterpretation(
|
|
150
|
+
score: number,
|
|
151
|
+
strongest: PillarSummary,
|
|
152
|
+
weakest: PillarSummary,
|
|
153
|
+
compFit: number
|
|
154
|
+
): string {
|
|
155
|
+
let interpretation = "";
|
|
156
|
+
|
|
157
|
+
if (score >= 90) {
|
|
158
|
+
interpretation = "This idea is highly validated. Public evidence shows exceptional product-market potential. ";
|
|
159
|
+
} else if (score >= 70) {
|
|
160
|
+
interpretation = "Public evidence supports deeper validation. The idea shows strong promise but faces some friction. ";
|
|
161
|
+
} else if (score >= 50) {
|
|
162
|
+
interpretation = "The idea has moderate validation. There are signals of demand, but it may require a pivot or niche targeting. ";
|
|
163
|
+
} else {
|
|
164
|
+
interpretation = "Public evidence is currently lacking. This market may be too small, or the problem is not widely discussed online. ";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (compFit <= 30) {
|
|
168
|
+
interpretation += "WARNING: This is a highly saturated Red Ocean market with massive existing competition. ";
|
|
169
|
+
} else if (compFit >= 80) {
|
|
170
|
+
interpretation += "Excitingly, there appears to be very little direct competition (Blue Ocean). ";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interpretation += `Your strongest validation signal is ${strongest.label}, while ${weakest.label} remains the weakest link.`;
|
|
174
|
+
|
|
175
|
+
return interpretation;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function buildCompassReport(
|
|
179
|
+
input: BuildCompassReportInput,
|
|
180
|
+
): CompassReportModel {
|
|
181
|
+
const evidence = filterEvidenceForBundle(input.evidence, input.queryBundle);
|
|
182
|
+
const evidenceIntegrity = calculateEvidenceIntegrity(evidence);
|
|
183
|
+
const score = calculateCompassScore(input.pillarScores, {
|
|
184
|
+
uncertainty: input.uncertainty ?? 10,
|
|
185
|
+
});
|
|
186
|
+
const pillars = createPillars(input);
|
|
187
|
+
const strongestPillar = findPillar(pillars, (best, next) =>
|
|
188
|
+
next.score > best.score ? next : best,
|
|
189
|
+
);
|
|
190
|
+
const weakestPillar = findPillar(pillars, (weakest, next) =>
|
|
191
|
+
next.score < weakest.score ? next : weakest,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
methodologyVersion: input.methodologyVersion ?? "0.1",
|
|
196
|
+
idea: input.idea,
|
|
197
|
+
queryBundle: input.queryBundle,
|
|
198
|
+
queryQuality: evaluateQueryQuality(input.queryBundle),
|
|
199
|
+
pillars,
|
|
200
|
+
formulas: createFormulaReadouts(input.pillarScores),
|
|
201
|
+
summary: {
|
|
202
|
+
score: score.score,
|
|
203
|
+
uncertainty: score.uncertainty,
|
|
204
|
+
confidence: score.confidence,
|
|
205
|
+
range: calculateScoreRange(score),
|
|
206
|
+
},
|
|
207
|
+
integrity: summarizeIntegrity({
|
|
208
|
+
evidenceQuality: input.pillarScores.evidenceQuality,
|
|
209
|
+
queryLocked: input.queryBundle.locked,
|
|
210
|
+
...evidenceIntegrity,
|
|
211
|
+
}),
|
|
212
|
+
evidence,
|
|
213
|
+
strongestPillar,
|
|
214
|
+
weakestPillar,
|
|
215
|
+
interpretation: generateInterpretation(score.score, strongestPillar, weakestPillar, input.pillarScores.competitionFit),
|
|
216
|
+
disclaimer:
|
|
217
|
+
"This score reflects public evidence found through selected sources and queries. It is not a prediction of success, customer willingness to pay, or product quality.",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CompassReportModel } from "./reportTypes";
|
|
2
|
+
|
|
3
|
+
export function findBestEvidenceSource(report: CompassReportModel) {
|
|
4
|
+
const counts = report.evidence
|
|
5
|
+
.filter((item) => item.included)
|
|
6
|
+
.reduce<Record<string, number>>((sourceCounts, item) => {
|
|
7
|
+
sourceCounts[item.source] = (sourceCounts[item.source] ?? 0) + 1;
|
|
8
|
+
return sourceCounts;
|
|
9
|
+
}, {});
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
Object.entries(counts).sort(([, left], [, right]) => right - left)[0]?.[0] ??
|
|
13
|
+
"Unknown"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function summarizeEvidenceGap(report: CompassReportModel) {
|
|
18
|
+
if (report.integrity.confidenceCap) {
|
|
19
|
+
return `${report.integrity.confidenceCap} confidence cap`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `${report.weakestPillar.label} needs stronger evidence`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createDemoReport } from "./demoReport";
|
|
3
|
+
import {
|
|
4
|
+
createReportSnapshot,
|
|
5
|
+
listReportSnapshots,
|
|
6
|
+
saveReportSnapshot,
|
|
7
|
+
type StorageLike,
|
|
8
|
+
} from "./reportStorage";
|
|
9
|
+
|
|
10
|
+
function memoryStorage(): StorageLike {
|
|
11
|
+
const data = new Map<string, string>();
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
getItem: (key) => data.get(key) ?? null,
|
|
15
|
+
setItem: (key, value) => data.set(key, value),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("report storage service", () => {
|
|
20
|
+
it("creates compact report snapshots for local history", () => {
|
|
21
|
+
const snapshot = createReportSnapshot(createDemoReport(), "2026-05-24T04:00:00.000Z");
|
|
22
|
+
|
|
23
|
+
expect(snapshot).toMatchObject({
|
|
24
|
+
id: "privacy-safe-session-note-drafts-2026-05-24t04-00-00-000z",
|
|
25
|
+
ideaName: "Privacy-safe session note drafts",
|
|
26
|
+
score: 66,
|
|
27
|
+
uncertainty: 11,
|
|
28
|
+
confidence: "Medium",
|
|
29
|
+
strongestPillar: "Pain",
|
|
30
|
+
weakestPillar: "Activity",
|
|
31
|
+
bestChannel: "Reddit",
|
|
32
|
+
generatedAt: "2026-05-24T04:00:00.000Z",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("saves and lists snapshots newest first", () => {
|
|
37
|
+
const storage = memoryStorage();
|
|
38
|
+
const report = createDemoReport();
|
|
39
|
+
|
|
40
|
+
saveReportSnapshot(storage, report, "2026-05-24T04:00:00.000Z");
|
|
41
|
+
saveReportSnapshot(storage, report, "2026-05-24T05:00:00.000Z");
|
|
42
|
+
|
|
43
|
+
expect(listReportSnapshots(storage).map((item) => item.generatedAt)).toEqual([
|
|
44
|
+
"2026-05-24T05:00:00.000Z",
|
|
45
|
+
"2026-05-24T04:00:00.000Z",
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("keeps the ten newest snapshots", () => {
|
|
50
|
+
const storage = memoryStorage();
|
|
51
|
+
const report = createDemoReport();
|
|
52
|
+
|
|
53
|
+
for (let index = 0; index < 12; index += 1) {
|
|
54
|
+
saveReportSnapshot(
|
|
55
|
+
storage,
|
|
56
|
+
report,
|
|
57
|
+
`2026-05-24T${String(index).padStart(2, "0")}:00:00.000Z`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
expect(listReportSnapshots(storage)).toHaveLength(10);
|
|
62
|
+
expect(listReportSnapshots(storage)[0].generatedAt).toBe(
|
|
63
|
+
"2026-05-24T11:00:00.000Z",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("drops legacy snapshots that do not have comparison fields", () => {
|
|
68
|
+
const storage = memoryStorage();
|
|
69
|
+
|
|
70
|
+
storage.setItem(
|
|
71
|
+
"impact-compass:report-snapshots",
|
|
72
|
+
JSON.stringify([{ id: "old", ideaName: "Old", score: 50 }]),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(listReportSnapshots(storage)).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { CompassReportModel } from "./reportTypes";
|
|
2
|
+
import { findBestEvidenceSource, summarizeEvidenceGap } from "./reportInsights";
|
|
3
|
+
|
|
4
|
+
const storageKey = "impact-compass:report-snapshots";
|
|
5
|
+
const maxSnapshots = 10;
|
|
6
|
+
|
|
7
|
+
export type StorageLike = {
|
|
8
|
+
getItem(key: string): string | null;
|
|
9
|
+
setItem(key: string, value: string): void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ReportSnapshot = {
|
|
13
|
+
id: string;
|
|
14
|
+
ideaName: string;
|
|
15
|
+
lens: string;
|
|
16
|
+
score: number;
|
|
17
|
+
uncertainty: number;
|
|
18
|
+
confidence: string;
|
|
19
|
+
range: string;
|
|
20
|
+
rankBasis: number;
|
|
21
|
+
strongestPillar: string;
|
|
22
|
+
weakestPillar: string;
|
|
23
|
+
bestChannel: string;
|
|
24
|
+
evidenceGap: string;
|
|
25
|
+
generatedAt: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function slug(value: string) {
|
|
29
|
+
return value
|
|
30
|
+
.toLocaleLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
32
|
+
.replace(/^-|-$/g, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readSnapshots(storage: StorageLike): ReportSnapshot[] {
|
|
36
|
+
const raw = storage.getItem(storageKey);
|
|
37
|
+
|
|
38
|
+
if (!raw) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(raw);
|
|
44
|
+
return Array.isArray(parsed) ? parsed.filter(isReportSnapshot) : [];
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isReportSnapshot(value: unknown): value is ReportSnapshot {
|
|
51
|
+
if (!value || typeof value !== "object") {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const snapshot = value as Partial<ReportSnapshot>;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
typeof snapshot.id === "string" &&
|
|
59
|
+
typeof snapshot.ideaName === "string" &&
|
|
60
|
+
typeof snapshot.score === "number" &&
|
|
61
|
+
typeof snapshot.uncertainty === "number" &&
|
|
62
|
+
typeof snapshot.confidence === "string" &&
|
|
63
|
+
typeof snapshot.range === "string" &&
|
|
64
|
+
typeof snapshot.rankBasis === "number" &&
|
|
65
|
+
typeof snapshot.strongestPillar === "string" &&
|
|
66
|
+
typeof snapshot.weakestPillar === "string" &&
|
|
67
|
+
typeof snapshot.bestChannel === "string" &&
|
|
68
|
+
typeof snapshot.evidenceGap === "string" &&
|
|
69
|
+
typeof snapshot.generatedAt === "string"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createReportSnapshot(
|
|
74
|
+
report: CompassReportModel,
|
|
75
|
+
generatedAt: string,
|
|
76
|
+
): ReportSnapshot {
|
|
77
|
+
return {
|
|
78
|
+
id: `${slug(report.idea.name)}-${slug(generatedAt)}`,
|
|
79
|
+
ideaName: report.idea.name,
|
|
80
|
+
lens: report.idea.lens,
|
|
81
|
+
score: report.summary.score,
|
|
82
|
+
uncertainty: report.summary.uncertainty,
|
|
83
|
+
confidence: report.summary.confidence,
|
|
84
|
+
range: `${report.summary.range.lower}-${report.summary.range.upper}`,
|
|
85
|
+
rankBasis: report.summary.range.lower,
|
|
86
|
+
strongestPillar: report.strongestPillar.label,
|
|
87
|
+
weakestPillar: report.weakestPillar.label,
|
|
88
|
+
bestChannel: findBestEvidenceSource(report),
|
|
89
|
+
evidenceGap: summarizeEvidenceGap(report),
|
|
90
|
+
generatedAt,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function listReportSnapshots(storage: StorageLike): ReportSnapshot[] {
|
|
95
|
+
return readSnapshots(storage).sort((left, right) =>
|
|
96
|
+
right.generatedAt.localeCompare(left.generatedAt),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function saveReportSnapshot(
|
|
101
|
+
storage: StorageLike,
|
|
102
|
+
report: CompassReportModel,
|
|
103
|
+
generatedAt = new Date().toISOString(),
|
|
104
|
+
) {
|
|
105
|
+
const snapshot = createReportSnapshot(report, generatedAt);
|
|
106
|
+
const existing = readSnapshots(storage).filter((item) => item.id !== snapshot.id);
|
|
107
|
+
const next = [snapshot, ...existing]
|
|
108
|
+
.sort((left, right) => right.generatedAt.localeCompare(left.generatedAt))
|
|
109
|
+
.slice(0, maxSnapshots);
|
|
110
|
+
|
|
111
|
+
storage.setItem(storageKey, JSON.stringify(next));
|
|
112
|
+
return snapshot;
|
|
113
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { EvidenceItem } from "../domain/evidence";
|
|
2
|
+
import type { QueryBundle, QueryQuality } from "../domain/queryBundle";
|
|
3
|
+
import type { PillarScores, summarizeIntegrity } from "../domain/scoring";
|
|
4
|
+
|
|
5
|
+
export type IdeaBrief = {
|
|
6
|
+
name: string;
|
|
7
|
+
problem: string;
|
|
8
|
+
targetUser: string;
|
|
9
|
+
lens: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type PillarSummary = {
|
|
13
|
+
key: keyof PillarScores;
|
|
14
|
+
label: string;
|
|
15
|
+
score: number;
|
|
16
|
+
note: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type FormulaReadout = {
|
|
20
|
+
pillar: string;
|
|
21
|
+
score: number;
|
|
22
|
+
formula: string;
|
|
23
|
+
formulaLatex: string;
|
|
24
|
+
inputs: string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type CompassReportModel = {
|
|
28
|
+
methodologyVersion: string;
|
|
29
|
+
idea: IdeaBrief;
|
|
30
|
+
queryBundle: QueryBundle;
|
|
31
|
+
queryQuality: QueryQuality;
|
|
32
|
+
pillars: PillarSummary[];
|
|
33
|
+
formulas: FormulaReadout[];
|
|
34
|
+
summary: {
|
|
35
|
+
score: number;
|
|
36
|
+
uncertainty: number;
|
|
37
|
+
confidence: string;
|
|
38
|
+
range: {
|
|
39
|
+
lower: number;
|
|
40
|
+
upper: number;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
integrity: ReturnType<typeof summarizeIntegrity>;
|
|
44
|
+
evidence: EvidenceItem[];
|
|
45
|
+
strongestPillar: PillarSummary;
|
|
46
|
+
weakestPillar: PillarSummary;
|
|
47
|
+
interpretation: string;
|
|
48
|
+
disclaimer: string;
|
|
49
|
+
};
|