@assistkick/create 1.0.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 (178) hide show
  1. package/dist/bin/create.d.ts +2 -0
  2. package/dist/bin/create.js +25 -0
  3. package/dist/bin/create.js.map +1 -0
  4. package/dist/src/scaffolder.d.ts +22 -0
  5. package/dist/src/scaffolder.js +120 -0
  6. package/dist/src/scaffolder.js.map +1 -0
  7. package/package.json +24 -0
  8. package/templates/product-system/.env.example +8 -0
  9. package/templates/product-system/CLAUDE.md +45 -0
  10. package/templates/product-system/package.json +32 -0
  11. package/templates/product-system/packages/backend/package.json +37 -0
  12. package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
  13. package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
  14. package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
  15. package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
  16. package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
  17. package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
  18. package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
  19. package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
  20. package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
  21. package/templates/product-system/packages/backend/src/server.ts +159 -0
  22. package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
  23. package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
  24. package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
  25. package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
  26. package/templates/product-system/packages/backend/src/services/init.ts +80 -0
  27. package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
  28. package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
  29. package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
  30. package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
  31. package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
  32. package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
  33. package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
  34. package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
  35. package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
  36. package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
  37. package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
  38. package/templates/product-system/packages/backend/tsconfig.json +22 -0
  39. package/templates/product-system/packages/frontend/index.html +13 -0
  40. package/templates/product-system/packages/frontend/package-lock.json +2666 -0
  41. package/templates/product-system/packages/frontend/package.json +30 -0
  42. package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
  43. package/templates/product-system/packages/frontend/src/App.tsx +29 -0
  44. package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
  45. package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
  46. package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
  47. package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
  48. package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
  49. package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
  50. package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
  51. package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
  52. package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
  53. package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
  54. package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
  55. package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
  56. package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
  57. package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
  58. package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
  59. package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
  60. package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
  61. package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
  62. package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
  63. package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
  64. package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
  65. package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
  66. package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
  67. package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
  68. package/templates/product-system/packages/frontend/src/main.tsx +12 -0
  69. package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
  70. package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
  71. package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
  72. package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
  73. package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
  74. package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
  75. package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
  76. package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
  77. package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
  78. package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
  79. package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
  80. package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
  81. package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
  82. package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
  83. package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
  84. package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
  85. package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
  86. package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
  87. package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
  88. package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
  89. package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
  90. package/templates/product-system/packages/frontend/tsconfig.json +21 -0
  91. package/templates/product-system/packages/frontend/vite.config.ts +20 -0
  92. package/templates/product-system/packages/shared/.env.example +3 -0
  93. package/templates/product-system/packages/shared/README.md +1 -0
  94. package/templates/product-system/packages/shared/db/migrate.ts +32 -0
  95. package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
  96. package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
  97. package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
  98. package/templates/product-system/packages/shared/db/schema.ts +137 -0
  99. package/templates/product-system/packages/shared/drizzle.config.js +14 -0
  100. package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
  101. package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
  102. package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
  103. package/templates/product-system/packages/shared/lib/constants.ts +327 -0
  104. package/templates/product-system/packages/shared/lib/db.ts +81 -0
  105. package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
  106. package/templates/product-system/packages/shared/lib/graph.ts +186 -0
  107. package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
  108. package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
  109. package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
  110. package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
  111. package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
  112. package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
  113. package/templates/product-system/packages/shared/lib/session.ts +152 -0
  114. package/templates/product-system/packages/shared/lib/validator.ts +117 -0
  115. package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
  116. package/templates/product-system/packages/shared/package.json +30 -0
  117. package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
  118. package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
  119. package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
  120. package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
  121. package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
  122. package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
  123. package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
  124. package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
  125. package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
  126. package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
  127. package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
  128. package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
  129. package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
  130. package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
  131. package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
  132. package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
  133. package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
  134. package/templates/product-system/packages/shared/tsconfig.json +24 -0
  135. package/templates/product-system/pnpm-workspace.yaml +2 -0
  136. package/templates/product-system/smoke_test.ts +219 -0
  137. package/templates/product-system/tests/coherence_review.test.ts +562 -0
  138. package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
  139. package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
  140. package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
  141. package/templates/product-system/tests/feature_kind.test.ts +139 -0
  142. package/templates/product-system/tests/gap_indicators.test.ts +199 -0
  143. package/templates/product-system/tests/graceful_init.test.ts +142 -0
  144. package/templates/product-system/tests/graph_legend.test.ts +314 -0
  145. package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
  146. package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
  147. package/templates/product-system/tests/kanban.test.ts +529 -0
  148. package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
  149. package/templates/product-system/tests/node_search.test.ts +340 -0
  150. package/templates/product-system/tests/node_sizing.test.ts +170 -0
  151. package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
  152. package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
  153. package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
  154. package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
  155. package/templates/product-system/tests/pipeline.test.ts +195 -0
  156. package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
  157. package/templates/product-system/tests/play_all.test.ts +296 -0
  158. package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
  159. package/templates/product-system/tests/relevance_search.test.ts +186 -0
  160. package/templates/product-system/tests/search_reorder.test.ts +88 -0
  161. package/templates/product-system/tests/serve_ui.test.ts +281 -0
  162. package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
  163. package/templates/product-system/tests/session_context_recall.test.ts +135 -0
  164. package/templates/product-system/tests/side_panel.test.ts +345 -0
  165. package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
  166. package/templates/product-system/tests/url_routing_test.ts +122 -0
  167. package/templates/product-system/tests/user_login.test.ts +150 -0
  168. package/templates/product-system/tests/user_registration.test.ts +205 -0
  169. package/templates/product-system/tests/web_terminal.test.ts +572 -0
  170. package/templates/product-system/tests/work_summary.test.ts +211 -0
  171. package/templates/product-system/tests/zoom_pan.test.ts +43 -0
  172. package/templates/product-system/tsconfig.json +24 -0
  173. package/templates/skills/product-bootstrap/SKILL.md +312 -0
  174. package/templates/skills/product-code-reviewer/SKILL.md +147 -0
  175. package/templates/skills/product-debugger/SKILL.md +206 -0
  176. package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
  177. package/templates/skills/product-developer/SKILL.md +182 -0
  178. package/templates/skills/product-interview/SKILL.md +220 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Tests for Edge Type Color Coding (feat_016).
