@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
22import test from "node:test" ;
33import {
44 extractSection ,
5+ extractBullets ,
56 firstParagraphOrBullet ,
67 parseWorkspaceSummary ,
78} from "./parse-workspace-summary" ;
@@ -138,6 +139,21 @@ const SAMPLE_PRODUCT = `# PRODUCT
138139- Main value props: simplicity, speed, async-first
139140` ;
140141
142+ const SAMPLE_MARKET = `# MARKET
143+
144+ ## ICP hypotheses
145+ - Remote-first engineering teams of 10-50 at seed/Series A startups that outgrew Notion for sprint planning.
146+ - Secondary: product managers at mid-market SaaS companies needing async standup visibility.
147+
148+ ## Personas
149+ - VP Engineering at a Series A startup managing 3 squads across timezones.
150+ - Product manager juggling roadmap alignment across engineering, design, and GTM.
151+
152+ ## Main customer pains
153+ - Scattered async communication across Slack, Notion, and Linear causes dropped context.
154+ - Standup meetings waste 30+ min/day for distributed teams with no persistent record.
155+ ` ;
156+
141157const SAMPLE_GROWTH = `# GROWTH
142158
143159## Strategic summary
@@ -161,8 +177,8 @@ const SAMPLE_GROWTH = `# GROWTH
161177- Run a Product Hunt launch within 30 days
162178` ;
163179
164- void test ( "parseWorkspaceSummary extracts all 5 data points" , ( ) => {
165- const result = parseWorkspaceSummary ( SAMPLE_PRODUCT , null , SAMPLE_GROWTH ) ;
180+ void test ( "parseWorkspaceSummary extracts all data points including icp and opportunities " , ( ) => {
181+ const result = parseWorkspaceSummary ( SAMPLE_PRODUCT , SAMPLE_MARKET , SAMPLE_GROWTH ) ;
166182
167183 assert . ok ( result . productSummary , "productSummary should be extracted" ) ;
168184 assert . ok (
@@ -193,6 +209,15 @@ void test("parseWorkspaceSummary extracts all 5 data points", () => {
193209 result . topOpportunity . includes ( "community" ) || result . topOpportunity . includes ( "Reddit" ) ,
194210 "topOpportunity should capture an experiment or strategy" ,
195211 ) ;
212+
213+ assert . ok ( result . icp , "icp should be extracted from MARKET.md" ) ;
214+ assert . ok (
215+ result . icp . includes ( "engineering teams" ) || result . icp . includes ( "Remote-first" ) ,
216+ "icp should describe ideal customer" ,
217+ ) ;
218+
219+ assert . ok ( result . opportunities . length >= 2 , "opportunities should have at least 2 bullets" ) ;
220+ assert . ok ( result . opportunities . length <= 3 , "opportunities should have at most 3 bullets" ) ;
196221} ) ;
197222
198223void test ( "parseWorkspaceSummary returns nulls for missing files" , ( ) => {
@@ -202,6 +227,8 @@ void test("parseWorkspaceSummary returns nulls for missing files", () => {
202227 assert . equal ( result . valueProposition , null ) ;
203228 assert . equal ( result . mainRisk , null ) ;
204229 assert . equal ( result . topOpportunity , null ) ;
230+ assert . equal ( result . icp , null ) ;
231+ assert . deepEqual ( result . opportunities , [ ] ) ;
205232} ) ;
206233
207234void test ( "parseWorkspaceSummary handles partial data (only product)" , ( ) => {
@@ -211,6 +238,9 @@ void test("parseWorkspaceSummary handles partial data (only product)", () => {
211238 assert . ok ( result . valueProposition ) ;
212239 assert . equal ( result . mainRisk , null ) ;
213240 assert . equal ( result . topOpportunity , null ) ;
241+ // ICP falls back to PRODUCT.md targetAudience when no MARKET.md
242+ assert . ok ( result . icp , "icp should fall back to product target users" ) ;
243+ assert . deepEqual ( result . opportunities , [ ] ) ;
214244} ) ;
215245
216246void test ( "parseWorkspaceSummary handles partial data (only growth)" , ( ) => {
@@ -220,6 +250,8 @@ void test("parseWorkspaceSummary handles partial data (only growth)", () => {
220250 assert . equal ( result . valueProposition , null ) ;
221251 assert . ok ( result . mainRisk ) ;
222252 assert . ok ( result . topOpportunity ) ;
253+ assert . equal ( result . icp , null ) ;
254+ assert . ok ( result . opportunities . length > 0 , "opportunities from GROWTH.md" ) ;
223255} ) ;
224256
225257// ---------------------------------------------------------------------------
@@ -309,3 +341,74 @@ void test("parseWorkspaceSummary never throws on malformed input", () => {
309341 const r3 = parseWorkspaceSummary ( "### Sub\n#### Deep\n##### Deeper" , null , null ) ;
310342 assert . equal ( r3 . productSummary , null ) ;
311343} ) ;
344+
345+ // ---------------------------------------------------------------------------
346+ // extractBullets
347+ // ---------------------------------------------------------------------------
348+
349+ void test ( "extractBullets extracts up to max bullets from section text" , ( ) => {
350+ const section = "- First bullet here.\n- Second bullet here.\n- Third bullet here.\n- Fourth bullet here." ;
351+ const result = extractBullets ( section , 3 ) ;
352+ assert . equal ( result . length , 3 ) ;
353+ assert . ok ( result [ 0 ] . startsWith ( "First" ) ) ;
354+ assert . ok ( result [ 2 ] . startsWith ( "Third" ) ) ;
355+ } ) ;
356+
357+ void test ( "extractBullets returns empty array for empty input" , ( ) => {
358+ assert . deepEqual ( extractBullets ( "" , 3 ) , [ ] ) ;
359+ assert . deepEqual ( extractBullets ( " " , 3 ) , [ ] ) ;
360+ } ) ;
361+
362+ void test ( "extractBullets stops at sub-headings" , ( ) => {
363+ const section = "- First bullet.\n### Sub heading\n- Second bullet." ;
364+ const result = extractBullets ( section , 3 ) ;
365+ assert . equal ( result . length , 1 ) ;
366+ } ) ;
367+
368+ void test ( "extractBullets skips label-like lines" , ( ) => {
369+ const section = "Key opportunity\n- Actual bullet with real content here.\n- Another real bullet with details." ;
370+ const result = extractBullets ( section , 3 ) ;
371+ assert . ok ( result . length >= 1 ) ;
372+ assert . ok ( result [ 0 ] . includes ( "Actual bullet" ) ) ;
373+ } ) ;
374+
375+ // ---------------------------------------------------------------------------
376+ // ICP extraction
377+ // ---------------------------------------------------------------------------
378+
379+ void test ( "parseWorkspaceSummary extracts ICP from MARKET.md 'ICP hypotheses'" , ( ) => {
380+ const result = parseWorkspaceSummary ( null , SAMPLE_MARKET , null ) ;
381+ assert . ok ( result . icp , "icp should be extracted from MARKET.md" ) ;
382+ assert . ok (
383+ result . icp . includes ( "engineering teams" ) || result . icp . includes ( "Remote-first" ) ,
384+ "icp should describe the ideal customer" ,
385+ ) ;
386+ } ) ;
387+
388+ void test ( "parseWorkspaceSummary ICP falls back to MARKET.md 'Personas' heading" , ( ) => {
389+ const marketMd = `# MARKET\n\n## Personas\n- DevOps leads at mid-size SaaS companies managing CI/CD pipelines.\n` ;
390+ const result = parseWorkspaceSummary ( null , marketMd , null ) ;
391+ assert . ok ( result . icp , "icp should fall back to Personas heading" ) ;
392+ assert . ok ( result . icp . includes ( "DevOps" ) ) ;
393+ } ) ;
394+
395+ void test ( "parseWorkspaceSummary ICP falls back to PRODUCT.md when MARKET.md is null" , ( ) => {
396+ const result = parseWorkspaceSummary ( SAMPLE_PRODUCT , null , null ) ;
397+ assert . ok ( result . icp , "icp should fall back to product target users" ) ;
398+ assert . ok ( result . icp . includes ( "Remote-first" ) || result . icp . includes ( "startup" ) ) ;
399+ } ) ;
400+
401+ // ---------------------------------------------------------------------------
402+ // Opportunities extraction
403+ // ---------------------------------------------------------------------------
404+
405+ void test ( "parseWorkspaceSummary extracts opportunities from GROWTH.md" , ( ) => {
406+ const result = parseWorkspaceSummary ( null , null , SAMPLE_GROWTH ) ;
407+ assert . ok ( result . opportunities . length >= 2 , "should extract at least 2 opportunity bullets" ) ;
408+ assert . ok ( result . opportunities . length <= 3 , "should extract at most 3 opportunity bullets" ) ;
409+ } ) ;
410+
411+ void test ( "parseWorkspaceSummary returns empty opportunities when GROWTH.md is null" , ( ) => {
412+ const result = parseWorkspaceSummary ( SAMPLE_PRODUCT , null , null ) ;
413+ assert . deepEqual ( result . opportunities , [ ] ) ;
414+ } ) ;
0 commit comments