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,68 @@
|
|
|
1
|
+
import type { EvidenceItem } from "../../domain/evidence";
|
|
2
|
+
import type { QueryBundle } from "../../domain/queryBundle";
|
|
3
|
+
import type { FetchJson, SourceAdapter } from "./sourceAdapter";
|
|
4
|
+
|
|
5
|
+
type RedditPost = {
|
|
6
|
+
data: {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
selftext: string;
|
|
10
|
+
score: number;
|
|
11
|
+
num_comments: number;
|
|
12
|
+
permalink: string;
|
|
13
|
+
created_utc: number;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type RedditSearchResponse = {
|
|
18
|
+
data?: {
|
|
19
|
+
children?: RedditPost[];
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function buildRedditSearchUrl(term: string) {
|
|
24
|
+
const query = encodeURIComponent(term);
|
|
25
|
+
return `https://www.reddit.com/search.json?q=${query}&limit=5`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizePost(post: RedditPost, query: string): EvidenceItem {
|
|
29
|
+
const data = post.data;
|
|
30
|
+
return {
|
|
31
|
+
id: `reddit-post-${data.id}`,
|
|
32
|
+
source: "Reddit",
|
|
33
|
+
sourceType: "post",
|
|
34
|
+
date: new Date(data.created_utc * 1000).toISOString().slice(0, 10),
|
|
35
|
+
query,
|
|
36
|
+
snippet: data.title,
|
|
37
|
+
link: `https://reddit.com${data.permalink}`,
|
|
38
|
+
metricContribution: "Pain",
|
|
39
|
+
included: true,
|
|
40
|
+
reason: `${data.num_comments} comments and ${data.score} score.`,
|
|
41
|
+
duplicateCluster: `reddit-${data.id}`,
|
|
42
|
+
signalStrength: Math.min(100, data.score + data.num_comments * 2),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createRedditSourceAdapter({
|
|
47
|
+
fetchJson,
|
|
48
|
+
}: {
|
|
49
|
+
fetchJson: FetchJson;
|
|
50
|
+
}): SourceAdapter {
|
|
51
|
+
return {
|
|
52
|
+
id: "reddit",
|
|
53
|
+
label: "Reddit",
|
|
54
|
+
bestFor: "Finding visceral pain points, consumer discussions, and unfiltered feedback.",
|
|
55
|
+
limitations: "Search can return memes or unrelated subreddits.",
|
|
56
|
+
async scan(bundle: QueryBundle) {
|
|
57
|
+
const query = bundle.problemKeywords[0] ?? bundle.solutionKeywords[0];
|
|
58
|
+
|
|
59
|
+
if (!query) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const response = (await fetchJson(buildRedditSearchUrl(query))) as RedditSearchResponse;
|
|
64
|
+
|
|
65
|
+
return (response.data?.children ?? []).map((post) => normalizePost(post, query));
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EvidenceItem } from "../../domain/evidence";
|
|
2
|
+
import type { QueryBundle } from "../../domain/queryBundle";
|
|
3
|
+
|
|
4
|
+
export type FetchJson = (url: string) => Promise<unknown>;
|
|
5
|
+
|
|
6
|
+
export type SourceAdapter = {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
bestFor: string;
|
|
10
|
+
limitations: string;
|
|
11
|
+
scan(bundle: QueryBundle): Promise<EvidenceItem[]>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { EvidenceItem } from "../../domain/evidence";
|
|
2
|
+
import type { QueryBundle } from "../../domain/queryBundle";
|
|
3
|
+
import type { FetchJson, SourceAdapter } from "./sourceAdapter";
|
|
4
|
+
|
|
5
|
+
type StackItem = {
|
|
6
|
+
question_id: number;
|
|
7
|
+
title: string;
|
|
8
|
+
link: string;
|
|
9
|
+
score: number;
|
|
10
|
+
answer_count: number;
|
|
11
|
+
creation_date: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type StackExchangeResponse = {
|
|
15
|
+
items?: StackItem[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function buildStackExchangeSearchUrl(term: string) {
|
|
19
|
+
const query = encodeURIComponent(term);
|
|
20
|
+
return `https://api.stackexchange.com/2.3/search?order=desc&sort=relevance&intitle=${query}&site=stackoverflow&pagesize=5`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeQuestion(item: StackItem, query: string): EvidenceItem {
|
|
24
|
+
return {
|
|
25
|
+
id: `stackoverflow-q-${item.question_id}`,
|
|
26
|
+
source: "Stack Exchange",
|
|
27
|
+
sourceType: "question",
|
|
28
|
+
date: new Date(item.creation_date * 1000).toISOString().slice(0, 10),
|
|
29
|
+
query,
|
|
30
|
+
snippet: item.title,
|
|
31
|
+
link: item.link,
|
|
32
|
+
metricContribution: "Channel Fit",
|
|
33
|
+
included: true,
|
|
34
|
+
reason: `${item.answer_count} answers and ${item.score} score on StackOverflow.`,
|
|
35
|
+
duplicateCluster: `so-${item.question_id}`,
|
|
36
|
+
signalStrength: Math.min(100, item.score * 5 + item.answer_count * 10),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createStackExchangeSourceAdapter({
|
|
41
|
+
fetchJson,
|
|
42
|
+
}: {
|
|
43
|
+
fetchJson: FetchJson;
|
|
44
|
+
}): SourceAdapter {
|
|
45
|
+
return {
|
|
46
|
+
id: "stackexchange",
|
|
47
|
+
label: "Stack Exchange",
|
|
48
|
+
bestFor: "Technical problem validation and finding where devs hang out.",
|
|
49
|
+
limitations: "Heavily skewed towards engineering problems.",
|
|
50
|
+
async scan(bundle: QueryBundle) {
|
|
51
|
+
const query = bundle.problemKeywords[0] ?? bundle.solutionKeywords[0];
|
|
52
|
+
|
|
53
|
+
if (!query) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const response = (await fetchJson(buildStackExchangeSearchUrl(query))) as StackExchangeResponse;
|
|
58
|
+
|
|
59
|
+
return (response.items ?? []).map((item) => normalizeQuestion(item, query));
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { EvidenceItem } from "../../domain/evidence";
|
|
2
|
+
import type { QueryBundle } from "../../domain/queryBundle";
|
|
3
|
+
import type { FetchJson, SourceAdapter } from "./sourceAdapter";
|
|
4
|
+
|
|
5
|
+
type WikiItem = {
|
|
6
|
+
pageid: number;
|
|
7
|
+
title: string;
|
|
8
|
+
snippet: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type WikipediaResponse = {
|
|
13
|
+
query?: {
|
|
14
|
+
search?: WikiItem[];
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function buildWikipediaSearchUrl(term: string) {
|
|
19
|
+
const query = encodeURIComponent(term);
|
|
20
|
+
return `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${query}&utf8=&format=json&srlimit=5`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeWiki(item: WikiItem, query: string): EvidenceItem {
|
|
24
|
+
return {
|
|
25
|
+
id: `wiki-page-${item.pageid}`,
|
|
26
|
+
source: "Wikipedia",
|
|
27
|
+
sourceType: "article",
|
|
28
|
+
date: item.timestamp.slice(0, 10),
|
|
29
|
+
query,
|
|
30
|
+
snippet: item.title,
|
|
31
|
+
link: `https://en.wikipedia.org/?curid=${item.pageid}`,
|
|
32
|
+
metricContribution: "Evidence Quality",
|
|
33
|
+
included: true,
|
|
34
|
+
reason: `Established encyclopedia article exists for this concept.`,
|
|
35
|
+
duplicateCluster: `wiki-${item.pageid}`,
|
|
36
|
+
signalStrength: 80, // Wiki presence usually implies high baseline quality
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createWikipediaSourceAdapter({
|
|
41
|
+
fetchJson,
|
|
42
|
+
}: {
|
|
43
|
+
fetchJson: FetchJson;
|
|
44
|
+
}): SourceAdapter {
|
|
45
|
+
return {
|
|
46
|
+
id: "wikipedia",
|
|
47
|
+
label: "Wikipedia",
|
|
48
|
+
bestFor: "Establishing baseline conceptual validity and terminology.",
|
|
49
|
+
limitations: "Does not prove market demand, only concept existence.",
|
|
50
|
+
async scan(bundle: QueryBundle) {
|
|
51
|
+
const query = bundle.solutionKeywords[0] ?? bundle.problemKeywords[0];
|
|
52
|
+
|
|
53
|
+
if (!query) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const response = (await fetchJson(buildWikipediaSearchUrl(query))) as WikipediaResponse;
|
|
58
|
+
|
|
59
|
+
return (response.query?.search ?? []).map((item) => normalizeWiki(item, query));
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { EvidenceItem } from "../domain/evidence";
|
|
2
|
+
import type { PillarScores } from "../domain/scoring";
|
|
3
|
+
|
|
4
|
+
export const therapyPillarScores: PillarScores = {
|
|
5
|
+
demand: 64,
|
|
6
|
+
pain: 82,
|
|
7
|
+
momentum: 58,
|
|
8
|
+
competitionFit: 66,
|
|
9
|
+
activity: 49,
|
|
10
|
+
channelFit: 72,
|
|
11
|
+
evidenceQuality: 61,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const therapyEvidenceSeed: EvidenceItem[] = [
|
|
15
|
+
{
|
|
16
|
+
id: "therapist-paperwork-reddit",
|
|
17
|
+
source: "Reddit",
|
|
18
|
+
sourceType: "post",
|
|
19
|
+
date: "2026-05-08",
|
|
20
|
+
query: "therapist paperwork",
|
|
21
|
+
snippet: "Private-practice therapists discussing documentation spilling into evenings.",
|
|
22
|
+
link: "https://www.reddit.com/",
|
|
23
|
+
metricContribution: "Pain",
|
|
24
|
+
included: true,
|
|
25
|
+
reason: "Direct pain language and matching audience.",
|
|
26
|
+
duplicateCluster: "therapy-paperwork-1",
|
|
27
|
+
signalStrength: 82,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "soap-notes-youtube",
|
|
31
|
+
source: "YouTube",
|
|
32
|
+
sourceType: "video",
|
|
33
|
+
date: "2026-04-18",
|
|
34
|
+
query: "SOAP notes",
|
|
35
|
+
snippet: "Tutorial demand around writing faster SOAP notes.",
|
|
36
|
+
link: "https://www.youtube.com/",
|
|
37
|
+
metricContribution: "Demand",
|
|
38
|
+
included: true,
|
|
39
|
+
reason: "Searchable education demand around the workflow.",
|
|
40
|
+
duplicateCluster: "soap-notes-1",
|
|
41
|
+
signalStrength: 64,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "therapy-launch-producthunt",
|
|
45
|
+
source: "Product Hunt",
|
|
46
|
+
sourceType: "launch",
|
|
47
|
+
date: "2026-03-21",
|
|
48
|
+
query: "therapy notes app",
|
|
49
|
+
snippet: "A recent launch for clinical note workflow automation.",
|
|
50
|
+
link: "https://www.producthunt.com/",
|
|
51
|
+
metricContribution: "Competition Fit",
|
|
52
|
+
included: true,
|
|
53
|
+
reason: "Comparable product supply signal.",
|
|
54
|
+
duplicateCluster: "therapy-launch-1",
|
|
55
|
+
signalStrength: 66,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "privacy-therapy-hn",
|
|
59
|
+
source: "Hacker News",
|
|
60
|
+
sourceType: "comment",
|
|
61
|
+
date: "2026-02-16",
|
|
62
|
+
query: "privacy therapy notes",
|
|
63
|
+
snippet: "Discussion of privacy concerns around assisted clinical documentation.",
|
|
64
|
+
link: "https://news.ycombinator.com/",
|
|
65
|
+
metricContribution: "Evidence Quality",
|
|
66
|
+
included: true,
|
|
67
|
+
reason: "Relevant privacy objection for adoption risk.",
|
|
68
|
+
duplicateCluster: "privacy-therapy-1",
|
|
69
|
+
signalStrength: 61,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "documentation-stack-question",
|
|
73
|
+
source: "Stack Exchange",
|
|
74
|
+
sourceType: "question",
|
|
75
|
+
date: "2026-01-12",
|
|
76
|
+
query: "mental health notes",
|
|
77
|
+
snippet: "Question around structured documentation practices.",
|
|
78
|
+
link: "https://stackexchange.com/",
|
|
79
|
+
metricContribution: "Channel Fit",
|
|
80
|
+
included: true,
|
|
81
|
+
reason: "Professional workflow question with matching terminology.",
|
|
82
|
+
duplicateCluster: "documentation-question-1",
|
|
83
|
+
signalStrength: 72,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "ehr-notes-github",
|
|
87
|
+
source: "GitHub",
|
|
88
|
+
sourceType: "repo",
|
|
89
|
+
date: "2026-04-02",
|
|
90
|
+
query: "EHR notes",
|
|
91
|
+
snippet: "Open-source clinical documentation experiments with slow recent activity.",
|
|
92
|
+
link: "https://github.com/",
|
|
93
|
+
metricContribution: "Activity",
|
|
94
|
+
included: true,
|
|
95
|
+
reason: "Builder activity signal around adjacent documentation tools.",
|
|
96
|
+
duplicateCluster: "ehr-notes-1",
|
|
97
|
+
signalStrength: 49,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "session-notes-reddit",
|
|
101
|
+
source: "Reddit",
|
|
102
|
+
sourceType: "comment",
|
|
103
|
+
date: "2026-05-11",
|
|
104
|
+
query: "session notes",
|
|
105
|
+
snippet: "Solo clinician describes a workaround for drafting notes after appointments.",
|
|
106
|
+
link: "https://www.reddit.com/",
|
|
107
|
+
metricContribution: "Pain",
|
|
108
|
+
included: true,
|
|
109
|
+
reason: "Workaround behavior and matching audience.",
|
|
110
|
+
duplicateCluster: "session-notes-workaround-1",
|
|
111
|
+
signalStrength: 80,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "soap-note-template-youtube",
|
|
115
|
+
source: "YouTube",
|
|
116
|
+
sourceType: "video",
|
|
117
|
+
date: "2026-03-05",
|
|
118
|
+
query: "SOAP note template",
|
|
119
|
+
snippet: "Creator tutorial showing repeated search demand for structured note templates.",
|
|
120
|
+
link: "https://www.youtube.com/",
|
|
121
|
+
metricContribution: "Demand",
|
|
122
|
+
included: true,
|
|
123
|
+
reason: "Template/tutorial signal supports demand pillar.",
|
|
124
|
+
duplicateCluster: "soap-template-1",
|
|
125
|
+
signalStrength: 62,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "clinical-ai-launch",
|
|
129
|
+
source: "Product Hunt",
|
|
130
|
+
sourceType: "launch",
|
|
131
|
+
date: "2026-02-27",
|
|
132
|
+
query: "clinical AI notes",
|
|
133
|
+
snippet: "Launch comments discuss privacy-safe AI notes for healthcare teams.",
|
|
134
|
+
link: "https://www.producthunt.com/",
|
|
135
|
+
metricContribution: "Momentum",
|
|
136
|
+
included: true,
|
|
137
|
+
reason: "Recent launch signal with adoption objections.",
|
|
138
|
+
duplicateCluster: "clinical-ai-launch-1",
|
|
139
|
+
signalStrength: 58,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "documentation-burden-hn",
|
|
143
|
+
source: "Hacker News",
|
|
144
|
+
sourceType: "comment",
|
|
145
|
+
date: "2026-04-29",
|
|
146
|
+
query: "clinical documentation burden",
|
|
147
|
+
snippet: "Clinicians discussing documentation load as a drag on care time.",
|
|
148
|
+
link: "https://news.ycombinator.com/",
|
|
149
|
+
metricContribution: "Pain",
|
|
150
|
+
included: true,
|
|
151
|
+
reason: "Adjacent professional pain signal.",
|
|
152
|
+
duplicateCluster: "documentation-burden-1",
|
|
153
|
+
signalStrength: 78,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "clinical-notes-github",
|
|
157
|
+
source: "GitHub",
|
|
158
|
+
sourceType: "repo",
|
|
159
|
+
date: "2026-03-30",
|
|
160
|
+
query: "clinical notes automation",
|
|
161
|
+
snippet: "Repository activity around structured clinical note workflows.",
|
|
162
|
+
link: "https://github.com/",
|
|
163
|
+
metricContribution: "Activity",
|
|
164
|
+
included: true,
|
|
165
|
+
reason: "Recent builder activity in adjacent note automation space.",
|
|
166
|
+
duplicateCluster: "clinical-notes-repo-1",
|
|
167
|
+
signalStrength: 51,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: "physical-therapy-notes",
|
|
171
|
+
source: "Reddit",
|
|
172
|
+
sourceType: "post",
|
|
173
|
+
date: "2026-05-02",
|
|
174
|
+
query: "therapy notes",
|
|
175
|
+
snippet: "Physical therapy treatment notes workflow discussion.",
|
|
176
|
+
link: "https://www.reddit.com/",
|
|
177
|
+
metricContribution: "Demand",
|
|
178
|
+
included: true,
|
|
179
|
+
reason: "Seed inclusion before query-bundle filtering.",
|
|
180
|
+
duplicateCluster: "physical-therapy-notes-1",
|
|
181
|
+
signalStrength: 24,
|
|
182
|
+
},
|
|
183
|
+
];
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"next-env.d.ts",
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
".next/dev/types/**/*.ts",
|
|
31
|
+
"**/*.mts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules"]
|
|
34
|
+
}
|