edsger 0.56.3 → 0.58.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 (81) hide show
  1. package/dist/api/chat.js +55 -2
  2. package/dist/api/cross-product.d.ts +8 -1
  3. package/dist/api/cross-product.js +44 -1
  4. package/dist/api/intelligence.js +98 -0
  5. package/dist/api/issues/get-issue.js +26 -0
  6. package/dist/api/issues/issue-utils.js +52 -0
  7. package/dist/api/issues/test-cases.js +89 -14
  8. package/dist/api/issues/update-issue.js +46 -8
  9. package/dist/api/issues/user-stories.js +89 -14
  10. package/dist/api/products/test-cases.d.ts +18 -0
  11. package/dist/api/products/test-cases.js +51 -0
  12. package/dist/api/products.js +21 -0
  13. package/dist/api/release-test-cases.js +38 -0
  14. package/dist/api/releases.js +86 -0
  15. package/dist/api/tasks.js +41 -4
  16. package/dist/api/test-reports.js +22 -4
  17. package/dist/api/user-psychology.d.ts +101 -0
  18. package/dist/api/user-psychology.js +143 -0
  19. package/dist/auth/auth-store.d.ts +33 -0
  20. package/dist/auth/auth-store.js +39 -0
  21. package/dist/commands/agent-workflow/chat-worker.js +187 -15
  22. package/dist/commands/agent-workflow/processor.d.ts +11 -0
  23. package/dist/commands/agent-workflow/processor.js +81 -2
  24. package/dist/commands/product-test-cases/index.d.ts +12 -0
  25. package/dist/commands/product-test-cases/index.js +40 -0
  26. package/dist/commands/screen-flow/index.d.ts +16 -0
  27. package/dist/commands/screen-flow/index.js +45 -0
  28. package/dist/commands/user-psychology/index.d.ts +7 -0
  29. package/dist/commands/user-psychology/index.js +51 -0
  30. package/dist/index.js +65 -0
  31. package/dist/phases/analyze-logs/index.js +27 -6
  32. package/dist/phases/bug-fixing/context-fetcher.js +26 -5
  33. package/dist/phases/find-features/index.js +53 -9
  34. package/dist/phases/find-shared/mcp.js +21 -0
  35. package/dist/phases/growth-analysis/context.d.ts +5 -3
  36. package/dist/phases/growth-analysis/context.js +52 -5
  37. package/dist/phases/output-contracts.js +140 -0
  38. package/dist/phases/pr-resolve/github-reply.d.ts +5 -2
  39. package/dist/phases/pr-resolve/github-reply.js +19 -3
  40. package/dist/phases/pr-resolve/index.js +19 -5
  41. package/dist/phases/pr-resolve/prompts.js +17 -18
  42. package/dist/phases/pr-shared/agent-utils.d.ts +11 -3
  43. package/dist/phases/pr-shared/agent-utils.js +48 -4
  44. package/dist/phases/product-test-cases/index.d.ts +25 -0
  45. package/dist/phases/product-test-cases/index.js +174 -0
  46. package/dist/phases/product-test-cases/prompts.d.ts +24 -0
  47. package/dist/phases/product-test-cases/prompts.js +80 -0
  48. package/dist/phases/product-test-cases/types.d.ts +17 -0
  49. package/dist/phases/product-test-cases/types.js +27 -0
  50. package/dist/phases/screen-flow/index.d.ts +23 -0
  51. package/dist/phases/screen-flow/index.js +285 -0
  52. package/dist/phases/screen-flow/mcp-server.d.ts +195 -0
  53. package/dist/phases/screen-flow/mcp-server.js +262 -0
  54. package/dist/phases/screen-flow/prompts.d.ts +19 -0
  55. package/dist/phases/screen-flow/prompts.js +41 -0
  56. package/dist/phases/screen-flow/theme.d.ts +19 -0
  57. package/dist/phases/screen-flow/theme.js +193 -0
  58. package/dist/phases/screen-flow/types.d.ts +130 -0
  59. package/dist/phases/screen-flow/types.js +81 -0
  60. package/dist/phases/user-psychology/agent.d.ts +16 -0
  61. package/dist/phases/user-psychology/agent.js +105 -0
  62. package/dist/phases/user-psychology/context.d.ts +10 -0
  63. package/dist/phases/user-psychology/context.js +65 -0
  64. package/dist/phases/user-psychology/index.d.ts +18 -0
  65. package/dist/phases/user-psychology/index.js +96 -0
  66. package/dist/phases/user-psychology/prompts.d.ts +2 -0
  67. package/dist/phases/user-psychology/prompts.js +41 -0
  68. package/dist/services/audit-logs.js +67 -9
  69. package/dist/services/branches.js +90 -14
  70. package/dist/services/phase-ratings.js +71 -9
  71. package/dist/services/product-logs.js +65 -5
  72. package/dist/services/pull-requests.js +74 -14
  73. package/dist/skills/phase/screen-flow/SKILL.md +78 -0
  74. package/dist/skills/phase/user-psychology/SKILL.md +135 -0
  75. package/dist/supabase/client.d.ts +23 -0
  76. package/dist/supabase/client.js +90 -0
  77. package/dist/system/session-manager.js +97 -24
  78. package/dist/types/index.d.ts +3 -0
  79. package/dist/utils/logger.js +24 -4
  80. package/package.json +4 -3
  81. package/vitest.config.ts +1 -0