3
+ * Uses node:test built-in runner.
4
+ * Tests that each relation type has a distinct color, edge labels
5
+ * map correctly, and the toggle controls label visibility.
6
+ */
7
+ import { describe, it } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { EDGE_COLORS, EDGE_LABELS } from '../packages/frontend/src/constants/graph.js';
10
+
11
+ const ALL_RELATION_TYPES = [
12
+ 'contains', 'depends_on', 'governed_by', 'constrained_by',
13
+ 'implemented_with', 'reads_writes', 'exposes', 'consumes',
14
+ 'performed_by', 'escalates_to', 'relates_to',
15
+ ];
16
+
17
+ const createRenderer = () => {
18
+ const renderer = {
19
+ edgeLabelsVisible: true,
20
+ toggleEdgeLabels() {
21
+ this.edgeLabelsVisible = !this.edgeLabelsVisible;
22
+ return this.edgeLabelsVisible;
23
+ },
24
+ buildNodes(graphData: any) {
25
+ const edgeCounts = new Map();
26
+ graphData.edges.forEach((e: any) => {
27
+ edgeCounts.set(e.from, (edgeCounts.get(e.from) || 0) + 1);
28
+ edgeCounts.set(e.to, (edgeCounts.get(e.to) || 0) + 1);
29
+ });
30
+ return graphData.nodes.map((n: any) => ({
31
+ ...n,
32
+ edgeCount: edgeCounts.get(n.id) || 0,
33
+ }));
34
+ },
35
+ buildLinks(graphData: any, nodes: any[]) {
36
+ const nodeIds = new Set(nodes.map((n: any) => n.id));
37
+ return graphData.edges
38
+ .filter((e: any) => nodeIds.has(e.from) && nodeIds.has(e.to))
39
+ .map((e: any) => ({ source: e.from, target: e.to, relation: e.relation }));
40
+ },
41
+ };
42
+ return renderer;
43
+ };
44
+
45
+ describe('Edge Type Color Coding', () => {
46
+ describe('EDGE_COLORS mapping', () => {
47
+ it('has a color entry for every relation type', () => {
48
+ for (const rel of ALL_RELATION_TYPES) {
49
+ assert.ok(EDGE_COLORS[rel], `missing color for relation: ${rel}`);
50
+ }
51
+ });
52
+
53
+ it('has all distinct colors (no two relations share a color)', () => {
54
+ const colors = Object.values(EDGE_COLORS);
55
+ const unique = new Set(colors);
56
+ assert.equal(unique.size, colors.length, 'all edge colors should be unique');
57
+ });
58
+
59
+ it('uses valid hex color strings', () => {
60
+ const hexPattern = /^#[0-9a-fA-F]{6}$/;
61
+ for (const [rel, color] of Object.entries(EDGE_COLORS)) {
62
+ assert.ok(hexPattern.test(color), `${rel} color "${color}" is not valid hex`);
63
+ }
64
+ });
65
+ });
66
+
67
+ describe('EDGE_LABELS mapping', () => {
68
+ it('has a label entry for every relation type', () => {
69
+ for (const rel of ALL_RELATION_TYPES) {
70
+ assert.ok(EDGE_LABELS[rel], `missing label for relation: ${rel}`);
71
+ }
72
+ });
73
+
74
+ it('produces human-readable labels (no underscores)', () => {
75
+ for (const [rel, label] of Object.entries(EDGE_LABELS)) {
76
+ assert.ok(!label.includes('_'), `label for ${rel} should not contain underscores: "${label}"`);
77
+ }
78
+ });
79
+ });
80
+
81
+ describe('toggleEdgeLabels', () => {
82
+ it('starts with edge labels visible by default', () => {
83
+ const renderer = createRenderer();
84
+ assert.equal(renderer.edgeLabelsVisible, true);
85
+ });
86
+
87
+ it('toggles visibility off and returns false', () => {
88
+ const renderer = createRenderer();
89
+ const result = renderer.toggleEdgeLabels();
90
+ assert.equal(result, false);
91
+ assert.equal(renderer.edgeLabelsVisible, false);
92
+ });
93
+
94
+ it('toggles back on and returns true', () => {
95
+ const renderer = createRenderer();
96
+ renderer.toggleEdgeLabels(); // off
97
+ const result = renderer.toggleEdgeLabels(); // on
98
+ assert.equal(result, true);
99
+ assert.equal(renderer.edgeLabelsVisible, true);
100
+ });
101
+ });
102
+
103
+ describe('buildLinks preserves relation type', () => {
104
+ it('includes relation field in built links', () => {
105
+ const renderer = createRenderer();
106
+ const graphData = {
107
+ nodes: [
108
+ { id: 'a', type: 'feature', name: 'A', completeness: 1, open_questions_count: 0 },
109
+ { id: 'b', type: 'component', name: 'B', completeness: 1, open_questions_count: 0 },
110
+ ],
111
+ edges: [
112
+ { from: 'a', to: 'b', relation: 'contains' },
113
+ ],
114
+ };
115
+
116
+ const nodes = renderer.buildNodes(graphData);
117
+ const links = renderer.buildLinks(graphData, nodes);
118
+
119
+ assert.equal(links.length, 1);
120
+ assert.equal(links[0].relation, 'contains');
121
+ assert.equal(links[0].source, 'a');
122
+ assert.equal(links[0].target, 'b');
123
+ });
124
+
125
+ it('preserves different relation types across multiple edges', () => {
126
+ const renderer = createRenderer();
127
+ const graphData = {
128
+ nodes: [
129
+ { id: 'a', type: 'feature', name: 'A', completeness: 1, open_questions_count: 0 },
130
+ { id: 'b', type: 'component', name: 'B', completeness: 1, open_questions_count: 0 },
131
+ { id: 'c', type: 'decision', name: 'C', completeness: 1, open_questions_count: 0 },
132
+ ],
133
+ edges: [
134
+ { from: 'a', to: 'b', relation: 'contains' },
135
+ { from: 'a', to: 'c', relation: 'governed_by' },
136
+ { from: 'b', to: 'c', relation: 'depends_on' },
137
+ ],
138
+ };
139
+
140
+ const nodes = renderer.buildNodes(graphData);
141
+ const links = renderer.buildLinks(graphData, nodes);
142
+
143
+ const relations = links.map(l => l.relation);
144
+ assert.deepEqual(relations.sort(), ['contains', 'depends_on', 'governed_by']);
145
+ });
146
+ });
147
+ });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Unit tests for emitToolUseEvents in claude-service.js.
3
+ */
4
+ import { describe, it } from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { emitToolUseEvents } from '../packages/shared/lib/claude-service.js';
7
+
8
+ describe('emitToolUseEvents', () => {
9
+ it('invokes callback for TodoWrite tool_use events', () => {
10
+ const calls = [];
11
+ const line = JSON.stringify({
12
+ type: 'assistant',
13
+ message: {
14
+ content: [{
15
+ type: 'tool_use',
16
+ name: 'TodoWrite',
17
+ input: {
18
+ todos: [
19
+ { content: 'Parse events', status: 'completed', activeForm: 'Parsing events' },
20
+ { content: 'Write tests', status: 'in_progress', activeForm: 'Writing tests' },
21
+ { content: 'Run tests', status: 'pending', activeForm: 'Running tests' },
22
+ ],
23
+ },
24
+ }],
25
+ },
26
+ });
27
+
28
+ emitToolUseEvents(line, (name, input) => calls.push({ name, input }));
29
+
30
+ assert.equal(calls.length, 1);
31
+ assert.equal(calls[0].name, 'TodoWrite');
32
+ assert.equal(calls[0].input.todos.length, 3);
33
+ assert.equal(calls[0].input.todos[0].status, 'completed');
34
+ });
35
+
36
+ it('invokes callback for multiple tool_use blocks in one event', () => {
37
+ const calls = [];
38
+ const line = JSON.stringify({
39
+ type: 'assistant',
40
+ message: {
41
+ content: [
42
+ { type: 'text', text: 'Some text' },
43
+ { type: 'tool_use', name: 'Read', input: { file_path: '/a.js' } },
44
+ { type: 'tool_use', name: 'TodoWrite', input: { todos: [{ content: 'X', status: 'pending', activeForm: 'X' }] } },
45
+ ],
46
+ },
47
+ });
48
+
49
+ emitToolUseEvents(line, (name, input) => calls.push({ name, input }));
50
+
51
+ assert.equal(calls.length, 2);
52
+ assert.equal(calls[0].name, 'Read');
53
+ assert.equal(calls[1].name, 'TodoWrite');
54
+ });
55
+
56
+ it('does nothing for non-assistant events', () => {
57
+ const calls = [];
58
+ const line = JSON.stringify({ type: 'result', result: 'done' });
59
+
60
+ emitToolUseEvents(line, (name, input) => calls.push({ name, input }));
61
+
62
+ assert.equal(calls.length, 0);
63
+ });
64
+
65
+ it('does nothing for invalid JSON', () => {
66
+ const calls = [];
67
+ emitToolUseEvents('not valid json', (name, input) => calls.push({ name, input }));
68
+ assert.equal(calls.length, 0);
69
+ });
70
+
71
+ it('handles missing name gracefully', () => {
72
+ const calls = [];
73
+ const line = JSON.stringify({
74
+ type: 'assistant',
75
+ message: {
76
+ content: [{ type: 'tool_use', input: { todos: [] } }],
77
+ },
78
+ });
79
+
80
+ emitToolUseEvents(line, (name, input) => calls.push({ name, input }));
81
+
82
+ assert.equal(calls.length, 1);
83
+ assert.equal(calls[0].name, 'unknown');
84
+ });
85
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Tests for feature kind field support (feat_027).
3
+ * Uses node:test built-in runner.
4
+ */
5
+ import { describe, it } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { VALID_FEATURE_KINDS } from '../packages/shared/lib/constants.js';
8
+
9
+ describe('Feature kind constants', () => {
10
+ it('defines exactly three valid kinds', () => {
11
+ assert.deepEqual(VALID_FEATURE_KINDS, ['new', 'improvement', 'bugfix']);
12
+ });
13
+ });
14
+
15
+ describe('Feature kind data transformations', () => {
16
+ it('defaults missing kind to "new" for backward compatibility', () => {
17
+ const nodes = [
18
+ { id: 'feat_001', type: 'feature', status: 'defined', name: 'Old Feature' },
19
+ { id: 'feat_002', type: 'feature', status: 'defined', name: 'New Feature', kind: 'new' },
20
+ { id: 'feat_003', type: 'feature', status: 'defined', name: 'Bug Fix', kind: 'bugfix' },
21
+ ];
22
+
23
+ const withKinds = nodes.map(n => ({
24
+ ...n,
25
+ kind: n.kind || 'new',
26
+ }));
27
+
28
+ assert.equal(withKinds[0].kind, 'new');
29
+ assert.equal(withKinds[1].kind, 'new');
30
+ assert.equal(withKinds[2].kind, 'bugfix');
31
+ });
32
+
33
+ it('validates kind values against allowed list', () => {
34
+ const isValidKind = (kind) => VALID_FEATURE_KINDS.includes(kind);
35
+
36
+ assert.ok(isValidKind('new'));
37
+ assert.ok(isValidKind('improvement'));
38
+ assert.ok(isValidKind('bugfix'));
39
+ assert.ok(!isValidKind('enhancement'));
40
+ assert.ok(!isValidKind(''));
41
+ assert.ok(!isValidKind(undefined));
42
+ });
43
+
44
+ it('only applies kind to feature nodes', () => {
45
+ const nodes = [
46
+ { id: 'feat_001', type: 'feature', name: 'A Feature' },
47
+ { id: 'comp_001', type: 'component', name: 'A Component' },
48
+ { id: 'dec_001', type: 'decision', name: 'A Decision' },
49
+ ];
50
+
51
+ const withKinds = nodes.map(n => ({
52
+ ...n,
53
+ ...(n.type === 'feature' ? { kind: n.kind || 'new' } : {}),
54
+ }));
55
+
56
+ assert.equal(withKinds[0].kind, 'new');
57
+ assert.equal(withKinds[1].kind, undefined);
58
+ assert.equal(withKinds[2].kind, undefined);
59
+ });
60
+ });
61
+
62
+ describe('Kanban card kind display logic', () => {
63
+ it('includes kind in card data from graph nodes', () => {
64
+ const graphNodes = [
65
+ { id: 'feat_001', type: 'feature', status: 'defined', name: 'Feature A', completeness: 1 },
66
+ { id: 'feat_002', type: 'feature', status: 'defined', name: 'Feature B', completeness: 0.8, kind: 'bugfix' },
67
+ { id: 'feat_003', type: 'feature', status: 'defined', name: 'Feature C', completeness: 0.5, kind: 'improvement' },
68
+ ];
69
+
70
+ const kanbanData = {
71
+ feat_001: { column: 'todo', rejection_count: 0, notes: [] },
72
+ feat_002: { column: 'todo', rejection_count: 1, notes: [] },
73
+ feat_003: { column: 'in_progress', rejection_count: 0, notes: [] },
74
+ };
75
+
76
+ const featureNodes = new Map();
77
+ graphNodes
78
+ .filter(n => n.type === 'feature' && n.status === 'defined')
79
+ .forEach(n => featureNodes.set(n.id, n));
80
+
81
+ const cards = Object.entries(kanbanData)
82
+ .filter(([id, entry]) => entry.column === 'todo' && featureNodes.has(id))
83
+ .map(([id, entry]) => ({
84
+ id,
85
+ name: featureNodes.get(id).name,
86
+ kind: featureNodes.get(id).kind || 'new',
87
+ rejectionCount: entry.rejection_count || 0,
88
+ }));
89
+
90
+ assert.equal(cards.length, 2);
91
+ assert.equal(cards[0].kind, 'new');
92
+ assert.equal(cards[1].kind, 'bugfix');
93
+ });
94
+
95
+ it('only shows kind badge for non-default kinds', () => {
96
+ const shouldShowBadge = (kind) => kind && kind !== 'new';
97
+
98
+ assert.ok(!shouldShowBadge('new'));
99
+ assert.ok(!shouldShowBadge(undefined));
100
+ assert.ok(shouldShowBadge('improvement'));
101
+ assert.ok(shouldShowBadge('bugfix'));
102
+ });
103
+ });
104
+
105
+ describe('Feature kind in frontmatter', () => {
106
+ it('builds correct frontmatter for feature with kind', () => {
107
+ const type = 'feature';
108
+ const kind = 'bugfix';
109
+ const frontmatter = {
110
+ id: 'feat_001',
111
+ type,
112
+ name: 'Fix login bug',
113
+ status: 'draft',
114
+ priority: 'high',
115
+ ...(type === 'feature' ? { kind } : {}),
116
+ created_at: '2026-01-01T00:00:00.000Z',
117
+ updated_at: '2026-01-01T00:00:00.000Z',
118
+ };
119
+
120
+ assert.equal(frontmatter.kind, 'bugfix');
121
+ });
122
+
123
+ it('does not add kind to non-feature frontmatter', () => {
124
+ const type = 'component';
125
+ const kind = 'new';
126
+ const frontmatter = {
127
+ id: 'comp_001',
128
+ type,
129
+ name: 'A Component',
130
+ status: 'draft',
131
+ priority: 'medium',
132
+ ...(type === 'feature' ? { kind } : {}),
133
+ created_at: '2026-01-01T00:00:00.000Z',
134
+ updated_at: '2026-01-01T00:00:00.000Z',
135
+ };
136
+
137
+ assert.equal(frontmatter.kind, undefined);
138
+ });
139
+ });
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Tests for Visual Gap Indicators (feat_013).
3
+ * Uses node:test built-in runner.
4
+ * Tests the data transformation and classification logic that drives
5
+ * pulsing rings and warning icons on nodes with open questions.
6
+ */
7
+ import { describe, it } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ const createRenderer = () => ({
10
+ nodeRadius(d: any) {
11
+ const base = 8;
12
+ const scale = Math.min(d.edgeCount * 1.5, 20);
13
+ return base + scale;
14
+ },
15
+ nodeStroke(d: any) {
16
+ if (d.completeness < 1 && d.open_questions_count > 0) return '#ff6b6b';
17
+ if (d.completeness < 1) return '#ffd43b';
18
+ return 'transparent';
19
+ },
20
+ nodeStrokeWidth(d: any) {
21
+ if (d.completeness < 1) return 3;
22
+ return 0;
23
+ },
24
+ hasGapIndicator(d: any) {
25
+ return d.open_questions_count > 0;
26
+ },
27
+ buildNodes(graphData: any) {
28
+ const edgeCounts = new Map();
29
+ graphData.edges.forEach((e: any) => {
30
+ edgeCounts.set(e.from, (edgeCounts.get(e.from) || 0) + 1);
31
+ edgeCounts.set(e.to, (edgeCounts.get(e.to) || 0) + 1);
32
+ });
33
+ return graphData.nodes.map((n: any) => ({
34
+ ...n,
35
+ edgeCount: edgeCounts.get(n.id) || 0,
36
+ }));
37
+ },
38
+ });
39
+
40
+ // Test node fixtures
41
+ const makeNode = (overrides = {}) => ({
42
+ id: 'feat_001',
43
+ type: 'feature',
44
+ name: 'Test Feature',
45
+ completeness: 1.0,
46
+ open_questions_count: 0,
47
+ edgeCount: 2,
48
+ ...overrides,
49
+ });
50
+
51
+ describe('Visual Gap Indicators', () => {
52
+
53
+ describe('hasGapIndicator', () => {
54
+ const renderer = createRenderer();
55
+
56
+ it('returns true for nodes with open questions', () => {
57
+ const node = makeNode({ open_questions_count: 3 });
58
+ assert.equal(renderer.hasGapIndicator(node), true);
59
+ });
60
+
61
+ it('returns true for nodes with 1 open question', () => {
62
+ const node = makeNode({ open_questions_count: 1 });
63
+ assert.equal(renderer.hasGapIndicator(node), true);
64
+ });
65
+
66
+ it('returns false for nodes with zero open questions', () => {
67
+ const node = makeNode({ open_questions_count: 0 });
68
+ assert.equal(renderer.hasGapIndicator(node), false);
69
+ });
70
+
71
+ it('returns false for complete nodes without open questions', () => {
72
+ const node = makeNode({ completeness: 1.0, open_questions_count: 0 });
73
+ assert.equal(renderer.hasGapIndicator(node), false);
74
+ });
75
+
76
+ it('returns true for complete nodes with open questions', () => {
77
+ const node = makeNode({ completeness: 1.0, open_questions_count: 2 });
78
+ assert.equal(renderer.hasGapIndicator(node), true);
79
+ });
80
+
81
+ it('returns false for incomplete nodes without open questions', () => {
82
+ const node = makeNode({ completeness: 0.5, open_questions_count: 0 });
83
+ assert.equal(renderer.hasGapIndicator(node), false);
84
+ });
85
+ });
86
+
87
+ describe('nodeStroke distinguishes gap types', () => {
88
+ const renderer = createRenderer();
89
+
90
+ it('returns red for incomplete nodes with open questions', () => {
91
+ const node = makeNode({ completeness: 0.5, open_questions_count: 2 });
92
+ assert.equal(renderer.nodeStroke(node), '#ff6b6b');
93
+ });
94
+
95
+ it('returns yellow for incomplete nodes without open questions', () => {
96
+ const node = makeNode({ completeness: 0.5, open_questions_count: 0 });
97
+ assert.equal(renderer.nodeStroke(node), '#ffd43b');
98
+ });
99
+
100
+ it('returns transparent for complete nodes', () => {
101
+ const node = makeNode({ completeness: 1.0, open_questions_count: 0 });
102
+ assert.equal(renderer.nodeStroke(node), 'transparent');
103
+ });
104
+ });
105
+
106
+ describe('nodeStrokeWidth', () => {
107
+ const renderer = createRenderer();
108
+
109
+ it('returns 3 for incomplete nodes', () => {
110
+ const node = makeNode({ completeness: 0.7 });
111
+ assert.equal(renderer.nodeStrokeWidth(node), 3);
112
+ });
113
+
114
+ it('returns 0 for complete nodes', () => {
115
+ const node = makeNode({ completeness: 1.0 });
116
+ assert.equal(renderer.nodeStrokeWidth(node), 0);
117
+ });
118
+ });
119
+
120
+ describe('gap node filtering from graph data', () => {
121
+ const renderer = createRenderer();
122
+
123
+ it('filters nodes with open questions for gap indicators', () => {
124
+ const graphData = {
125
+ nodes: [
126
+ { id: 'feat_001', type: 'feature', name: 'A', completeness: 1.0, open_questions_count: 0 },
127
+ { id: 'feat_002', type: 'feature', name: 'B', completeness: 0.5, open_questions_count: 2 },
128
+ { id: 'comp_001', type: 'component', name: 'C', completeness: 0.8, open_questions_count: 0 },
129
+ { id: 'dec_001', type: 'decision', name: 'D', completeness: 0.3, open_questions_count: 1 },
130
+ ],
131
+ edges: [],
132
+ };
133
+
134
+ const nodes = renderer.buildNodes(graphData);
135
+ const gapNodes = nodes.filter(d => renderer.hasGapIndicator(d));
136
+
137
+ assert.equal(gapNodes.length, 2);
138
+ assert.deepEqual(
139
+ gapNodes.map(n => n.id).sort(),
140
+ ['dec_001', 'feat_002']
141
+ );
142
+ });
143
+
144
+ it('returns empty array when no nodes have open questions', () => {
145
+ const graphData = {
146
+ nodes: [
147
+ { id: 'feat_001', type: 'feature', name: 'A', completeness: 0.5, open_questions_count: 0 },
148
+ { id: 'feat_002', type: 'feature', name: 'B', completeness: 1.0, open_questions_count: 0 },
149
+ ],
150
+ edges: [],
151
+ };
152
+
153
+ const nodes = renderer.buildNodes(graphData);
154
+ const gapNodes = nodes.filter(d => renderer.hasGapIndicator(d));
155
+
156
+ assert.equal(gapNodes.length, 0);
157
+ });
158
+
159
+ it('includes all nodes when all have open questions', () => {
160
+ const graphData = {
161
+ nodes: [
162
+ { id: 'feat_001', type: 'feature', name: 'A', completeness: 0.5, open_questions_count: 1 },
163
+ { id: 'feat_002', type: 'feature', name: 'B', completeness: 0.8, open_questions_count: 3 },
164
+ ],
165
+ edges: [],
166
+ };
167
+
168
+ const nodes = renderer.buildNodes(graphData);
169
+ const gapNodes = nodes.filter(d => renderer.hasGapIndicator(d));
170
+
171
+ assert.equal(gapNodes.length, 2);
172
+ });
173
+ });
174
+
175
+ describe('visual distinction between gap types', () => {
176
+ const renderer = createRenderer();
177
+
178
+ it('incomplete-only nodes get yellow border but no gap indicator', () => {
179
+ const node = makeNode({ completeness: 0.6, open_questions_count: 0 });
180
+ assert.equal(renderer.nodeStroke(node), '#ffd43b');
181
+ assert.equal(renderer.nodeStrokeWidth(node), 3);
182
+ assert.equal(renderer.hasGapIndicator(node), false);
183
+ });
184
+
185
+ it('incomplete nodes with open questions get red border AND gap indicator', () => {
186
+ const node = makeNode({ completeness: 0.6, open_questions_count: 2 });
187
+ assert.equal(renderer.nodeStroke(node), '#ff6b6b');
188
+ assert.equal(renderer.nodeStrokeWidth(node), 3);
189
+ assert.equal(renderer.hasGapIndicator(node), true);
190
+ });
191
+
192
+ it('complete nodes with open questions get gap indicator but transparent border', () => {
193
+ const node = makeNode({ completeness: 1.0, open_questions_count: 1 });
194
+ assert.equal(renderer.nodeStroke(node), 'transparent');
195
+ assert.equal(renderer.nodeStrokeWidth(node), 0);
196
+ assert.equal(renderer.hasGapIndicator(node), true);
197
+ });
198
+ });
199
+ });