gswd 1.0.1 → 1.1.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/bin/gswd-tools.cjs +228 -0
- package/commands/gswd/imagine.md +7 -1
- package/commands/gswd/start.md +507 -32
- package/dist/lib/audit.d.ts +205 -0
- package/dist/lib/audit.js +805 -0
- package/dist/lib/bootstrap.d.ts +103 -0
- package/dist/lib/bootstrap.js +563 -0
- package/dist/lib/compile.d.ts +239 -0
- package/dist/lib/compile.js +1152 -0
- package/dist/lib/config.d.ts +49 -0
- package/dist/lib/config.js +150 -0
- package/dist/lib/imagine-agents.d.ts +54 -0
- package/dist/lib/imagine-agents.js +185 -0
- package/dist/lib/imagine-gate.d.ts +47 -0
- package/dist/lib/imagine-gate.js +131 -0
- package/dist/lib/imagine-input.d.ts +46 -0
- package/dist/lib/imagine-input.js +233 -0
- package/dist/lib/imagine-synthesis.d.ts +90 -0
- package/dist/lib/imagine-synthesis.js +453 -0
- package/dist/lib/imagine.d.ts +56 -0
- package/dist/lib/imagine.js +413 -0
- package/dist/lib/intake.d.ts +27 -0
- package/dist/lib/intake.js +82 -0
- package/dist/lib/parse.d.ts +59 -0
- package/dist/lib/parse.js +171 -0
- package/dist/lib/render.d.ts +309 -0
- package/dist/lib/render.js +624 -0
- package/dist/lib/specify-agents.d.ts +120 -0
- package/dist/lib/specify-agents.js +269 -0
- package/dist/lib/specify-journeys.d.ts +124 -0
- package/dist/lib/specify-journeys.js +279 -0
- package/dist/lib/specify-nfr.d.ts +45 -0
- package/dist/lib/specify-nfr.js +159 -0
- package/dist/lib/specify-roles.d.ts +46 -0
- package/dist/lib/specify-roles.js +88 -0
- package/dist/lib/specify.d.ts +70 -0
- package/dist/lib/specify.js +676 -0
- package/dist/lib/state.d.ts +140 -0
- package/dist/lib/state.js +340 -0
- package/dist/tests/audit.test.d.ts +4 -0
- package/dist/tests/audit.test.js +1579 -0
- package/dist/tests/bootstrap.test.d.ts +5 -0
- package/dist/tests/bootstrap.test.js +611 -0
- package/dist/tests/compile.test.d.ts +4 -0
- package/dist/tests/compile.test.js +862 -0
- package/dist/tests/config.test.d.ts +4 -0
- package/dist/tests/config.test.js +191 -0
- package/dist/tests/imagine-agents.test.d.ts +6 -0
- package/dist/tests/imagine-agents.test.js +179 -0
- package/dist/tests/imagine-gate.test.d.ts +6 -0
- package/dist/tests/imagine-gate.test.js +264 -0
- package/dist/tests/imagine-input.test.d.ts +6 -0
- package/dist/tests/imagine-input.test.js +283 -0
- package/dist/tests/imagine-synthesis.test.d.ts +7 -0
- package/dist/tests/imagine-synthesis.test.js +380 -0
- package/dist/tests/imagine.test.d.ts +8 -0
- package/dist/tests/imagine.test.js +406 -0
- package/dist/tests/parse.test.d.ts +4 -0
- package/dist/tests/parse.test.js +285 -0
- package/dist/tests/render.test.d.ts +4 -0
- package/dist/tests/render.test.js +236 -0
- package/dist/tests/specify-agents.test.d.ts +4 -0
- package/dist/tests/specify-agents.test.js +352 -0
- package/dist/tests/specify-journeys.test.d.ts +5 -0
- package/dist/tests/specify-journeys.test.js +440 -0
- package/dist/tests/specify-nfr.test.d.ts +4 -0
- package/dist/tests/specify-nfr.test.js +205 -0
- package/dist/tests/specify-roles.test.d.ts +4 -0
- package/dist/tests/specify-roles.test.js +136 -0
- package/dist/tests/specify.test.d.ts +9 -0
- package/dist/tests/specify.test.js +544 -0
- package/dist/tests/state.test.d.ts +4 -0
- package/dist/tests/state.test.js +316 -0
- package/lib/bootstrap.ts +37 -11
- package/lib/compile.ts +426 -4
- package/lib/imagine-agents.ts +53 -7
- package/lib/imagine-synthesis.ts +170 -6
- package/lib/imagine.ts +59 -5
- package/lib/intake.ts +60 -0
- package/lib/parse.ts +2 -1
- package/lib/render.ts +566 -5
- package/lib/specify-agents.ts +25 -3
- package/lib/state.ts +115 -0
- package/package.json +3 -2
- package/templates/gswd/DECISIONS.template.md +3 -0
package/lib/compile.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface ParsedFR {
|
|
|
28
28
|
scope: 'v1' | 'v2' | 'out';
|
|
29
29
|
priority: 'P0' | 'P1' | 'P2';
|
|
30
30
|
sourceJourneys: string[];
|
|
31
|
+
acceptanceCriteria: string[];
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export interface ParsedNFR {
|
|
@@ -69,6 +70,11 @@ export interface SpecBundle {
|
|
|
69
70
|
journeys: ParsedJourney[];
|
|
70
71
|
integrations: ParsedIntegration[];
|
|
71
72
|
projectSlug: string;
|
|
73
|
+
architectureSummary: string;
|
|
74
|
+
icpHighlights: string;
|
|
75
|
+
competitionLandscape: string;
|
|
76
|
+
gtmPositioning: string;
|
|
77
|
+
journeyInsights: string;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
// ─── Constants (all exported) ─────────────────────────────────────────────────
|
|
@@ -151,12 +157,26 @@ export function parseFRsFromSpec(
|
|
|
151
157
|
|
|
152
158
|
const sourceJourneys = Array.from(frToJourneys.get(pos.id) ?? new Set<string>()).sort();
|
|
153
159
|
|
|
160
|
+
// Extract acceptance criteria: lines after "**Acceptance Criteria:**" heading
|
|
161
|
+
const acceptanceCriteria: string[] = [];
|
|
162
|
+
const acMatch = section.match(/\*{0,2}Acceptance Criteria:?\*{0,2}:?\s*\n([\s\S]*?)(?=\*\*|###|$)/i);
|
|
163
|
+
if (acMatch) {
|
|
164
|
+
const acLines = acMatch[1].split('\n');
|
|
165
|
+
for (const acLine of acLines) {
|
|
166
|
+
const bulletMatch = acLine.match(/^[-*]\s+(.+)/);
|
|
167
|
+
if (bulletMatch) {
|
|
168
|
+
acceptanceCriteria.push(bulletMatch[1].trim());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
154
173
|
return {
|
|
155
174
|
id: pos.id,
|
|
156
175
|
description,
|
|
157
176
|
scope: (scopeMatch?.[1]?.toLowerCase() as 'v1' | 'v2' | 'out') ?? 'v1',
|
|
158
177
|
priority: (priorityMatch?.[1] as 'P0' | 'P1' | 'P2') ?? 'P1',
|
|
159
178
|
sourceJourneys,
|
|
179
|
+
acceptanceCriteria,
|
|
160
180
|
};
|
|
161
181
|
});
|
|
162
182
|
|
|
@@ -346,6 +366,12 @@ export function parseSpecBundle(
|
|
|
346
366
|
nfr: string;
|
|
347
367
|
journeys: string;
|
|
348
368
|
integrations: string;
|
|
369
|
+
architecture?: string;
|
|
370
|
+
icp?: string;
|
|
371
|
+
competition?: string;
|
|
372
|
+
gtm?: string;
|
|
373
|
+
researchSummary?: string;
|
|
374
|
+
features?: string;
|
|
349
375
|
},
|
|
350
376
|
statePath?: string
|
|
351
377
|
): SpecBundle {
|
|
@@ -412,6 +438,52 @@ export function parseSpecBundle(
|
|
|
412
438
|
}
|
|
413
439
|
}
|
|
414
440
|
|
|
441
|
+
// Populate new enrichment fields from research files
|
|
442
|
+
const architectureContent = files.architecture ?? '';
|
|
443
|
+
const icpContent = files.icp ?? '';
|
|
444
|
+
const competitionContent = files.competition ?? '';
|
|
445
|
+
const gtmContent = files.gtm ?? '';
|
|
446
|
+
const featuresContent = files.features ?? '';
|
|
447
|
+
|
|
448
|
+
const architectureSummary =
|
|
449
|
+
extractHeadingContent(architectureContent, '## Standard Architecture') ??
|
|
450
|
+
extractHeadingContent(architectureContent, '## Executive Summary') ??
|
|
451
|
+
'';
|
|
452
|
+
|
|
453
|
+
const icpHighlights =
|
|
454
|
+
extractHeadingContent(icpContent, '## ICP Highlights') ??
|
|
455
|
+
extractHeadingContent(icpContent, '## Target Users') ??
|
|
456
|
+
extractHeadingContent(featuresContent, '### Expected Features') ??
|
|
457
|
+
extractHeadingContent(featuresContent, '## Expected Features') ??
|
|
458
|
+
'';
|
|
459
|
+
|
|
460
|
+
const competitionLandscape =
|
|
461
|
+
extractHeadingContent(competitionContent, '## Competitor Analysis') ??
|
|
462
|
+
extractHeadingContent(competitionContent, '## Landscape') ??
|
|
463
|
+
extractHeadingContent(featuresContent, '## Competitor Feature Analysis') ??
|
|
464
|
+
'';
|
|
465
|
+
|
|
466
|
+
const gtmPositioning =
|
|
467
|
+
extractHeadingContent(gtmContent, '## GTM Positioning') ??
|
|
468
|
+
extractHeadingContent(gtmContent, '## Positioning') ??
|
|
469
|
+
extractHeadingContent(gtmContent, '## Executive Summary') ??
|
|
470
|
+
'';
|
|
471
|
+
|
|
472
|
+
// Synthesize journeyInsights from journeys array
|
|
473
|
+
const journeyTypeCounts = new Map<string, number>();
|
|
474
|
+
for (const journey of journeys) {
|
|
475
|
+
const count = journeyTypeCounts.get(journey.type) ?? 0;
|
|
476
|
+
journeyTypeCounts.set(journey.type, count + 1);
|
|
477
|
+
}
|
|
478
|
+
const journeyInsightParts: string[] = [];
|
|
479
|
+
for (const journey of journeys) {
|
|
480
|
+
const frCount = journey.linkedFRs.length;
|
|
481
|
+
journeyInsightParts.push(`${journey.id} (${journey.name}, type: ${journey.type}, FRs: ${frCount})`);
|
|
482
|
+
}
|
|
483
|
+
const journeyInsights = journeyInsightParts.length > 0
|
|
484
|
+
? journeyInsightParts.join('\n')
|
|
485
|
+
: '';
|
|
486
|
+
|
|
415
487
|
return {
|
|
416
488
|
vision,
|
|
417
489
|
targetUser,
|
|
@@ -429,6 +501,11 @@ export function parseSpecBundle(
|
|
|
429
501
|
journeys,
|
|
430
502
|
integrations,
|
|
431
503
|
projectSlug,
|
|
504
|
+
architectureSummary,
|
|
505
|
+
icpHighlights,
|
|
506
|
+
competitionLandscape,
|
|
507
|
+
gtmPositioning,
|
|
508
|
+
journeyInsights,
|
|
432
509
|
};
|
|
433
510
|
}
|
|
434
511
|
|
|
@@ -476,6 +553,96 @@ export function generateProjectDoc(bundle: SpecBundle): string {
|
|
|
476
553
|
}
|
|
477
554
|
lines.push('');
|
|
478
555
|
|
|
556
|
+
// ── ## Context section ──────────────────────────────────────────────────────
|
|
557
|
+
lines.push('## Context');
|
|
558
|
+
lines.push('');
|
|
559
|
+
lines.push('Product context synthesized from GSWD research pipeline. Each subsection summarizes key findings and references the source artifact.');
|
|
560
|
+
lines.push('');
|
|
561
|
+
|
|
562
|
+
// Architecture Summary
|
|
563
|
+
lines.push('### Architecture Summary');
|
|
564
|
+
lines.push('');
|
|
565
|
+
if (bundle.architectureSummary) {
|
|
566
|
+
lines.push(bundle.architectureSummary);
|
|
567
|
+
} else {
|
|
568
|
+
lines.push('*(Architecture research not available — run /gswd:imagine to generate)*');
|
|
569
|
+
}
|
|
570
|
+
lines.push('');
|
|
571
|
+
lines.push('> See: .planning/research/ARCHITECTURE.md for full details');
|
|
572
|
+
lines.push('');
|
|
573
|
+
|
|
574
|
+
// ICP Highlights
|
|
575
|
+
lines.push('### ICP Highlights');
|
|
576
|
+
lines.push('');
|
|
577
|
+
if (bundle.icpHighlights) {
|
|
578
|
+
lines.push(bundle.icpHighlights);
|
|
579
|
+
} else {
|
|
580
|
+
lines.push('*(ICP research not available)*');
|
|
581
|
+
}
|
|
582
|
+
lines.push('');
|
|
583
|
+
lines.push('> See: .planning/research/ICP.md for full details');
|
|
584
|
+
lines.push('');
|
|
585
|
+
|
|
586
|
+
// Competition Landscape
|
|
587
|
+
lines.push('### Competition Landscape');
|
|
588
|
+
lines.push('');
|
|
589
|
+
if (bundle.competitionLandscape) {
|
|
590
|
+
lines.push(bundle.competitionLandscape);
|
|
591
|
+
} else {
|
|
592
|
+
lines.push('*(Competition research not available)*');
|
|
593
|
+
}
|
|
594
|
+
lines.push('');
|
|
595
|
+
lines.push('> See: .planning/research/COMPETITION.md for full details');
|
|
596
|
+
lines.push('');
|
|
597
|
+
|
|
598
|
+
// GTM Positioning
|
|
599
|
+
lines.push('### GTM Positioning');
|
|
600
|
+
lines.push('');
|
|
601
|
+
if (bundle.gtmPositioning) {
|
|
602
|
+
lines.push(bundle.gtmPositioning);
|
|
603
|
+
} else {
|
|
604
|
+
lines.push('*(GTM research not available)*');
|
|
605
|
+
}
|
|
606
|
+
lines.push('');
|
|
607
|
+
lines.push('> See: .planning/research/GTM.md for full details');
|
|
608
|
+
lines.push('');
|
|
609
|
+
|
|
610
|
+
// Journey Insights
|
|
611
|
+
lines.push('### Journey Insights');
|
|
612
|
+
lines.push('');
|
|
613
|
+
if (bundle.journeyInsights) {
|
|
614
|
+
lines.push(bundle.journeyInsights);
|
|
615
|
+
} else if (bundle.journeys.length > 0) {
|
|
616
|
+
// Synthesize from journeys array
|
|
617
|
+
for (const j of bundle.journeys) {
|
|
618
|
+
lines.push(`${j.id} (${j.name}, type: ${j.type}, FRs: ${j.linkedFRs.length})`);
|
|
619
|
+
}
|
|
620
|
+
} else {
|
|
621
|
+
lines.push('*(No journeys defined)*');
|
|
622
|
+
}
|
|
623
|
+
lines.push('');
|
|
624
|
+
lines.push('> See: .planning/research/FEATURES.md for full details');
|
|
625
|
+
lines.push('');
|
|
626
|
+
|
|
627
|
+
// GSWD Artifacts table
|
|
628
|
+
lines.push('### GSWD Artifacts');
|
|
629
|
+
lines.push('');
|
|
630
|
+
lines.push('| File | Description |');
|
|
631
|
+
lines.push('|------|-------------|');
|
|
632
|
+
lines.push('| .planning/IMAGINE.md | Product vision, target user, and direction |');
|
|
633
|
+
lines.push('| .planning/DECISIONS.md | Frozen decisions, success metrics, risks |');
|
|
634
|
+
lines.push('| .planning/SPEC.md | Functional requirements with IDs |');
|
|
635
|
+
lines.push('| .planning/NFR.md | Non-functional requirements |');
|
|
636
|
+
lines.push('| .planning/JOURNEYS.md | User journeys with FR linkages |');
|
|
637
|
+
lines.push('| .planning/INTEGRATIONS.md | External integrations and fallbacks |');
|
|
638
|
+
lines.push('| .planning/research/ARCHITECTURE.md | Architecture research |');
|
|
639
|
+
lines.push('| .planning/research/FEATURES.md | Feature landscape research |');
|
|
640
|
+
lines.push('| .planning/research/PITFALLS.md | Common pitfalls research |');
|
|
641
|
+
lines.push('| .planning/research/STACK.md | Technology stack research |');
|
|
642
|
+
lines.push('| .planning/research/SUMMARY.md | Research executive summary |');
|
|
643
|
+
lines.push('| .planning/research/gswd/SUMMARY.md | GSD-format research bridge |');
|
|
644
|
+
lines.push('');
|
|
645
|
+
|
|
479
646
|
return lines.join('\n');
|
|
480
647
|
}
|
|
481
648
|
|
|
@@ -506,11 +673,65 @@ export function generateRequirementsDoc(bundle: SpecBundle): string {
|
|
|
506
673
|
const v1FRs = sortedFRs.filter((fr) => fr.scope === 'v1');
|
|
507
674
|
const v2FRs = sortedFRs.filter((fr) => fr.scope === 'v2');
|
|
508
675
|
|
|
676
|
+
// Build a journey map for quick lookup
|
|
677
|
+
const journeyMap = new Map<string, ParsedJourney>();
|
|
678
|
+
for (const journey of bundle.journeys) {
|
|
679
|
+
journeyMap.set(journey.id, journey);
|
|
680
|
+
}
|
|
681
|
+
|
|
509
682
|
lines.push('### v1 (In Scope)');
|
|
510
683
|
lines.push('');
|
|
511
684
|
for (const fr of v1FRs) {
|
|
512
685
|
lines.push(`#### ${fr.id}: ${fr.description}`);
|
|
513
686
|
lines.push(`**Scope:** ${fr.scope} **Priority:** ${fr.priority}`);
|
|
687
|
+
|
|
688
|
+
// Journey origin
|
|
689
|
+
if (fr.sourceJourneys.length > 0) {
|
|
690
|
+
const journeyRefs = fr.sourceJourneys
|
|
691
|
+
.sort()
|
|
692
|
+
.map((jId) => {
|
|
693
|
+
const journey = journeyMap.get(jId);
|
|
694
|
+
return journey ? `${jId}: ${journey.name}` : jId;
|
|
695
|
+
})
|
|
696
|
+
.join(', ');
|
|
697
|
+
lines.push(`**Origin:** From ${journeyRefs}`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Acceptance criteria
|
|
701
|
+
if (fr.acceptanceCriteria && fr.acceptanceCriteria.length > 0) {
|
|
702
|
+
lines.push('**Acceptance Criteria:**');
|
|
703
|
+
for (const criterion of fr.acceptanceCriteria) {
|
|
704
|
+
lines.push(`- ${criterion}`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// NFR constraints — apply heuristic based on NFR category
|
|
709
|
+
const applicableNFRs: string[] = [];
|
|
710
|
+
for (const nfr of bundle.nfrs) {
|
|
711
|
+
const cat = nfr.category.toLowerCase();
|
|
712
|
+
// Security and performance apply to all v1 FRs
|
|
713
|
+
if (cat === 'security' || cat === 'performance') {
|
|
714
|
+
applicableNFRs.push(`${nfr.id} (${nfr.category.charAt(0).toUpperCase() + nfr.category.slice(1)})`);
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
// Observability applies to all v1 FRs
|
|
718
|
+
if (cat === 'observability') {
|
|
719
|
+
applicableNFRs.push(`${nfr.id} (${nfr.category.charAt(0).toUpperCase() + nfr.category.slice(1)})`);
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
// Privacy applies if FR touches user/data/personal/profile/account keywords
|
|
723
|
+
if (cat === 'privacy') {
|
|
724
|
+
const desc = fr.description.toLowerCase();
|
|
725
|
+
if (desc.includes('user') || desc.includes('data') || desc.includes('personal') ||
|
|
726
|
+
desc.includes('profile') || desc.includes('account')) {
|
|
727
|
+
applicableNFRs.push(`${nfr.id} (${nfr.category.charAt(0).toUpperCase() + nfr.category.slice(1)})`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
if (applicableNFRs.length > 0) {
|
|
732
|
+
lines.push(`**Applicable NFR Constraints:** ${applicableNFRs.join(', ')}`);
|
|
733
|
+
}
|
|
734
|
+
|
|
514
735
|
lines.push('');
|
|
515
736
|
}
|
|
516
737
|
if (v1FRs.length === 0) {
|
|
@@ -614,6 +835,12 @@ export function generateRoadmapDoc(bundle: SpecBundle): string {
|
|
|
614
835
|
if (journeys.length === 0) {
|
|
615
836
|
lines.push('*(no journeys assigned)*');
|
|
616
837
|
} else {
|
|
838
|
+
// Emit a Goal line with journey IDs
|
|
839
|
+
const goalJourneys = journeys
|
|
840
|
+
.map((j) => `${j.id} (${j.name})`)
|
|
841
|
+
.join(', ');
|
|
842
|
+
lines.push(`**Goal:** Implement ${goalJourneys}`);
|
|
843
|
+
lines.push('');
|
|
617
844
|
for (const j of journeys) {
|
|
618
845
|
lines.push(`- **${j.id}**: ${j.name}`);
|
|
619
846
|
const frRefs = [...j.linkedFRs].sort().join(', ');
|
|
@@ -631,6 +858,10 @@ export function generateRoadmapDoc(bundle: SpecBundle): string {
|
|
|
631
858
|
if (obsSecNFRs.length === 0) {
|
|
632
859
|
lines.push('*(no NFRs assigned)*');
|
|
633
860
|
} else {
|
|
861
|
+
// Emit a Goal line for Phase 4
|
|
862
|
+
const nfrGoal = obsSecNFRs.map((nfr) => nfr.id).join(', ');
|
|
863
|
+
lines.push(`**Goal:** Enforce ${nfrGoal}`);
|
|
864
|
+
lines.push('');
|
|
634
865
|
for (const nfr of obsSecNFRs) {
|
|
635
866
|
lines.push(`- **${nfr.id}**: ${nfr.description}`);
|
|
636
867
|
}
|
|
@@ -643,6 +874,160 @@ export function generateRoadmapDoc(bundle: SpecBundle): string {
|
|
|
643
874
|
return lines.join('\n');
|
|
644
875
|
}
|
|
645
876
|
|
|
877
|
+
/**
|
|
878
|
+
* Generate a GSD-format research SUMMARY.md from SpecBundle.
|
|
879
|
+
*
|
|
880
|
+
* Maps GSWD spec content to GSD research template headings.
|
|
881
|
+
* Pure function — no Date calls, no I/O.
|
|
882
|
+
* Versioned with <!-- gswd-schema-version: 1 --> header.
|
|
883
|
+
*/
|
|
884
|
+
export function generateResearchSummary(bundle: SpecBundle): string {
|
|
885
|
+
const lines: string[] = [];
|
|
886
|
+
|
|
887
|
+
lines.push('<!-- gswd-schema-version: 1 -->');
|
|
888
|
+
lines.push('# Project Research Summary');
|
|
889
|
+
lines.push('');
|
|
890
|
+
lines.push(`**Project:** ${bundle.projectSlug}`);
|
|
891
|
+
lines.push('**Domain:** Product specification');
|
|
892
|
+
lines.push('**Confidence:** HIGH');
|
|
893
|
+
lines.push('');
|
|
894
|
+
|
|
895
|
+
// ## Executive Summary
|
|
896
|
+
lines.push('## Executive Summary');
|
|
897
|
+
lines.push('');
|
|
898
|
+
if (bundle.vision) {
|
|
899
|
+
lines.push(bundle.vision);
|
|
900
|
+
lines.push('');
|
|
901
|
+
}
|
|
902
|
+
if (bundle.productDirection) {
|
|
903
|
+
lines.push(bundle.productDirection);
|
|
904
|
+
lines.push('');
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// ## Key Findings
|
|
908
|
+
lines.push('## Key Findings');
|
|
909
|
+
lines.push('');
|
|
910
|
+
|
|
911
|
+
// ### Recommended Stack
|
|
912
|
+
lines.push('### Recommended Stack');
|
|
913
|
+
lines.push('');
|
|
914
|
+
if (bundle.architectureSummary) {
|
|
915
|
+
lines.push(bundle.architectureSummary);
|
|
916
|
+
} else {
|
|
917
|
+
lines.push('*(Stack research not available from GSWD — will be determined during GSD planning)*');
|
|
918
|
+
}
|
|
919
|
+
lines.push('');
|
|
920
|
+
|
|
921
|
+
// ### Expected Features
|
|
922
|
+
lines.push('### Expected Features');
|
|
923
|
+
lines.push('');
|
|
924
|
+
if (bundle.icpHighlights) {
|
|
925
|
+
lines.push(bundle.icpHighlights);
|
|
926
|
+
} else {
|
|
927
|
+
// Synthesize from FRs
|
|
928
|
+
const p0FRs = bundle.frs.filter((f) => f.scope === 'v1' && f.priority === 'P0');
|
|
929
|
+
const p1FRs = bundle.frs.filter((f) => f.scope === 'v1' && f.priority === 'P1');
|
|
930
|
+
|
|
931
|
+
if (p0FRs.length > 0) {
|
|
932
|
+
lines.push('**Must have (table stakes):**');
|
|
933
|
+
for (const fr of p0FRs) {
|
|
934
|
+
lines.push(`- ${fr.description}`);
|
|
935
|
+
}
|
|
936
|
+
lines.push('');
|
|
937
|
+
}
|
|
938
|
+
if (p1FRs.length > 0) {
|
|
939
|
+
lines.push('**Should have:**');
|
|
940
|
+
for (const fr of p1FRs) {
|
|
941
|
+
lines.push(`- ${fr.description}`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
lines.push('');
|
|
946
|
+
|
|
947
|
+
// ### Architecture Approach
|
|
948
|
+
lines.push('### Architecture Approach');
|
|
949
|
+
lines.push('');
|
|
950
|
+
if (bundle.architectureSummary) {
|
|
951
|
+
lines.push(bundle.architectureSummary);
|
|
952
|
+
} else {
|
|
953
|
+
lines.push('*(Architecture to be determined during GSD planning)*');
|
|
954
|
+
}
|
|
955
|
+
lines.push('');
|
|
956
|
+
|
|
957
|
+
// ### Critical Pitfalls
|
|
958
|
+
lines.push('### Critical Pitfalls');
|
|
959
|
+
lines.push('');
|
|
960
|
+
if (bundle.risks.length > 0) {
|
|
961
|
+
for (const risk of bundle.risks) {
|
|
962
|
+
lines.push(`- ${risk}`);
|
|
963
|
+
}
|
|
964
|
+
} else {
|
|
965
|
+
lines.push('*(No risks identified)*');
|
|
966
|
+
}
|
|
967
|
+
lines.push('');
|
|
968
|
+
|
|
969
|
+
// ## Implications for Roadmap
|
|
970
|
+
lines.push('## Implications for Roadmap');
|
|
971
|
+
lines.push('');
|
|
972
|
+
// Group journeys by phase and describe implications
|
|
973
|
+
const phaseJourneyMap = new Map<number, ParsedJourney[]>();
|
|
974
|
+
for (let i = 1; i <= 4; i++) {
|
|
975
|
+
phaseJourneyMap.set(i, []);
|
|
976
|
+
}
|
|
977
|
+
for (const journey of bundle.journeys) {
|
|
978
|
+
const phase = JOURNEY_TYPE_TO_PHASE[journey.type] ?? 3;
|
|
979
|
+
phaseJourneyMap.get(phase)!.push(journey);
|
|
980
|
+
}
|
|
981
|
+
for (let phaseNum = 1; phaseNum <= 3; phaseNum++) {
|
|
982
|
+
const phaseJ = phaseJourneyMap.get(phaseNum)!;
|
|
983
|
+
if (phaseJ.length > 0) {
|
|
984
|
+
const journeyList = phaseJ.map((j) => `${j.id} (${j.name})`).join(', ');
|
|
985
|
+
lines.push(`- **Phase ${phaseNum}**: ${PHASE_NAMES[phaseNum]} — ${journeyList}`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
const obsSecNFRs = bundle.nfrs.filter(
|
|
989
|
+
(nfr) => nfr.category === 'observability' || nfr.category === 'security'
|
|
990
|
+
);
|
|
991
|
+
if (obsSecNFRs.length > 0) {
|
|
992
|
+
const nfrList = obsSecNFRs.map((nfr) => nfr.id).join(', ');
|
|
993
|
+
lines.push(`- **Phase 4**: ${PHASE_NAMES[4]} — enforce ${nfrList}`);
|
|
994
|
+
}
|
|
995
|
+
lines.push('');
|
|
996
|
+
|
|
997
|
+
// ## Confidence Assessment
|
|
998
|
+
lines.push('## Confidence Assessment');
|
|
999
|
+
lines.push('');
|
|
1000
|
+
lines.push('| Area | Confidence | Notes |');
|
|
1001
|
+
lines.push('|------|------------|-------|');
|
|
1002
|
+
lines.push(
|
|
1003
|
+
`| Stack | ${bundle.architectureSummary ? 'HIGH' : 'LOW'} | ${
|
|
1004
|
+
bundle.architectureSummary ? 'From GSWD research' : 'Not researched in GSWD'
|
|
1005
|
+
} |`
|
|
1006
|
+
);
|
|
1007
|
+
lines.push(`| Features | HIGH | ${bundle.frs.length} FRs defined with traceability |`);
|
|
1008
|
+
lines.push(
|
|
1009
|
+
`| Architecture | ${bundle.architectureSummary ? 'HIGH' : 'MEDIUM'} | ${
|
|
1010
|
+
bundle.architectureSummary ? 'From GSWD research' : 'Inferred from requirements'
|
|
1011
|
+
} |`
|
|
1012
|
+
);
|
|
1013
|
+
lines.push(`| Pitfalls | MEDIUM | ${bundle.risks.length} risks identified |`);
|
|
1014
|
+
lines.push('');
|
|
1015
|
+
|
|
1016
|
+
// ## Sources
|
|
1017
|
+
lines.push('## Sources');
|
|
1018
|
+
lines.push('');
|
|
1019
|
+
lines.push('### Primary (HIGH confidence)');
|
|
1020
|
+
lines.push('- GSWD specification pipeline — full imagine/specify/audit/compile cycle');
|
|
1021
|
+
lines.push('');
|
|
1022
|
+
|
|
1023
|
+
lines.push('---');
|
|
1024
|
+
lines.push('*Research completed: via GSWD compile*');
|
|
1025
|
+
lines.push('*Ready for roadmap: yes*');
|
|
1026
|
+
lines.push('');
|
|
1027
|
+
|
|
1028
|
+
return lines.join('\n');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
646
1031
|
// ─── Validator Types ──────────────────────────────────────────────────────────
|
|
647
1032
|
|
|
648
1033
|
export interface ValidatorFinding {
|
|
@@ -809,6 +1194,7 @@ export function generateStateDoc(bundle: SpecBundle): string {
|
|
|
809
1194
|
|
|
810
1195
|
/**
|
|
811
1196
|
* Read all 6 spec artifact files from a planning directory.
|
|
1197
|
+
* Also reads research files from .planning/research/ directory.
|
|
812
1198
|
* Returns a content map keyed by artifact type.
|
|
813
1199
|
* Missing files return empty string (does not throw).
|
|
814
1200
|
*/
|
|
@@ -831,6 +1217,25 @@ export function readSpecArtifacts(planningDir: string): Record<string, string> {
|
|
|
831
1217
|
}
|
|
832
1218
|
}
|
|
833
1219
|
|
|
1220
|
+
// Read research files from .planning/research/ subdirectory
|
|
1221
|
+
const researchDir = path.join(planningDir, 'research');
|
|
1222
|
+
const researchFileMap: Array<[string, string]> = [
|
|
1223
|
+
['ARCHITECTURE.md', 'architecture'],
|
|
1224
|
+
['ICP.md', 'icp'],
|
|
1225
|
+
['COMPETITION.md', 'competition'],
|
|
1226
|
+
['GTM.md', 'gtm'],
|
|
1227
|
+
['SUMMARY.md', 'researchSummary'],
|
|
1228
|
+
['FEATURES.md', 'features'],
|
|
1229
|
+
];
|
|
1230
|
+
|
|
1231
|
+
for (const [filename, key] of researchFileMap) {
|
|
1232
|
+
try {
|
|
1233
|
+
result[key] = fs.readFileSync(path.join(researchDir, filename), 'utf-8');
|
|
1234
|
+
} catch {
|
|
1235
|
+
result[key] = '';
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
834
1239
|
return result;
|
|
835
1240
|
}
|
|
836
1241
|
|
|
@@ -841,7 +1246,7 @@ export interface CompileWorkflowResult {
|
|
|
841
1246
|
error?: string;
|
|
842
1247
|
validatorResult?: ValidatorResult;
|
|
843
1248
|
filesWritten: string[];
|
|
844
|
-
generated?: { project: string; requirements: string; roadmap: string; state: string };
|
|
1249
|
+
generated?: { project: string; requirements: string; roadmap: string; state: string; researchSummary: string };
|
|
845
1250
|
}
|
|
846
1251
|
|
|
847
1252
|
// ─── Workflow Orchestrator ────────────────────────────────────────────────────
|
|
@@ -879,7 +1284,7 @@ export function runCompileWorkflow(options: {
|
|
|
879
1284
|
// Step 3: Read spec artifacts
|
|
880
1285
|
const files = readSpecArtifacts(planningDir);
|
|
881
1286
|
|
|
882
|
-
// Step 4: Parse into SpecBundle
|
|
1287
|
+
// Step 4: Parse into SpecBundle (includes research file enrichment)
|
|
883
1288
|
const bundle = parseSpecBundle(
|
|
884
1289
|
{
|
|
885
1290
|
imagine: files['imagine'] ?? '',
|
|
@@ -888,16 +1293,23 @@ export function runCompileWorkflow(options: {
|
|
|
888
1293
|
nfr: files['nfr'] ?? '',
|
|
889
1294
|
journeys: files['journeys'] ?? '',
|
|
890
1295
|
integrations: files['integrations'] ?? '',
|
|
1296
|
+
architecture: files['architecture'] ?? '',
|
|
1297
|
+
icp: files['icp'] ?? '',
|
|
1298
|
+
competition: files['competition'] ?? '',
|
|
1299
|
+
gtm: files['gtm'] ?? '',
|
|
1300
|
+
researchSummary: files['researchSummary'] ?? '',
|
|
1301
|
+
features: files['features'] ?? '',
|
|
891
1302
|
},
|
|
892
1303
|
statePath
|
|
893
1304
|
);
|
|
894
1305
|
|
|
895
|
-
// Step 5: Generate all 4 docs (pure functions)
|
|
1306
|
+
// Step 5: Generate all 4 contract docs + GSD research bridge (pure functions)
|
|
896
1307
|
const generated = {
|
|
897
1308
|
project: generateProjectDoc(bundle),
|
|
898
1309
|
requirements: generateRequirementsDoc(bundle),
|
|
899
1310
|
roadmap: generateRoadmapDoc(bundle),
|
|
900
1311
|
state: generateStateDoc(bundle),
|
|
1312
|
+
researchSummary: generateResearchSummary(bundle),
|
|
901
1313
|
};
|
|
902
1314
|
|
|
903
1315
|
// Step 6: Run validator
|
|
@@ -909,7 +1321,7 @@ export function runCompileWorkflow(options: {
|
|
|
909
1321
|
return { passed: false, validatorResult, filesWritten: [] };
|
|
910
1322
|
}
|
|
911
1323
|
|
|
912
|
-
// Step 8: Write all 4 docs atomically
|
|
1324
|
+
// Step 8: Write all 4 contract docs atomically
|
|
913
1325
|
const fileOutputMap: Array<[string, string]> = [
|
|
914
1326
|
['PROJECT.md', generated.project],
|
|
915
1327
|
['REQUIREMENTS.md', generated.requirements],
|
|
@@ -924,6 +1336,16 @@ export function runCompileWorkflow(options: {
|
|
|
924
1336
|
filesWritten.push(filePath);
|
|
925
1337
|
}
|
|
926
1338
|
|
|
1339
|
+
// Step 8b: Write GSD research bridge SUMMARY.md to .planning/research/gswd/SUMMARY.md
|
|
1340
|
+
// planningDir resolves to .planning/ in production; research/gswd/ is a subdirectory
|
|
1341
|
+
const gsdResearchDir = path.join(planningDir, 'research', 'gswd');
|
|
1342
|
+
if (!fs.existsSync(gsdResearchDir)) {
|
|
1343
|
+
fs.mkdirSync(gsdResearchDir, { recursive: true });
|
|
1344
|
+
}
|
|
1345
|
+
const summaryPath = path.join(gsdResearchDir, 'SUMMARY.md');
|
|
1346
|
+
safeWriteFile(summaryPath, generated.researchSummary);
|
|
1347
|
+
filesWritten.push(summaryPath);
|
|
1348
|
+
|
|
927
1349
|
// Step 9: Update state: stage_status.compile = 'done', stage = 'compile'
|
|
928
1350
|
updateStageStatus(statePath, 'compile', 'done');
|
|
929
1351
|
const state = readState(statePath);
|
package/lib/imagine-agents.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import * as fs from 'node:fs';
|
|
12
12
|
import type { StarterBrief } from './imagine-input.js';
|
|
13
|
+
import { extractHeadline } from './render.js';
|
|
13
14
|
|
|
14
15
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
15
16
|
|
|
@@ -30,6 +31,12 @@ export interface AgentResult {
|
|
|
30
31
|
|
|
31
32
|
export type SpawnFn = (prompt: string) => Promise<string>;
|
|
32
33
|
|
|
34
|
+
export type OnAgentComplete = (result: {
|
|
35
|
+
agent: string;
|
|
36
|
+
headline: string;
|
|
37
|
+
status: 'complete' | 'failed';
|
|
38
|
+
}) => void;
|
|
39
|
+
|
|
33
40
|
// ─── Agent Definitions ───────────────────────────────────────────────────────
|
|
34
41
|
|
|
35
42
|
/**
|
|
@@ -76,7 +83,12 @@ export const IMAGINE_AGENTS: AgentDefinition[] = [
|
|
|
76
83
|
*
|
|
77
84
|
* Reads agent definition file and injects StarterBrief as context.
|
|
78
85
|
*/
|
|
79
|
-
export function buildAgentPrompt(
|
|
86
|
+
export function buildAgentPrompt(
|
|
87
|
+
agent: AgentDefinition,
|
|
88
|
+
brief: StarterBrief,
|
|
89
|
+
previousOutput?: string,
|
|
90
|
+
userFeedback?: string,
|
|
91
|
+
): string {
|
|
80
92
|
let definition: string;
|
|
81
93
|
try {
|
|
82
94
|
definition = fs.readFileSync(agent.definitionPath, 'utf-8');
|
|
@@ -86,13 +98,27 @@ export function buildAgentPrompt(agent: AgentDefinition, brief: StarterBrief): s
|
|
|
86
98
|
|
|
87
99
|
const briefJson = JSON.stringify(brief, null, 2);
|
|
88
100
|
|
|
89
|
-
|
|
101
|
+
let prompt = `${definition}
|
|
90
102
|
|
|
91
103
|
<starter_brief>
|
|
92
104
|
${briefJson}
|
|
93
|
-
</starter_brief
|
|
105
|
+
</starter_brief>`;
|
|
106
|
+
|
|
107
|
+
// Re-run augmentation (IMAGINE-03): include previous output + user feedback
|
|
108
|
+
if (previousOutput || userFeedback) {
|
|
109
|
+
prompt += '\n\n<rerun_context>';
|
|
110
|
+
if (previousOutput) {
|
|
111
|
+
prompt += `\n\n## Previous Output\nYour previous research produced the following findings. Build on these — do not start from scratch unless the user feedback explicitly asks for a different direction.\n\n${previousOutput}`;
|
|
112
|
+
}
|
|
113
|
+
if (userFeedback) {
|
|
114
|
+
prompt += `\n\n## User Feedback\nThe user was not satisfied with the previous directions and provided this feedback. Use it to refine your research:\n\n${userFeedback}`;
|
|
115
|
+
}
|
|
116
|
+
prompt += '\n</rerun_context>';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
prompt += `\n\nProduce your output now. Include all required headings: ${agent.requiredHeadings.join(', ')}.`;
|
|
94
120
|
|
|
95
|
-
|
|
121
|
+
return prompt;
|
|
96
122
|
}
|
|
97
123
|
|
|
98
124
|
// ─── Orchestration ───────────────────────────────────────────────────────────
|
|
@@ -115,6 +141,9 @@ export async function orchestrateAgents(
|
|
|
115
141
|
maxParallel: number,
|
|
116
142
|
brief: StarterBrief,
|
|
117
143
|
spawnFn: SpawnFn,
|
|
144
|
+
previousOutputs?: Record<string, string>,
|
|
145
|
+
userFeedback?: string,
|
|
146
|
+
onComplete?: OnAgentComplete,
|
|
118
147
|
): Promise<AgentResult[]> {
|
|
119
148
|
const results: AgentResult[] = [];
|
|
120
149
|
|
|
@@ -126,23 +155,40 @@ export async function orchestrateAgents(
|
|
|
126
155
|
const batchPromises = batch.map(async (agent): Promise<AgentResult> => {
|
|
127
156
|
const start = Date.now();
|
|
128
157
|
try {
|
|
129
|
-
const
|
|
158
|
+
const previousOutput = previousOutputs?.[agent.name];
|
|
159
|
+
const prompt = buildAgentPrompt(agent, brief, previousOutput, userFeedback);
|
|
130
160
|
const content = await spawnFn(prompt);
|
|
131
|
-
|
|
161
|
+
const result: AgentResult = {
|
|
132
162
|
agent: agent.name,
|
|
133
163
|
content,
|
|
134
164
|
status: 'complete',
|
|
135
165
|
duration_ms: Date.now() - start,
|
|
136
166
|
};
|
|
167
|
+
if (onComplete) {
|
|
168
|
+
onComplete({
|
|
169
|
+
agent: agent.name,
|
|
170
|
+
headline: extractHeadline(result.content, agent.name),
|
|
171
|
+
status: result.status,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
137
175
|
} catch (err: unknown) {
|
|
138
176
|
const message = err instanceof Error ? err.message : String(err);
|
|
139
|
-
|
|
177
|
+
const result: AgentResult = {
|
|
140
178
|
agent: agent.name,
|
|
141
179
|
content: '',
|
|
142
180
|
status: 'failed',
|
|
143
181
|
error: message,
|
|
144
182
|
duration_ms: Date.now() - start,
|
|
145
183
|
};
|
|
184
|
+
if (onComplete) {
|
|
185
|
+
onComplete({
|
|
186
|
+
agent: agent.name,
|
|
187
|
+
headline: message || 'Agent failed',
|
|
188
|
+
status: 'failed',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
146
192
|
}
|
|
147
193
|
});
|
|
148
194
|
|