edsger 0.45.1 → 0.47.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.
Files changed (140) hide show
  1. package/.claude/settings.local.json +3 -23
  2. package/dist/api/__tests__/app-store.test.d.ts +7 -0
  3. package/dist/api/__tests__/app-store.test.js +60 -0
  4. package/dist/api/__tests__/intelligence.test.d.ts +11 -0
  5. package/dist/api/__tests__/intelligence.test.js +315 -0
  6. package/dist/api/features/__tests__/feature-utils.test.d.ts +4 -0
  7. package/dist/api/features/__tests__/feature-utils.test.js +370 -0
  8. package/dist/api/features/__tests__/status-updater.test.d.ts +4 -0
  9. package/dist/api/features/__tests__/status-updater.test.js +88 -0
  10. package/dist/commands/build/__tests__/build.test.d.ts +5 -0
  11. package/dist/commands/build/__tests__/build.test.js +206 -0
  12. package/dist/commands/build/__tests__/detect-project.test.d.ts +6 -0
  13. package/dist/commands/build/__tests__/detect-project.test.js +160 -0
  14. package/dist/commands/build/__tests__/run-build.test.d.ts +6 -0
  15. package/dist/commands/build/__tests__/run-build.test.js +433 -0
  16. package/dist/commands/intelligence/__tests__/command.test.d.ts +4 -0
  17. package/dist/commands/intelligence/__tests__/command.test.js +48 -0
  18. package/dist/commands/run-sheet/index.js +6 -0
  19. package/dist/commands/workflow/core/__tests__/feature-filter.test.d.ts +5 -0
  20. package/dist/commands/workflow/core/__tests__/feature-filter.test.js +316 -0
  21. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.d.ts +4 -0
  22. package/dist/commands/workflow/core/__tests__/pipeline-evaluator.test.js +397 -0
  23. package/dist/commands/workflow/core/__tests__/state-manager.test.d.ts +4 -0
  24. package/dist/commands/workflow/core/__tests__/state-manager.test.js +384 -0
  25. package/dist/commands/workflow/executors/phase-executor.js +3 -1
  26. package/dist/commands/workflow/phase-orchestrator.js +1 -2
  27. package/dist/config/__tests__/config.test.d.ts +4 -0
  28. package/dist/config/__tests__/config.test.js +286 -0
  29. package/dist/config/__tests__/feature-status.test.d.ts +4 -0
  30. package/dist/config/__tests__/feature-status.test.js +111 -0
  31. package/dist/errors/__tests__/index.test.d.ts +4 -0
  32. package/dist/errors/__tests__/index.test.js +349 -0
  33. package/dist/index.js +0 -0
  34. package/dist/phases/app-store-generation/__tests__/agent.test.d.ts +5 -0
  35. package/dist/phases/app-store-generation/__tests__/agent.test.js +142 -0
  36. package/dist/phases/app-store-generation/__tests__/context.test.d.ts +4 -0
  37. package/dist/phases/app-store-generation/__tests__/context.test.js +284 -0
  38. package/dist/phases/app-store-generation/__tests__/prompts.test.d.ts +4 -0
  39. package/dist/phases/app-store-generation/__tests__/prompts.test.js +122 -0
  40. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.d.ts +5 -0
  41. package/dist/phases/app-store-generation/__tests__/screenshot-composer.test.js +826 -0
  42. package/dist/phases/app-store-generation/index.js +1 -2
  43. package/dist/phases/branch-planning/index.js +1 -2
  44. package/dist/phases/bug-fixing/analyzer.js +1 -2
  45. package/dist/phases/code-implementation/index.js +1 -2
  46. package/dist/phases/code-refine/index.js +1 -2
  47. package/dist/phases/code-review/__tests__/diff-utils.test.d.ts +1 -0
  48. package/dist/phases/code-review/__tests__/diff-utils.test.js +101 -0
  49. package/dist/phases/code-review/index.js +1 -2
  50. package/dist/phases/code-testing/analyzer.js +1 -2
  51. package/dist/phases/feature-analysis/index.js +1 -2
  52. package/dist/phases/functional-testing/analyzer.js +1 -2
  53. package/dist/phases/growth-analysis/index.js +1 -2
  54. package/dist/phases/intelligence-analysis/__tests__/context.test.d.ts +4 -0
  55. package/dist/phases/intelligence-analysis/__tests__/context.test.js +192 -0
  56. package/dist/phases/intelligence-analysis/__tests__/matching.test.d.ts +13 -0
  57. package/dist/phases/intelligence-analysis/__tests__/matching.test.js +154 -0
  58. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.d.ts +5 -0
  59. package/dist/phases/intelligence-analysis/__tests__/orchestration.test.js +378 -0
  60. package/dist/phases/intelligence-analysis/__tests__/prompts.test.d.ts +4 -0
  61. package/dist/phases/intelligence-analysis/__tests__/prompts.test.js +33 -0
  62. package/dist/phases/pr-execution/__tests__/file-assigner.test.d.ts +1 -0
  63. package/dist/phases/pr-execution/__tests__/file-assigner.test.js +303 -0
  64. package/dist/phases/pr-execution/index.js +1 -0
  65. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.d.ts +1 -0
  66. package/dist/phases/pr-resolve/__tests__/checklist-learner.test.js +157 -0
  67. package/dist/phases/pr-resolve/__tests__/prompts.test.d.ts +1 -0
  68. package/dist/phases/pr-resolve/__tests__/prompts.test.js +116 -0
  69. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.d.ts +1 -0
  70. package/dist/phases/pr-resolve/__tests__/resolve-mapping.test.js +138 -0
  71. package/dist/phases/pr-resolve/__tests__/types.test.d.ts +1 -0
  72. package/dist/phases/pr-resolve/__tests__/types.test.js +43 -0
  73. package/dist/phases/pr-resolve/__tests__/workspace.test.d.ts +1 -0
  74. package/dist/phases/pr-resolve/__tests__/workspace.test.js +111 -0
  75. package/dist/phases/pr-review/__tests__/prompts.test.d.ts +1 -0
  76. package/dist/phases/pr-review/__tests__/prompts.test.js +49 -0
  77. package/dist/phases/pr-review/__tests__/review-comments.test.d.ts +1 -0
  78. package/dist/phases/pr-review/__tests__/review-comments.test.js +110 -0
  79. package/dist/phases/pr-shared/__tests__/agent-utils.test.d.ts +1 -0
  80. package/dist/phases/pr-shared/__tests__/agent-utils.test.js +91 -0
  81. package/dist/phases/pr-shared/__tests__/context.test.d.ts +1 -0
  82. package/dist/phases/pr-shared/__tests__/context.test.js +94 -0
  83. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.d.ts +1 -0
  84. package/dist/phases/pr-splitting/__tests__/import-dep-validator.test.js +331 -0
  85. package/dist/phases/pr-splitting/index.js +1 -2
  86. package/dist/phases/release-sync/github.d.ts +12 -0
  87. package/dist/phases/release-sync/github.js +39 -0
  88. package/dist/phases/release-sync/snapshot.js +0 -1
  89. package/dist/phases/run-sheet/index.d.ts +15 -0
  90. package/dist/phases/run-sheet/index.js +161 -29
  91. package/dist/phases/run-sheet/render.d.ts +23 -5
  92. package/dist/phases/run-sheet/render.js +195 -31
  93. package/dist/phases/smoke-test/__tests__/agent.test.d.ts +4 -0
  94. package/dist/phases/smoke-test/__tests__/agent.test.js +84 -0
  95. package/dist/phases/smoke-test/__tests__/github.test.d.ts +9 -0
  96. package/dist/phases/smoke-test/__tests__/github.test.js +120 -0
  97. package/dist/phases/smoke-test/__tests__/snapshot.test.d.ts +8 -0
  98. package/dist/phases/smoke-test/__tests__/snapshot.test.js +93 -0
  99. package/dist/phases/smoke-test/agent.js +2 -4
  100. package/dist/phases/smoke-test/github.d.ts +54 -0
  101. package/dist/phases/smoke-test/github.js +101 -0
  102. package/dist/phases/smoke-test/index.js +11 -6
  103. package/dist/phases/smoke-test/snapshot.d.ts +27 -0
  104. package/dist/phases/smoke-test/snapshot.js +157 -0
  105. package/dist/phases/technical-design/index.js +1 -2
  106. package/dist/phases/test-cases-analysis/index.js +1 -2
  107. package/dist/phases/user-stories-analysis/index.js +1 -2
  108. package/dist/services/coaching/__tests__/coaching-agent.test.d.ts +1 -0
  109. package/dist/services/coaching/__tests__/coaching-agent.test.js +74 -0
  110. package/dist/services/coaching/__tests__/coaching-loop.test.d.ts +1 -0
  111. package/dist/services/coaching/__tests__/coaching-loop.test.js +59 -0
  112. package/dist/services/coaching/__tests__/self-rating.test.d.ts +1 -0
  113. package/dist/services/coaching/__tests__/self-rating.test.js +188 -0
  114. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.d.ts +4 -0
  115. package/dist/services/lifecycle-agent/__tests__/phase-criteria.test.js +133 -0
  116. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.d.ts +4 -0
  117. package/dist/services/lifecycle-agent/__tests__/transition-rules.test.js +336 -0
  118. package/dist/services/lifecycle-agent/index.d.ts +24 -0
  119. package/dist/services/lifecycle-agent/index.js +25 -0
  120. package/dist/services/lifecycle-agent/phase-criteria.d.ts +57 -0
  121. package/dist/services/lifecycle-agent/phase-criteria.js +335 -0
  122. package/dist/services/lifecycle-agent/transition-rules.d.ts +60 -0
  123. package/dist/services/lifecycle-agent/transition-rules.js +184 -0
  124. package/dist/services/lifecycle-agent/types.d.ts +190 -0
  125. package/dist/services/lifecycle-agent/types.js +12 -0
  126. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.d.ts +1 -0
  127. package/dist/services/phase-hooks/__tests__/bindings-fetcher.test.js +122 -0
  128. package/dist/services/phase-hooks/__tests__/hook-executor.test.d.ts +1 -0
  129. package/dist/services/phase-hooks/__tests__/hook-executor.test.js +321 -0
  130. package/dist/services/phase-hooks/__tests__/hook-runner.test.d.ts +1 -0
  131. package/dist/services/phase-hooks/__tests__/hook-runner.test.js +261 -0
  132. package/dist/services/phase-hooks/__tests__/plugin-loader.test.d.ts +1 -0
  133. package/dist/services/phase-hooks/__tests__/plugin-loader.test.js +158 -0
  134. package/dist/services/video/__tests__/video-pipeline.test.d.ts +6 -0
  135. package/dist/services/video/__tests__/video-pipeline.test.js +249 -0
  136. package/dist/workspace/__tests__/workspace-manager.test.d.ts +7 -0
  137. package/dist/workspace/__tests__/workspace-manager.test.js +52 -0
  138. package/dist/workspace/workspace-manager.js +17 -4
  139. package/package.json +1 -1
  140. package/.env.local +0 -12