@@ -3,6 +3,7 @@
3
3
  * Allows phases to manage branches via MCP
4
4
  */
5
5
  import { callMcpEndpoint } from '../api/mcp-client.js';
6
+ import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
6
7
  import { logDebug, logInfo, logSuccess } from '../utils/logger.js';
7
8
  /**
8
9
  * List all branches for an issue
@@ -12,10 +13,29 @@ export async function getBranches(options) {
12
13
  if (verbose) {
13
14
  logInfo(`Fetching branches for issue: ${issueId}`);
14
15
  }
15
- const result = (await callMcpEndpoint('branches/list', {
16
- issue_id: issueId,
17
- }));
18
- const branches = result?.branches || [];
16
+ let branches = null;
17
+ if (hasSupabaseSession()) {
18
+ try {
19
+ const { data, error } = await getSupabase()
20
+ .from('branches')
21
+ .select('*')
22
+ .eq('issue_id', issueId)
23
+ .order('created_at', { ascending: true });
24
+ if (error) {
25
+ throw new Error(error.message);
26
+ }
27
+ branches = (data || []);
28
+ }
29
+ catch {
30
+ // Fall through to MCP
31
+ }
32
+ }
33
+ if (branches == null) {
34
+ const result = (await callMcpEndpoint('branches/list', {
35
+ issue_id: issueId,
36
+ }));
37
+ branches = result?.branches || [];
38
+ }
19
39
  if (verbose) {
20
40
  logSuccess(`Found ${branches.length} branches`);
21
41
  }
@@ -42,11 +62,30 @@ export async function createBranches(options, branches) {
42
62
  if (verbose) {
43
63
  logInfo(`Creating ${branches.length} branches for issue: ${issueId}`);
44
64
  }
45
- const result = (await callMcpEndpoint('branches/create', {
46
- issue_id: issueId,
47
- branches,
48
- }));
49
- const createdBranches = result?.created_branches || [];
65
+ let createdBranches = null;
66
+ if (hasSupabaseSession() && branches.length > 0) {
67
+ try {
68
+ const rows = branches.map((b) => ({ issue_id: issueId, ...b }));
69
+ const { data, error } = await getSupabase()
70
+ .from('branches')
71
+ .insert(rows)
72
+ .select();
73
+ if (error) {
74
+ throw new Error(error.message);
75
+ }
76
+ createdBranches = (data || []);
77
+ }
78
+ catch {
79
+ // Fall through to MCP
80
+ }
81
+ }
82
+ if (createdBranches == null) {
83
+ const result = (await callMcpEndpoint('branches/create', {
84
+ issue_id: issueId,
85
+ branches,
86
+ }));
87
+ createdBranches = result?.created_branches || [];
88
+ }
50
89
  if (verbose) {
51
90
  logSuccess(`Created ${createdBranches.length} branches`);
52
91
  createdBranches.forEach((b, idx) => {
@@ -62,14 +101,35 @@ export async function updateBranch(branchId, updates, verbose) {
62
101
  if (verbose) {
63
102
  logInfo(`Updating branch: ${branchId}`);
64
103
  }
65
- const result = (await callMcpEndpoint('branches/update', {
66
- branch_id: branchId,
67
- ...updates,
68
- }));
104
+ let updated = null;
105
+ if (hasSupabaseSession()) {
106
+ try {
107
+ const { data, error } = await getSupabase()
108
+ .from('branches')
109
+ .update(updates)
110
+ .eq('id', branchId)
111
+ .select()
112
+ .single();
113
+ if (error) {
114
+ throw new Error(error.message);
115
+ }
116
+ updated = data ?? null;
117
+ }
118
+ catch {
119
+ // Fall through to MCP
120
+ }
121
+ }
122
+ if (updated == null) {
123
+ const result = (await callMcpEndpoint('branches/update', {
124
+ branch_id: branchId,
125
+ ...updates,
126
+ }));
127
+ updated = result?.branch;
128
+ }
69
129
  if (verbose) {
70
130
  logSuccess(`Branch updated successfully`);
71
131
  }
72
- return result?.branch;
132
+ return updated;
73
133
  }
74
134
  /**
75
135
  * Clear all branches for an issue (used before re-planning)
@@ -273,6 +333,22 @@ export async function getBranchById(branchId, verbose) {
273
333
  if (verbose) {
274
334
  logInfo(`Fetching branch: ${branchId}`);
275
335
  }
336
+ if (hasSupabaseSession()) {
337
+ try {
338
+ const { data, error } = await getSupabase()
339
+ .from('branches')
340
+ .select('*')
341
+ .eq('id', branchId)
342
+ .maybeSingle();
343
+ if (error) {
344
+ throw new Error(error.message);
345
+ }
346
+ return data ?? null;
347
+ }
348
+ catch {
349
+ // Fall through to MCP
350
+ }
351
+ }
276
352
  const result = (await callMcpEndpoint('branches/get', {
277
353
  branch_id: branchId,
278
354
  }));
@@ -3,13 +3,14 @@
3
3
  * Provides functions to create and query phase ratings for AI quality tracking
4
4
  */
5
5
  import { callMcpEndpoint } from '../api/mcp-client.js';
6
+ import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
6
7
  import { logDebug, logError, logInfo } from '../utils/logger.js';
7
8
  /**
8
9
  * Create a phase rating record in the database
9
10
  */
10
11
  export async function createPhaseRating(params, verbose) {
11
12
  try {
12
- const result = await callMcpEndpoint('phase_ratings/create', {
13
+ const payload = {
13
14
  issue_id: params.issueId,
14
15
  phase: params.phase,
15
16
  score: params.rating.score,
@@ -21,11 +22,34 @@ export async function createPhaseRating(params, verbose) {
21
22
  iteration: params.iteration,
22
23
  coaching_analysis: params.coachingAnalysis || null,
23
24
  metadata: params.metadata || {},
24
- });
25
+ };
26
+ let ratingId = null;
27
+ let usedSdk = false;
28
+ if (hasSupabaseSession()) {
29
+ try {
30
+ const { data, error } = await getSupabase()
31
+ .from('phase_ratings')
32
+ .insert(payload)
33
+ .select('id')
34
+ .single();
35
+ if (error) {
36
+ throw new Error(error.message);
37
+ }
38
+ ratingId = data?.id ?? null;
39
+ usedSdk = true;
40
+ }
41
+ catch {
42
+ // Fall through to MCP
43
+ }
44
+ }
45
+ if (!usedSdk) {
46
+ const result = await callMcpEndpoint('phase_ratings/create', payload);
47
+ ratingId = result?.id || null;
48
+ }
25
49
  if (verbose) {
26
50
  logInfo(`Logged phase rating: ${params.phase} score=${params.rating.score}/100 (iteration ${params.iteration})`);
27
51
  }
28
- return result?.id || null;
52
+ return ratingId;
29
53
  }
30
54
  catch (error) {
31
55
  logError(`Failed to create phase rating: ${error instanceof Error ? error.message : String(error)}`);
@@ -37,6 +61,24 @@ export async function createPhaseRating(params, verbose) {
37
61
  */
38
62
  export async function updatePhaseRatingCoachingAnalysis(ratingId, coachingAnalysis, verbose) {
39
63
  try {
64
+ if (hasSupabaseSession()) {
65
+ try {
66
+ const { error } = await getSupabase()
67
+ .from('phase_ratings')
68
+ .update({ coaching_analysis: coachingAnalysis })
69
+ .eq('id', ratingId);
70
+ if (error) {
71
+ throw new Error(error.message);
72
+ }
73
+ if (verbose) {
74
+ logDebug('Updated phase rating with coaching analysis', verbose);
75
+ }
76
+ return true;
77
+ }
78
+ catch {
79
+ // Fall through to MCP
80
+ }
81
+ }
40
82
  await callMcpEndpoint('phase_ratings/update', {
41
83
  id: ratingId,
42
84
  coaching_analysis: coachingAnalysis,
@@ -56,12 +98,32 @@ export async function updatePhaseRatingCoachingAnalysis(ratingId, coachingAnalys
56
98
  */
57
99
  export async function getPhaseRatings(issueId, phase, verbose) {
58
100
  try {
59
- const result = await callMcpEndpoint('phase_ratings/list', {
60
- issue_id: issueId,
61
- phase,
62
- });
63
- const ratings = (result?.ratings ||
64
- []);
101
+ let ratings = null;
102
+ if (hasSupabaseSession()) {
103
+ try {
104
+ const { data, error } = await getSupabase()
105
+ .from('phase_ratings')
106
+ .select('*')
107
+ .eq('issue_id', issueId)
108
+ .eq('phase', phase)
109
+ .order('iteration', { ascending: true });
110
+ if (error) {
111
+ throw new Error(error.message);
112
+ }
113
+ ratings = (data || []);
114
+ }
115
+ catch {
116
+ // Fall through to MCP
117
+ }
118
+ }
119
+ if (ratings == null) {
120
+ const result = await callMcpEndpoint('phase_ratings/list', {
121
+ issue_id: issueId,
122
+ phase,
123
+ });
124
+ ratings = (result?.ratings ||
125
+ []);
126
+ }
65
127
  if (verbose) {
66
128
  logDebug(`Fetched ${ratings.length} ratings for ${phase} (issue: ${issueId})`, verbose);
67
129
  }
@@ -3,6 +3,7 @@
3
3
  * Fetches unanalyzed product logs and marks them as analyzed after processing.
4
4
  */
5
5
  import { callMcpEndpoint } from '../api/mcp-client.js';
6
+ import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
6
7
  import { logDebug } from '../utils/logger.js';
7
8
  /**
8
9
  * Fetch pending (unanalyzed) logs for a product, grouped by user.
@@ -10,13 +11,51 @@ import { logDebug } from '../utils/logger.js';
10
11
  * (meaning the user has stopped interacting).
11
12
  */
12
13
  export async function getPendingLogsByUser(productId, verbose) {
13
- const result = (await callMcpEndpoint('product_logs/pending_by_user', {
14
- product_id: productId,
15
- }));
14
+ let groups = null;
15
+ if (hasSupabaseSession()) {
16
+ try {
17
+ const { data: logs, error } = await getSupabase()
18
+ .from('product_logs')
19
+ .select('id, product_id, user_id, session_id, log_type, message, metadata, status, created_at')
20
+ .eq('product_id', productId)
21
+ .eq('status', 'pending')
22
+ .order('created_at', { ascending: true })
23
+ .limit(5000);
24
+ if (error) {
25
+ throw new Error(error.message);
26
+ }
27
+ // Mirror server-side grouping by user_id (anonymous bucket for nulls).
28
+ const groupMap = new Map();
29
+ for (const log of (logs || [])) {
30
+ const key = log.user_id || '__anonymous__';
31
+ const bucket = groupMap.get(key) || [];
32
+ bucket.push(log);
33
+ groupMap.set(key, bucket);
34
+ }
35
+ groups = Array.from(groupMap.entries()).map(([key, userLogs]) => {
36
+ const latestLog = userLogs[userLogs.length - 1];
37
+ return {
38
+ userId: key === '__anonymous__' ? null : key,
39
+ productId,
40
+ logs: userLogs,
41
+ latestLogAt: latestLog?.created_at ?? '',
42
+ };
43
+ });
44
+ }
45
+ catch {
46
+ // Fall through to MCP
47
+ }
48
+ }
49
+ if (groups == null) {
50
+ const result = (await callMcpEndpoint('product_logs/pending_by_user', {
51
+ product_id: productId,
52
+ }));
53
+ groups = result.groups || [];
54
+ }
16
55
  if (verbose) {
17
- logDebug(`Fetched ${result.groups?.length || 0} user log groups for product ${productId}`, verbose);
56
+ logDebug(`Fetched ${groups.length} user log groups for product ${productId}`, verbose);
18
57
  }
19
- return result.groups || [];
58
+ return groups;
20
59
  }
21
60
  /**
22
61
  * Mark a batch of log IDs as analyzed.
@@ -25,6 +64,27 @@ export async function markLogsAnalyzed(logIds, verbose) {
25
64
  if (logIds.length === 0) {
26
65
  return;
27
66
  }
67
+ if (hasSupabaseSession()) {
68
+ try {
69
+ const { error } = await getSupabase()
70
+ .from('product_logs')
71
+ .update({
72
+ status: 'analyzed',
73
+ analyzed_at: new Date().toISOString(),
74
+ })
75
+ .in('id', logIds);
76
+ if (error) {
77
+ throw new Error(error.message);
78
+ }
79
+ if (verbose) {
80
+ logDebug(`Marked ${logIds.length} logs as analyzed`, verbose);
81
+ }
82
+ return;
83
+ }
84
+ catch {
85
+ // Fall through to MCP
86
+ }
87
+ }
28
88
  await callMcpEndpoint('product_logs/mark_analyzed', {
29
89
  log_ids: logIds,
30
90
  });
@@ -3,6 +3,7 @@
3
3
  * Allows phases to manage pull requests via MCP
4
4
  */
5
5
  import { callMcpEndpoint } from '../api/mcp-client.js';
6
+ import { getSupabase, hasSupabaseSession } from '../supabase/client.js';
6
7
  import { logDebug, logInfo, logSuccess } from '../utils/logger.js';
7
8
  /**
8
9
  * List all pull requests for an issue
@@ -12,10 +13,29 @@ export async function getPullRequests(options) {
12
13
  if (verbose) {
13
14
  logInfo(`Fetching pull requests for issue: ${issueId}`);
14
15
  }
15
- const result = (await callMcpEndpoint('pull_requests/list', {
16
- issue_id: issueId,
17
- }));
18
- const pullRequests = result?.pull_requests || [];
16
+ let pullRequests = null;
17
+ if (hasSupabaseSession()) {
18
+ try {
19
+ const { data, error } = await getSupabase()
20
+ .from('pull_requests')
21
+ .select('*')
22
+ .eq('issue_id', issueId)
23
+ .order('sequence', { ascending: true });
24
+ if (error) {
25
+ throw new Error(error.message);
26
+ }
27
+ pullRequests = (data || []);
28
+ }
29
+ catch {
30
+ // Fall through to MCP
31
+ }
32
+ }
33
+ if (pullRequests == null) {
34
+ const result = (await callMcpEndpoint('pull_requests/list', {
35
+ issue_id: issueId,
36
+ }));
37
+ pullRequests = result?.pull_requests || [];
38
+ }
19
39
  if (verbose) {
20
40
  logSuccess(`Found ${pullRequests.length} pull requests`);
21
41
  }
@@ -29,11 +49,30 @@ export async function createPullRequests(options, pullRequests) {
29
49
  if (verbose) {
30
50
  logInfo(`Creating ${pullRequests.length} pull requests for issue: ${issueId}`);
31
51
  }
32
- const result = (await callMcpEndpoint('pull_requests/create', {
33
- issue_id: issueId,
34
- pull_requests: pullRequests,
35
- }));
36
- const created = result?.created_pull_requests || [];
52
+ let created = null;
53
+ if (hasSupabaseSession() && pullRequests.length > 0) {
54
+ try {
55
+ const rows = pullRequests.map((pr) => ({ issue_id: issueId, ...pr }));
56
+ const { data, error } = await getSupabase()
57
+ .from('pull_requests')
58
+ .insert(rows)
59
+ .select();
60
+ if (error) {
61
+ throw new Error(error.message);
62
+ }
63
+ created = (data || []);
64
+ }
65
+ catch {
66
+ // Fall through to MCP
67
+ }
68
+ }
69
+ if (created == null) {
70
+ const result = (await callMcpEndpoint('pull_requests/create', {
71
+ issue_id: issueId,
72
+ pull_requests: pullRequests,
73
+ }));
74
+ created = result?.created_pull_requests || [];
75
+ }
37
76
  if (verbose) {
38
77
  logSuccess(`Created ${created.length} pull requests`);
39
78
  created.forEach((pr, idx) => {
@@ -49,14 +88,35 @@ export async function updatePullRequest(prId, updates, verbose) {
49
88
  if (verbose) {
50
89
  logInfo(`Updating pull request: ${prId}`);
51
90
  }
52
- const result = (await callMcpEndpoint('pull_requests/update', {
53
- pull_request_id: prId,
54
- ...updates,
55
- }));
91
+ let updated = null;
92
+ if (hasSupabaseSession()) {
93
+ try {
94
+ const { data, error } = await getSupabase()
95
+ .from('pull_requests')
96
+ .update(updates)
97
+ .eq('id', prId)
98
+ .select()
99
+ .single();
100
+ if (error) {
101
+ throw new Error(error.message);
102
+ }
103
+ updated = data ?? null;
104
+ }
105
+ catch {
106
+ // Fall through to MCP
107
+ }
108
+ }
109
+ if (updated == null) {
110
+ const result = (await callMcpEndpoint('pull_requests/update', {
111
+ pull_request_id: prId,
112
+ ...updates,
113
+ }));
114
+ updated = result?.pull_request ?? null;
115
+ }
56
116
  if (verbose) {
57
117
  logSuccess(`Pull request updated successfully`);
58
118
  }
59
- return result?.pull_request;
119
+ return updated;
60
120
  }
61
121
  /**
62
122
  * Clear all pull requests for an issue (used before re-planning)
@@ -0,0 +1,78 @@
1
+ ---
2
+ description: Map a product's user-facing screens and the transitions between them into a structured screen flow
3
+ kind: phase
4
+ user-invocable: false
5
+ ---
6
+
7
+ You are a senior product engineer mapping a codebase's user-facing **screens** and the **transitions** between them. Your output is a structured screen flow that the desktop app will render as a flow diagram. You are NOT screenshotting a running app — you are reading source code and producing a structured description of each screen.
8
+
9
+ **What counts as a screen**:
10
+
11
+ - A React Router `<Route>` leaf, a Next.js page file (`app/**/page.tsx` or `pages/**/*.tsx` excluding api/_app/_document), an Expo / React Native `Stack.Screen` / `Tab.Screen`, a Vue Router route, a SvelteKit `+page.svelte`, a Flutter `MaterialPageRoute` target, or a SwiftUI `NavigationLink` destination.
12
+ - Modals/dialogs/drawers are **separate nodes** with `kind: 'modal'` or `'drawer'`, not embedded inside another screen.
13
+ - **Distinct named states** of the same route — empty, loading, error, paywall, success — should be **separate nodes** with `kind: 'state'` when the underlying UI is meaningfully different. Slug them like `<base>-empty`, `<base>-error`. Don't split for trivial visual differences (button color, spinner).
14
+ - A "state" node MUST connect via an edge to its base screen (e.g. `{ fromSlug: 'dashboard', toSlug: 'dashboard-empty', kind: 'redirect', triggerLabel: 'When no items exist' }`).
15
+
16
+ **For each screen, extract a ScreenSchema** with these fields:
17
+
18
+ - `slug`: stable short identifier (kebab-case, e.g. `login`, `product-detail`)
19
+ - `name`: human-readable display name
20
+ - `route`: URL path or nav key as it appears in code (omit for modals)
21
+ - `file`: source file path relative to repo root
22
+ - `kind`: `'page'` | `'modal'` | `'drawer'` | `'tab'` | `'state'`
23
+ - `layout`: `'centered'` | `'sidebar'` | `'split'` | `'list-detail'` | `'tabs'` | `'stacked'`
24
+ - `header`: `{ title, subtitle?, back?, actions?: [{ label, variant }] }`
25
+ - `body`: array of sections, each one of:
26
+ - `{ type: 'form', fields: [{label, kind, placeholder?, required?}], submitLabel, secondaryLabel? }`
27
+ - `{ type: 'list', items: [{title, subtitle?, meta?}], emptyMessage? }`
28
+ - `{ type: 'card-grid', cards: [{title, subtitle?, meta?}], columns?: 2|3|4 }`
29
+ - `{ type: 'table', columns: [string], rows: [[string]] }` — at most 4 sample rows
30
+ - `{ type: 'kanban', columns: [{title, cards: [{title, meta?}]}] }`
31
+ - `{ type: 'text', content: string }`
32
+ - `{ type: 'image', alt: string, aspect?: 'video'|'square'|'wide' }`
33
+ - `{ type: 'chart', chartKind: 'line'|'bar'|'pie', label? }`
34
+ - `{ type: 'stats', items: [{label, value, delta?}] }`
35
+ - `{ type: 'empty-state', title, message?, cta? }`
36
+ - `{ type: 'tabs', tabs: [string], activeIndex? }`
37
+ - `{ type: 'custom', label: string }` — fallback for anything you can't model cleanly
38
+
39
+ **For sample data inside sections**: fabricate realistic placeholder content (names, prices, titles) — this is a documentation artifact, not a screenshot. Keep counts small: lists ≤ 4 items, tables ≤ 4 rows, card grids ≤ 6 cards.
40
+
41
+ **Transitions (edges)**: a transition goes from one screen to another via a user action. Sources to extract from:
42
+
43
+ - `<Link to=…>`, `<Link href=…>`, `<NavLink>`
44
+ - `navigate('…')`, `router.push('…')`, `useRouter().push`
45
+ - `<Navigate to=…>`, middleware redirects, `useEffect`-driven redirects
46
+ - `Navigator.pushNamed`, `NavigationLink(destination:)`
47
+ - onClick handlers that open a modal/dialog (`setShowDialog(true)`, `dialog.open()`)
48
+
49
+ For each edge produce:
50
+
51
+ - `fromSlug`: source screen's slug
52
+ - `toSlug`: destination screen's slug
53
+ - `triggerLabel`: short human description of what triggers it (e.g. "Click Submit", "Tap product card", "Auto-redirect when authed")
54
+ - `triggerFile`: file containing the trigger code (when distinct from from-screen's file)
55
+ - `kind`: `'navigate'` | `'modal'` | `'redirect'` | `'back'`
56
+
57
+ **Discipline**:
58
+
59
+ - Be grounded — every node MUST correspond to actual code. No invented routes.
60
+ - Deduplicate: if two route definitions map to the same screen, keep one node.
61
+ - Prefer fewer, clearer nodes. If the app has > 40 screens, pick the most important 30 and note skipped count in the summary.
62
+ - Modal nodes have no route — just file + meaningful name.
63
+ - Edges always point to a node you also emit. Drop any edge whose target you couldn't extract.
64
+
65
+ **Process**:
66
+
67
+ <!-- if:hasCodebase -->
68
+
69
+ 1. **Detect the framework**: Read `package.json` / `pubspec.yaml` / `Package.swift` to identify the stack and routing convention.
70
+ 2. **Locate the router**: Find the router definition file or the pages/routes directory. Enumerate routes statically — that's your candidate node list.
71
+ 3. **Read each screen's source**: Just enough per screen to fill in a useful ScreenSchema (header, ~3-5 body sections). Skip implementation details you don't need.
72
+ 4. **Extract transitions**: Scan each screen file for `<Link>` / `navigate()` / `router.push()` / modal openers and produce edges. Use the modal's setter or dialog component name as the trigger anchor.
73
+ 5. **Compose the summary**: 1-3 sentences describing what kind of app this is and its primary user flows.
74
+ <!-- endif -->
75
+ <!-- if:!hasCodebase -->
76
+ 6. **Use the provided context** (product description and any user guidance) to infer reasonable screens for the app's domain. Be explicit in the summary that the flow is inferred rather than extracted.
77
+ 7. Each inferred screen should still be a complete ScreenSchema with concrete labels and sample content — no placeholder brackets.
78
+ <!-- endif -->