@@ -13,9 +13,8 @@ function truncateKeywords(keywords) {
13
13
  }
14
14
  return keywords.slice(0, 100).replace(/,[^,]*$/, '');
15
15
  }
16
- export const generateAppStoreAssets = async (options, config
17
16
  // eslint-disable-next-line complexity
18
- ) => {
17
+ export const generateAppStoreAssets = async (options, config) => {
19
18
  const { productId, targetStore, screenshotsOnly, listingsOnly, verbose } = options;
20
19
  if (verbose) {
21
20
  logInfo(`Starting app store generation for product: ${productId}`);
@@ -116,9 +116,8 @@ async function persistBranches(sortedBranches, featureId, verbose) {
116
116
  }
117
117
  }
118
118
  }
119
- export const planFeatureBranches = async (options, config
120
119
  // eslint-disable-next-line complexity
121
- ) => {
120
+ export const planFeatureBranches = async (options, config) => {
122
121
  const { featureId, verbose, replaceExisting } = options;
123
122
  if (verbose) {
124
123
  logInfo(`Starting branch planning for feature ID: ${featureId}`);
@@ -32,9 +32,8 @@ async function* prompt(bugFixPrompt) {
32
32
  setTimeout(res, 10000);
33
33
  });
34
34
  }
35
- export const fixTestFailures = async (options, config
36
35
  // eslint-disable-next-line complexity
37
- ) => {
36
+ export const fixTestFailures = async (options, config) => {
38
37
  const { featureId, testErrors, attemptNumber = 1, verbose } = options;
39
38
  if (verbose) {
40
39
  logInfo(`Starting bug fixing for feature ID: ${featureId} (Attempt ${attemptNumber})`);
@@ -170,9 +170,8 @@ message, lastAssistantResponse, featureId, verbose
170
170
  }
171
171
  return null;
172
172
  }
173
- export const implementFeatureCode = async (options, config, checklistContext
174
173
  // eslint-disable-next-line complexity
175
- ) => {
174
+ export const implementFeatureCode = async (options, config, checklistContext) => {
176
175
  const { featureId, verbose, baseBranch = 'main' } = options;
177
176
  if (verbose) {
178
177
  logInfo(`Starting code implementation for feature ID: ${featureId}`);
@@ -41,9 +41,8 @@ export const MAX_REFINE_ITERATIONS = 10;
41
41
  * Similar to technical-design, this includes an iterative improvement cycle:
42
42
  * refine → verification → improve → re-refine (if needed)
43
43
  */
44
- export const refineCodeFromPRFeedback = async (options, config, checklistContext
45
44
  // eslint-disable-next-line complexity
46
- ) => {
45
+ export const refineCodeFromPRFeedback = async (options, config, checklistContext) => {
47
46
  const { featureId, githubToken, verbose } = options;
48
47
  if (verbose) {
49
48
  logInfo(`Starting code refine for feature ID: ${featureId}`);
@@ -0,0 +1,101 @@
1
+ import assert from 'node:assert';
2
+ import { describe, it } from 'node:test';
3
+ import { buildLineToPositionMap, findClosestPosition } from '../diff-utils.js';
4
+ void describe('buildLineToPositionMap', () => {
5
+ void it('maps simple additions correctly', () => {
6
+ const patch = `@@ -1,3 +1,4 @@
7
+ line 1
8
+ +new line
9
+ line 2
10
+ line 3`;
11
+ const map = buildLineToPositionMap(patch);
12
+ // line 1 is at position 1 (new file line 1)
13
+ assert.strictEqual(map.get(1), 1);
14
+ // new line is at position 2 (new file line 2)
15
+ assert.strictEqual(map.get(2), 2);
16
+ // line 2 is at position 3 (new file line 3)
17
+ assert.strictEqual(map.get(3), 3);
18
+ // line 3 is at position 4 (new file line 4)
19
+ assert.strictEqual(map.get(4), 4);
20
+ });
21
+ void it('handles deletions - deleted lines have no new file line number', () => {
22
+ const patch = `@@ -1,3 +1,2 @@
23
+ line 1
24
+ -deleted line
25
+ line 3`;
26
+ const map = buildLineToPositionMap(patch);
27
+ assert.strictEqual(map.get(1), 1);
28
+ // deleted line takes position 2 but no new line
29
+ // line 3 in old file becomes line 2 in new file, position 3
30
+ assert.strictEqual(map.get(2), 3);
31
+ });
32
+ void it('handles multiple hunks', () => {
33
+ const patch = `@@ -1,2 +1,2 @@
34
+ line 1
35
+ +added
36
+ @@ -10,2 +10,2 @@
37
+ line 10
38
+ +added at 11`;
39
+ const map = buildLineToPositionMap(patch);
40
+ // First hunk: line 1 -> pos 1, added -> pos 2
41
+ assert.strictEqual(map.get(1), 1);
42
+ assert.strictEqual(map.get(2), 2);
43
+ // Second hunk: line 10 -> pos 3, added at 11 -> pos 4
44
+ assert.strictEqual(map.get(10), 3);
45
+ assert.strictEqual(map.get(11), 4);
46
+ });
47
+ void it('returns empty map for patch with only hunk header', () => {
48
+ const patch = '@@ -0,0 +1 @@';
49
+ const map = buildLineToPositionMap(patch);
50
+ // Hunk header only, no content lines
51
+ assert.strictEqual(map.size, 0);
52
+ });
53
+ void it('handles hunk starting at line other than 1', () => {
54
+ const patch = `@@ -50,3 +50,3 @@
55
+ context
56
+ -old
57
+ +new`;
58
+ const map = buildLineToPositionMap(patch);
59
+ assert.strictEqual(map.get(50), 1);
60
+ // -old takes position 2 (no new line)
61
+ // +new takes position 3, new file line 51
62
+ assert.strictEqual(map.get(51), 3);
63
+ });
64
+ });
65
+ void describe('findClosestPosition', () => {
66
+ void it('returns exact match when available', () => {
67
+ const map = new Map([
68
+ [10, 5],
69
+ [11, 6],
70
+ [12, 7],
71
+ ]);
72
+ const result = findClosestPosition(11, map);
73
+ assert.deepStrictEqual(result, { position: 6, actualLine: 11 });
74
+ });
75
+ void it('returns nearby line below first', () => {
76
+ const map = new Map([
77
+ [10, 5],
78
+ [15, 8],
79
+ ]);
80
+ // Line 12 not in map, closest below is 15 (offset 3)
81
+ // closest above is 10 (offset 2) - but we check below first at each offset
82
+ // offset 1: check 13 (no), check 11 (no)
83
+ // offset 2: check 14 (no), check 10 (yes!)
84
+ const result = findClosestPosition(12, map);
85
+ assert.deepStrictEqual(result, { position: 5, actualLine: 10 });
86
+ });
87
+ void it('returns null when no line within range', () => {
88
+ const map = new Map([
89
+ [1, 1],
90
+ [100, 50],
91
+ ]);
92
+ // Line 50 is too far from both 1 and 100
93
+ const result = findClosestPosition(50, map);
94
+ assert.strictEqual(result, null);
95
+ });
96
+ void it('returns null for empty map', () => {
97
+ const map = new Map();
98
+ const result = findClosestPosition(5, map);
99
+ assert.strictEqual(result, null);
100
+ });
101
+ });
@@ -67,9 +67,8 @@ baseBranchInfo, baseBranchForRebase, originalBaseBranchForRebase, verbose) {
67
67
  /**
68
68
  * Main code review function
69
69
  */
70
- export const reviewPullRequest = async (options, config, checklistContext
71
70
  // eslint-disable-next-line complexity
72
- ) => {
71
+ export const reviewPullRequest = async (options, config, checklistContext) => {
73
72
  const { featureId, githubToken, verbose } = options;
74
73
  if (verbose) {
75
74
  logInfo(`Starting code review for feature ID: ${featureId}`);
@@ -31,9 +31,8 @@ async function* prompt(testingPrompt) {
31
31
  setTimeout(res, 10000);
32
32
  });
33
33
  }
34
- export const writeCodeTests = async (options, config
35
34
  // eslint-disable-next-line complexity
36
- ) => {
35
+ export const writeCodeTests = async (options, config) => {
37
36
  const { featureId, verbose } = options;
38
37
  if (verbose) {
39
38
  logInfo(`Starting code testing phase for feature ID: ${featureId}`);
@@ -6,9 +6,8 @@ import { executeAnalysisQuery, parseAnalysisResult } from './agent.js';
6
6
  import { prepareAnalysisContext } from './context.js';
7
7
  import { buildAnalysisResult, deleteArtifacts, deleteSpecificArtifacts, getAllDraftArtifactIds, resetReadyArtifactsToDraft, saveAnalysisArtifactsAsDraft, updateArtifactsToReady, } from './outcome.js';
8
8
  import { createFeatureAnalysisSystemPrompt } from './prompts.js';
9
- export const analyseFeature = async (options, config, checklistContext
10
9
  // eslint-disable-next-line complexity
11
- ) => {
10
+ export const analyseFeature = async (options, config, checklistContext) => {
12
11
  const { featureId, verbose } = options;
13
12
  if (verbose) {
14
13
  logInfo(`Starting feature analysis for feature ID: ${featureId}`);
@@ -326,9 +326,8 @@ async function saveTestResults(featureId, testStatus, structuredTestResult, last
326
326
  }
327
327
  return testReportResult;
328
328
  }
329
- export const runFunctionalTesting = async (options, config, checklistContext
330
329
  // eslint-disable-next-line complexity
331
- ) => {
330
+ export const runFunctionalTesting = async (options, config, checklistContext) => {
332
331
  const { featureId, verbose } = options;
333
332
  if (verbose) {
334
333
  logInfo(`Starting functional testing for feature ID: ${featureId}`);
@@ -49,9 +49,8 @@ contentSuggestions) {
49
49
  }
50
50
  return plans;
51
51
  }
52
- export const analyseGrowth = async (options, config
53
52
  // eslint-disable-next-line complexity
54
- ) => {
53
+ export const analyseGrowth = async (options, config) => {
55
54
  const { productId, verbose, guidance, analysisId } = options;
56
55
  if (verbose) {
57
56
  logInfo(`Starting growth analysis for product ID: ${productId}`);
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Unit tests for intelligence analysis context formatting.
3
+ */
4
+ export {};
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Unit tests for intelligence analysis context formatting.
3
+ */
4
+ import assert from 'node:assert';
5
+ import { describe, it } from 'node:test';
6
+ import { formatContextForPrompt, } from '../context.js';
7
+ const makeContext = (overrides = {}) => ({
8
+ product: {
9
+ id: 'prod-001',
10
+ name: 'Edsger',
11
+ description: 'AI-powered software development automation.',
12
+ features: [
13
+ {
14
+ name: 'Code Generation',
15
+ description: 'AI writes code',
16
+ status: 'shipped',
17
+ },
18
+ {
19
+ name: 'Growth Analysis',
20
+ description: 'Marketing automation',
21
+ status: 'shipped',
22
+ },
23
+ ],
24
+ },
25
+ confirmedCompetitors: [],
26
+ suggestedCompetitors: [],
27
+ previousSnapshots: new Map(),
28
+ ...overrides,
29
+ });
30
+ const makeCompetitor = (name, id = `comp-${name.toLowerCase()}`) => ({
31
+ id,
32
+ product_id: 'prod-001',
33
+ name,
34
+ url: `https://${name.toLowerCase()}.com`,
35
+ app_store_url: null,
36
+ play_store_url: null,
37
+ description: `${name} description`,
38
+ category: 'developer-tools',
39
+ status: 'confirmed',
40
+ discovery_source: 'ai_discovery',
41
+ discovery_reason: 'Frequently compared',
42
+ notes: null,
43
+ created_by: 'user-1',
44
+ created_at: '2026-03-25',
45
+ updated_at: '2026-03-25',
46
+ });
47
+ // ============================================================
48
+ // Product info in context
49
+ // ============================================================
50
+ void describe('formatContextForPrompt — product info', () => {
51
+ void it('should include product name', () => {
52
+ const result = formatContextForPrompt(makeContext());
53
+ assert.ok(result.includes('Edsger'));
54
+ });
55
+ void it('should include product description', () => {
56
+ const result = formatContextForPrompt(makeContext());
57
+ assert.ok(result.includes('AI-powered software development automation'));
58
+ });
59
+ void it('should include product ID', () => {
60
+ const result = formatContextForPrompt(makeContext());
61
+ assert.ok(result.includes('prod-001'));
62
+ });
63
+ void it('should list features with bold names', () => {
64
+ const result = formatContextForPrompt(makeContext());
65
+ assert.ok(result.includes('**Code Generation**'));
66
+ assert.ok(result.includes('**Growth Analysis**'));
67
+ });
68
+ void it('should show feature count', () => {
69
+ const result = formatContextForPrompt(makeContext());
70
+ assert.ok(result.includes('Product Features (2)'));
71
+ });
72
+ void it('should handle empty features', () => {
73
+ const ctx = makeContext({
74
+ product: { id: 'p1', name: 'X', description: '', features: [] },
75
+ });
76
+ const result = formatContextForPrompt(ctx);
77
+ assert.ok(result.includes('No features listed'));
78
+ });
79
+ void it('should handle missing description', () => {
80
+ const ctx = makeContext({
81
+ product: {
82
+ id: 'p1',
83
+ name: 'X',
84
+ description: undefined,
85
+ features: [],
86
+ },
87
+ });
88
+ const result = formatContextForPrompt(ctx);
89
+ assert.ok(result.includes('No description provided'));
90
+ });
91
+ });
92
+ // ============================================================
93
+ // No competitors — discovery prompt
94
+ // ============================================================
95
+ void describe('formatContextForPrompt — no competitors', () => {
96
+ void it('should indicate no competitors registered', () => {
97
+ const result = formatContextForPrompt(makeContext());
98
+ assert.ok(result.includes('No competitors registered yet'));
99
+ });
100
+ void it('should instruct to discover competitors', () => {
101
+ const result = formatContextForPrompt(makeContext());
102
+ assert.ok(result.includes('discover'));
103
+ });
104
+ });
105
+ // ============================================================
106
+ // With confirmed competitors
107
+ // ============================================================
108
+ void describe('formatContextForPrompt — with confirmed competitors', () => {
109
+ const competitor = makeCompetitor('Linear');
110
+ void it('should show competitor name as heading', () => {
111
+ const ctx = makeContext({ confirmedCompetitors: [competitor] });
112
+ const result = formatContextForPrompt(ctx);
113
+ assert.ok(result.includes('### Linear'));
114
+ });
115
+ void it('should show competitor ID', () => {
116
+ const ctx = makeContext({ confirmedCompetitors: [competitor] });
117
+ const result = formatContextForPrompt(ctx);
118
+ assert.ok(result.includes('comp-linear'));
119
+ });
120
+ void it('should show competitor URL', () => {
121
+ const ctx = makeContext({ confirmedCompetitors: [competitor] });
122
+ const result = formatContextForPrompt(ctx);
123
+ assert.ok(result.includes('https://linear.com'));
124
+ });
125
+ void it('should show confirmed count', () => {
126
+ const ctx = makeContext({
127
+ confirmedCompetitors: [makeCompetitor('A'), makeCompetitor('B')],
128
+ });
129
+ const result = formatContextForPrompt(ctx);
130
+ assert.ok(result.includes('Confirmed Competitors (2)'));
131
+ });
132
+ void it('should show N/A for missing app store URLs', () => {
133
+ const ctx = makeContext({ confirmedCompetitors: [competitor] });
134
+ const result = formatContextForPrompt(ctx);
135
+ assert.ok(result.includes('App Store**: N/A'));
136
+ });
137
+ void it('should include snapshot data when available', () => {
138
+ const snapshots = new Map();
139
+ snapshots.set('comp-linear', [
140
+ {
141
+ id: 'snap-1',
142
+ competitor_id: 'comp-linear',
143
+ product_id: 'prod-001',
144
+ features: [
145
+ { name: 'Issues', description: 'Issue tracking', is_new: false },
146
+ ],
147
+ pricing: { model: 'freemium' },
148
+ tech_stack: [],
149
+ app_rating: 4.5,
150
+ app_review_count: 1200,
151
+ app_version: '2.0',
152
+ app_last_updated: null,
153
+ recent_reviews: [],
154
+ social_mentions: {},
155
+ changes_detected: [],
156
+ source: 'ai_analysis',
157
+ raw_data: null,
158
+ created_at: '2026-03-25',
159
+ },
160
+ ]);
161
+ const ctx = makeContext({
162
+ confirmedCompetitors: [competitor],
163
+ previousSnapshots: snapshots,
164
+ });
165
+ const result = formatContextForPrompt(ctx);
166
+ assert.ok(result.includes('Rating: 4.5'));
167
+ assert.ok(result.includes('Reviews: 1200'));
168
+ assert.ok(result.includes('Pricing model: freemium'));
169
+ });
170
+ void it('should show "No previous data" without snapshots', () => {
171
+ const ctx = makeContext({ confirmedCompetitors: [competitor] });
172
+ const result = formatContextForPrompt(ctx);
173
+ assert.ok(result.includes('No previous data'));
174
+ });
175
+ });
176
+ // ============================================================
177
+ // Human guidance
178
+ // ============================================================
179
+ void describe('formatContextForPrompt — guidance', () => {
180
+ void it('should include guidance when provided', () => {
181
+ const result = formatContextForPrompt(makeContext(), 'Focus on pricing changes');
182
+ assert.ok(result.includes('Focus on pricing changes'));
183
+ });
184
+ void it('should have priority callout for guidance', () => {
185
+ const result = formatContextForPrompt(makeContext(), 'Focus on pricing');
186
+ assert.ok(result.includes('Follow this guidance closely'));
187
+ });
188
+ void it('should not include guidance section when absent', () => {
189
+ const result = formatContextForPrompt(makeContext());
190
+ assert.ok(!result.includes('Human Analysis Guidance'));
191
+ });
192
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Unit tests for competitor snapshot matching logic.
3
+ *
4
+ * Tests the PRODUCTION resolveCompetitorId and buildCompetitorIndex
5
+ * functions exported from index.ts.
6
+ *
7
+ * The matching uses a 4-tier strategy:
8
+ * 1. Direct competitor_id match
9
+ * 2. Exact name match (case-insensitive)
10
+ * 3. Normalized name match (strip suffixes like "App", "Inc")
11
+ * 4. Substring match
12
+ */
13
+ export {};
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Unit tests for competitor snapshot matching logic.
3
+ *
4
+ * Tests the PRODUCTION resolveCompetitorId and buildCompetitorIndex
5
+ * functions exported from index.ts.
6
+ *
7
+ * The matching uses a 4-tier strategy:
8
+ * 1. Direct competitor_id match
9
+ * 2. Exact name match (case-insensitive)
10
+ * 3. Normalized name match (strip suffixes like "App", "Inc")
11
+ * 4. Substring match
12
+ */
13
+ import assert from 'node:assert';
14
+ import { describe, it } from 'node:test';
15
+ import { buildCompetitorIndex, resolveCompetitorId } from '../index.js';
16
+ const competitors = [
17
+ { id: 'id-linear', name: 'Linear' },
18
+ { id: 'id-cursor', name: 'Cursor' },
19
+ { id: 'id-github-copilot', name: 'GitHub Copilot' },
20
+ { id: 'id-vercel', name: 'Vercel Inc' },
21
+ ];
22
+ const { idSet, nameToId } = buildCompetitorIndex(competitors);
23
+ // ============================================================
24
+ // buildCompetitorIndex
25
+ // ============================================================
26
+ void describe('buildCompetitorIndex', () => {
27
+ void it('should populate idSet with all competitor IDs', () => {
28
+ assert.strictEqual(idSet.size, 4);
29
+ assert.ok(idSet.has('id-linear'));
30
+ assert.ok(idSet.has('id-cursor'));
31
+ assert.ok(idSet.has('id-github-copilot'));
32
+ assert.ok(idSet.has('id-vercel'));
33
+ });
34
+ void it('should index exact names (lowercased)', () => {
35
+ assert.strictEqual(nameToId.get('linear'), 'id-linear');
36
+ assert.strictEqual(nameToId.get('cursor'), 'id-cursor');
37
+ assert.strictEqual(nameToId.get('github copilot'), 'id-github-copilot');
38
+ });
39
+ void it('should index normalized names (suffix stripped)', () => {
40
+ // "Vercel Inc" → "vercel" should also be indexed
41
+ assert.strictEqual(nameToId.get('vercel'), 'id-vercel');
42
+ });
43
+ void it('should not double-index when name has no suffix', () => {
44
+ // "Linear" has no suffix to strip, so only "linear" should be in map
45
+ // Count entries for id-linear
46
+ let count = 0;
47
+ for (const [, id] of nameToId) {
48
+ if (id === 'id-linear') {
49
+ count++;
50
+ }
51
+ }
52
+ assert.strictEqual(count, 1);
53
+ });
54
+ void it('should handle empty list', () => {
55
+ const { idSet: emptyIds, nameToId: emptyNames } = buildCompetitorIndex([]);
56
+ assert.strictEqual(emptyIds.size, 0);
57
+ assert.strictEqual(emptyNames.size, 0);
58
+ });
59
+ });
60
+ // ============================================================
61
+ // Tier 1: Direct ID match
62
+ // ============================================================
63
+ void describe('resolveCompetitorId — direct ID match', () => {
64
+ void it('should match by exact competitor_id', () => {
65
+ const result = resolveCompetitorId({ competitor_id: 'id-linear', competitor_name: 'Whatever' }, idSet, nameToId);
66
+ assert.strictEqual(result, 'id-linear');
67
+ });
68
+ void it('should ignore invalid competitor_id and fall through to name', () => {
69
+ const result = resolveCompetitorId({ competitor_id: 'id-nonexistent', competitor_name: 'Linear' }, idSet, nameToId);
70
+ assert.strictEqual(result, 'id-linear');
71
+ });
72
+ void it('should handle undefined competitor_id', () => {
73
+ const result = resolveCompetitorId({ competitor_name: 'Linear' }, idSet, nameToId);
74
+ assert.strictEqual(result, 'id-linear');
75
+ });
76
+ void it('should prefer ID over name when both match different competitors', () => {
77
+ // If AI returns id for Cursor but name "Linear", ID wins
78
+ const result = resolveCompetitorId({ competitor_id: 'id-cursor', competitor_name: 'Linear' }, idSet, nameToId);
79
+ assert.strictEqual(result, 'id-cursor');
80
+ });
81
+ });
82
+ // ============================================================
83
+ // Tier 2: Exact name match (case-insensitive)
84
+ // ============================================================
85
+ void describe('resolveCompetitorId — exact name match', () => {
86
+ void it('should match exact name', () => {
87
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'Linear' }, idSet, nameToId), 'id-linear');
88
+ });
89
+ void it('should match case-insensitively', () => {
90
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'linear' }, idSet, nameToId), 'id-linear');
91
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'LINEAR' }, idSet, nameToId), 'id-linear');
92
+ });
93
+ void it('should match multi-word names', () => {
94
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'GitHub Copilot' }, idSet, nameToId), 'id-github-copilot');
95
+ });
96
+ void it('should match multi-word names case-insensitively', () => {
97
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'github copilot' }, idSet, nameToId), 'id-github-copilot');
98
+ });
99
+ });
100
+ // ============================================================
101
+ // Tier 3: Normalized name match (strip suffixes)
102
+ // ============================================================
103
+ void describe('resolveCompetitorId — normalized name match', () => {
104
+ void it('should match "Vercel" against "Vercel Inc"', () => {
105
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'Vercel' }, idSet, nameToId), 'id-vercel');
106
+ });
107
+ void it('should match "Linear App" against "Linear"', () => {
108
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'Linear App' }, idSet, nameToId), 'id-linear');
109
+ });
110
+ void it('should match with ".io" suffix stripped', () => {
111
+ const { idSet: ids, nameToId: names } = buildCompetitorIndex([
112
+ { id: 'id-render', name: 'Render.io' },
113
+ ]);
114
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'Render' }, ids, names), 'id-render');
115
+ });
116
+ void it('should match with ".com" suffix stripped', () => {
117
+ const { idSet: ids, nameToId: names } = buildCompetitorIndex([
118
+ { id: 'id-netlify', name: 'Netlify.com' },
119
+ ]);
120
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'Netlify' }, ids, names), 'id-netlify');
121
+ });
122
+ void it('should match with "Ltd" suffix stripped', () => {
123
+ const { idSet: ids, nameToId: names } = buildCompetitorIndex([
124
+ { id: 'id-acme', name: 'Acme Ltd' },
125
+ ]);
126
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'Acme' }, ids, names), 'id-acme');
127
+ });
128
+ });
129
+ // ============================================================
130
+ // Tier 4: Substring match
131
+ // ============================================================
132
+ void describe('resolveCompetitorId — substring match', () => {
133
+ void it('should match when snapshot name contains competitor name', () => {
134
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'Cursor AI Editor' }, idSet, nameToId), 'id-cursor');
135
+ });
136
+ void it('should match when competitor name contains snapshot name', () => {
137
+ const { idSet: ids, nameToId: names } = buildCompetitorIndex([
138
+ { id: 'id-ghc', name: 'GitHub Copilot Enterprise' },
139
+ ]);
140
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'github copilot' }, ids, names), 'id-ghc');
141
+ });
142
+ });
143
+ // ============================================================
144
+ // No match
145
+ // ============================================================
146
+ void describe('resolveCompetitorId — no match', () => {
147
+ void it('should return undefined for unknown competitor', () => {
148
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'Completely Unknown Tool' }, idSet, nameToId), undefined);
149
+ });
150
+ void it('should return undefined with empty index', () => {
151
+ const { idSet: ids, nameToId: names } = buildCompetitorIndex([]);
152
+ assert.strictEqual(resolveCompetitorId({ competitor_name: 'Linear' }, ids, names), undefined);
153
+ });
154
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Integration tests for analyseIntelligence orchestration.
3
+ * Uses dependency injection to mock API calls and AI agent.
4
+ */
5
+ export {};