@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,285 @@
1
+ /**
2
+ * Tests for Node Type Toggle Filters (feat_029).
3
+ * Uses node:test built-in runner.
4
+ * Tests the filtering logic that hides/shows nodes by type using toggle chips.
5
+ */
6
+ import { describe, it } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+
9
+ // Minimal renderer mock replicating buildNodes and buildLinks with hiddenTypes
10
+ const createRenderer = (hiddenTypes = new Set()) => {
11
+ const renderer = {
12
+ hideDefinedActive: false,
13
+ hiddenTypes: new Set(hiddenTypes),
14
+
15
+ buildNodes: function (graphData) {
16
+ const edgeCounts = new Map();
17
+ graphData.edges.forEach(e => {
18
+ edgeCounts.set(e.from, (edgeCounts.get(e.from) || 0) + 1);
19
+ edgeCounts.set(e.to, (edgeCounts.get(e.to) || 0) + 1);
20
+ });
21
+
22
+ let nodes = graphData.nodes.map(n => ({
23
+ ...n,
24
+ edgeCount: edgeCounts.get(n.id) || 0,
25
+ }));
26
+
27
+ if (this.hideDefinedActive) {
28
+ nodes = nodes.filter(n => !(n.status === 'defined' && n.completeness === 1));
29
+ }
30
+
31
+ if (this.hiddenTypes.size > 0) {
32
+ nodes = nodes.filter(n => !this.hiddenTypes.has(n.type));
33
+ }
34
+
35
+ return nodes;
36
+ },
37
+
38
+ buildLinks: function (graphData, nodes) {
39
+ const nodeIds = new Set(nodes.map(n => n.id));
40
+ return graphData.edges
41
+ .filter(e => nodeIds.has(e.from) && nodeIds.has(e.to))
42
+ .map(e => ({
43
+ source: e.from,
44
+ target: e.to,
45
+ relation: e.relation,
46
+ }));
47
+ },
48
+
49
+ setHiddenTypes: function (hiddenTypes) {
50
+ this.hiddenTypes = new Set(hiddenTypes);
51
+ },
52
+ };
53
+ return renderer;
54
+ };
55
+
56
+ // Test fixtures
57
+ const makeGraphData = () => ({
58
+ nodes: [
59
+ { id: 'feat_001', type: 'feature', name: 'Feature A', status: 'defined', completeness: 1, open_questions_count: 0 },
60
+ { id: 'feat_002', type: 'feature', name: 'Feature B', status: 'draft', completeness: 0.3, open_questions_count: 1 },
61
+ { id: 'comp_001', type: 'component', name: 'Component A', status: 'defined', completeness: 1, open_questions_count: 0 },
62
+ { id: 'dec_001', type: 'decision', name: 'Decision A', status: 'defined', completeness: 1, open_questions_count: 0 },
63
+ { id: 'tech_001', type: 'tech_choice', name: 'Tech Choice A', status: 'defined', completeness: 1, open_questions_count: 0 },
64
+ { id: 'data_001', type: 'data_entity', name: 'Data Entity A', status: 'defined', completeness: 1, open_questions_count: 0 },
65
+ ],
66
+ edges: [
67
+ { from: 'feat_001', to: 'comp_001', relation: 'contains' },
68
+ { from: 'feat_002', to: 'dec_001', relation: 'depends_on' },
69
+ { from: 'feat_001', to: 'dec_001', relation: 'governed_by' },
70
+ { from: 'comp_001', to: 'tech_001', relation: 'implemented_with' },
71
+ { from: 'dec_001', to: 'data_001', relation: 'reads_writes' },
72
+ ],
73
+ });
74
+
75
+ describe('Node Type Toggle Filters', () => {
76
+
77
+ describe('all toggles on (default)', () => {
78
+ it('returns all nodes when no types are hidden', () => {
79
+ const renderer = createRenderer();
80
+ const graphData = makeGraphData();
81
+ const nodes = renderer.buildNodes(graphData);
82
+ assert.equal(nodes.length, 6);
83
+ });
84
+
85
+ it('returns all edges when no types are hidden', () => {
86
+ const renderer = createRenderer();
87
+ const graphData = makeGraphData();
88
+ const nodes = renderer.buildNodes(graphData);
89
+ const links = renderer.buildLinks(graphData, nodes);
90
+ assert.equal(links.length, 5);
91
+ });
92
+ });
93
+
94
+ describe('hiding a single type', () => {
95
+ it('hides all nodes of the toggled-off type', () => {
96
+ const renderer = createRenderer(new Set(['feature']));
97
+ const graphData = makeGraphData();
98
+ const nodes = renderer.buildNodes(graphData);
99
+
100
+ assert.equal(nodes.length, 4);
101
+ const hasFeature = nodes.some(n => n.type === 'feature');
102
+ assert.equal(hasFeature, false, 'No feature nodes should remain');
103
+ });
104
+
105
+ it('hides edges connected to hidden nodes', () => {
106
+ const renderer = createRenderer(new Set(['feature']));
107
+ const graphData = makeGraphData();
108
+ const nodes = renderer.buildNodes(graphData);
109
+ const links = renderer.buildLinks(graphData, nodes);
110
+
111
+ // feat_001->comp_001, feat_002->dec_001, feat_001->dec_001 should all be removed
112
+ // Only comp_001->tech_001 and dec_001->data_001 remain
113
+ assert.equal(links.length, 2);
114
+ const sources = links.map(l => l.source).sort();
115
+ assert.deepEqual(sources, ['comp_001', 'dec_001']);
116
+ });
117
+
118
+ it('hides decision type nodes and their edges', () => {
119
+ const renderer = createRenderer(new Set(['decision']));
120
+ const graphData = makeGraphData();
121
+ const nodes = renderer.buildNodes(graphData);
122
+ const links = renderer.buildLinks(graphData, nodes);
123
+
124
+ const hasDecision = nodes.some(n => n.type === 'decision');
125
+ assert.equal(hasDecision, false);
126
+
127
+ // Edges referencing dec_001 should be gone
128
+ const hasDecisionEdge = links.some(l => l.source === 'dec_001' || l.target === 'dec_001');
129
+ assert.equal(hasDecisionEdge, false);
130
+ });
131
+ });
132
+
133
+ describe('hiding multiple types', () => {
134
+ it('hides nodes of all toggled-off types', () => {
135
+ const renderer = createRenderer(new Set(['feature', 'component']));
136
+ const graphData = makeGraphData();
137
+ const nodes = renderer.buildNodes(graphData);
138
+
139
+ assert.equal(nodes.length, 3);
140
+ const types = new Set(nodes.map(n => n.type));
141
+ assert.equal(types.has('feature'), false);
142
+ assert.equal(types.has('component'), false);
143
+ assert.equal(types.has('decision'), true);
144
+ assert.equal(types.has('tech_choice'), true);
145
+ assert.equal(types.has('data_entity'), true);
146
+ });
147
+
148
+ it('only keeps edges between remaining visible nodes', () => {
149
+ const renderer = createRenderer(new Set(['feature', 'component']));
150
+ const graphData = makeGraphData();
151
+ const nodes = renderer.buildNodes(graphData);
152
+ const links = renderer.buildLinks(graphData, nodes);
153
+
154
+ // Only dec_001->data_001 should remain
155
+ assert.equal(links.length, 1);
156
+ assert.equal(links[0].source, 'dec_001');
157
+ assert.equal(links[0].target, 'data_001');
158
+ });
159
+ });
160
+
161
+ describe('hiding all types', () => {
162
+ it('returns no nodes when all types are hidden', () => {
163
+ const allTypes = new Set(['feature', 'component', 'decision', 'tech_choice', 'data_entity']);
164
+ const renderer = createRenderer(allTypes);
165
+ const graphData = makeGraphData();
166
+ const nodes = renderer.buildNodes(graphData);
167
+ assert.equal(nodes.length, 0);
168
+ });
169
+
170
+ it('returns no edges when all types are hidden', () => {
171
+ const allTypes = new Set(['feature', 'component', 'decision', 'tech_choice', 'data_entity']);
172
+ const renderer = createRenderer(allTypes);
173
+ const graphData = makeGraphData();
174
+ const nodes = renderer.buildNodes(graphData);
175
+ const links = renderer.buildLinks(graphData, nodes);
176
+ assert.equal(links.length, 0);
177
+ });
178
+ });
179
+
180
+ describe('setHiddenTypes method', () => {
181
+ it('updates the hidden types set', () => {
182
+ const renderer = createRenderer();
183
+ renderer.setHiddenTypes(new Set(['feature']));
184
+ assert.equal(renderer.hiddenTypes.has('feature'), true);
185
+ assert.equal(renderer.hiddenTypes.size, 1);
186
+ });
187
+
188
+ it('creates a defensive copy of the input set', () => {
189
+ const renderer = createRenderer();
190
+ const input = new Set(['feature']);
191
+ renderer.setHiddenTypes(input);
192
+ input.add('component');
193
+ assert.equal(renderer.hiddenTypes.has('component'), false);
194
+ });
195
+
196
+ it('changing hidden types changes buildNodes output', () => {
197
+ const renderer = createRenderer();
198
+ const graphData = makeGraphData();
199
+
200
+ const nodesBefore = renderer.buildNodes(graphData);
201
+ assert.equal(nodesBefore.length, 6);
202
+
203
+ renderer.setHiddenTypes(new Set(['feature']));
204
+ const nodesAfter = renderer.buildNodes(graphData);
205
+ assert.equal(nodesAfter.length, 4);
206
+ });
207
+ });
208
+
209
+ describe('interaction with hideDefinedActive filter', () => {
210
+ it('both filters can be active simultaneously', () => {
211
+ const renderer = createRenderer(new Set(['tech_choice']));
212
+ renderer.hideDefinedActive = true;
213
+ const graphData = makeGraphData();
214
+ const nodes = renderer.buildNodes(graphData);
215
+
216
+ // hideDefinedActive removes: feat_001, comp_001, dec_001, data_001 (all defined, completeness=1)
217
+ // hiddenTypes removes: tech_001 (tech_choice)
218
+ // Remaining: feat_002 only
219
+ assert.equal(nodes.length, 1);
220
+ assert.equal(nodes[0].id, 'feat_002');
221
+ });
222
+ });
223
+
224
+ describe('edge count preservation', () => {
225
+ it('calculates edge counts from full graph before filtering', () => {
226
+ const renderer = createRenderer(new Set(['component']));
227
+ const graphData = makeGraphData();
228
+ const nodes = renderer.buildNodes(graphData);
229
+
230
+ // feat_001 has 2 edges in the full graph (feat_001->comp_001, feat_001->dec_001)
231
+ const feat001 = nodes.find(n => n.id === 'feat_001');
232
+ assert.equal(feat001.edgeCount, 2);
233
+ });
234
+ });
235
+ });
236
+
237
+ describe('GraphLegend type toggle state', () => {
238
+ // Test the hiddenTypes tracking logic in isolation
239
+ const createLegendState = () => ({
240
+ hiddenTypes: new Set(),
241
+ toggle: function (type) {
242
+ if (this.hiddenTypes.has(type)) {
243
+ this.hiddenTypes.delete(type);
244
+ } else {
245
+ this.hiddenTypes.add(type);
246
+ }
247
+ },
248
+ });
249
+
250
+ it('starts with no hidden types (all on)', () => {
251
+ const legend = createLegendState();
252
+ assert.equal(legend.hiddenTypes.size, 0);
253
+ });
254
+
255
+ it('toggling a type adds it to hidden set', () => {
256
+ const legend = createLegendState();
257
+ legend.toggle('feature');
258
+ assert.equal(legend.hiddenTypes.has('feature'), true);
259
+ });
260
+
261
+ it('toggling a type twice removes it from hidden set', () => {
262
+ const legend = createLegendState();
263
+ legend.toggle('feature');
264
+ legend.toggle('feature');
265
+ assert.equal(legend.hiddenTypes.has('feature'), false);
266
+ });
267
+
268
+ it('can hide multiple types independently', () => {
269
+ const legend = createLegendState();
270
+ legend.toggle('feature');
271
+ legend.toggle('decision');
272
+ assert.equal(legend.hiddenTypes.size, 2);
273
+ assert.equal(legend.hiddenTypes.has('feature'), true);
274
+ assert.equal(legend.hiddenTypes.has('decision'), true);
275
+ });
276
+
277
+ it('toggling one type does not affect others', () => {
278
+ const legend = createLegendState();
279
+ legend.toggle('feature');
280
+ legend.toggle('decision');
281
+ legend.toggle('feature'); // re-enable feature
282
+ assert.equal(legend.hiddenTypes.has('feature'), false);
283
+ assert.equal(legend.hiddenTypes.has('decision'), true);
284
+ });
285
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Tests for Node Type Visual Encoding (feat_015).
3
+ * Uses node:test built-in runner.
4
+ * Tests shape mapping completeness, path generation, and consistency.
5
+ */
6
+ import { describe, it } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+
9
+ import { NODE_COLORS, TYPE_LABELS, NODE_SHAPES, nodeShapePath } from '../packages/frontend/src/constants/graph.js';
10
+
11
+ describe('Node Type Visual Encoding', () => {
12
+
13
+ describe('NODE_SHAPES completeness', () => {
14
+ it('should have a shape for every node type in NODE_COLORS', () => {
15
+ for (const type of Object.keys(NODE_COLORS)) {
16
+ assert.ok(NODE_SHAPES[type], `NODE_SHAPES should have shape for ${type}`);
17
+ }
18
+ });
19
+
20
+ it('should have exactly the same types as NODE_COLORS', () => {
21
+ assert.equal(
22
+ Object.keys(NODE_SHAPES).length,
23
+ Object.keys(NODE_COLORS).length,
24
+ 'NODE_SHAPES and NODE_COLORS should have the same number of entries'
25
+ );
26
+ });
27
+
28
+ it('should have a shape for every node type in TYPE_LABELS', () => {
29
+ for (const type of Object.keys(TYPE_LABELS)) {
30
+ assert.ok(NODE_SHAPES[type], `NODE_SHAPES should have shape for ${type}`);
31
+ }
32
+ });
33
+ });
34
+
35
+ describe('NODE_SHAPES uniqueness', () => {
36
+ it('should assign unique shapes to each node type', () => {
37
+ const shapes = Object.values(NODE_SHAPES);
38
+ const uniqueShapes = new Set(shapes);
39
+ assert.equal(
40
+ uniqueShapes.size,
41
+ shapes.length,
42
+ `All ${shapes.length} node types should have unique shapes, but only ${uniqueShapes.size} are unique`
43
+ );
44
+ });
45
+ });
46
+
47
+ describe('nodeShapePath', () => {
48
+ it('should return a valid SVG path string for every defined shape', () => {
49
+ const allShapes = new Set(Object.values(NODE_SHAPES));
50
+ for (const shape of allShapes) {
51
+ const path = nodeShapePath(shape, 10);
52
+ assert.ok(typeof path === 'string', `Path for ${shape} should be a string`);
53
+ assert.ok(path.length > 0, `Path for ${shape} should not be empty`);
54
+ assert.ok(path.startsWith('M'), `Path for ${shape} should start with M command`);
55
+ assert.ok(path.includes('Z'), `Path for ${shape} should be closed (contain Z)`);
56
+ }
57
+ });
58
+
59
+ it('should return a path for unknown shapes (fallback to circle)', () => {
60
+ const path = nodeShapePath('unknown_shape', 10);
61
+ assert.ok(typeof path === 'string');
62
+ assert.ok(path.startsWith('M'));
63
+ });
64
+
65
+ it('should scale paths based on radius parameter', () => {
66
+ const smallPath = nodeShapePath('square', 5);
67
+ const largePath = nodeShapePath('square', 20);
68
+ assert.notEqual(smallPath, largePath, 'Different radii should produce different paths');
69
+ });
70
+
71
+ it('should handle radius of 0 without error', () => {
72
+ const allShapes = new Set(Object.values(NODE_SHAPES));
73
+ for (const shape of allShapes) {
74
+ assert.doesNotThrow(() => nodeShapePath(shape, 0), `Shape ${shape} should not throw with radius 0`);
75
+ }
76
+ });
77
+
78
+ it('should produce centered paths (contain negative coordinates)', () => {
79
+ // All shapes should be centered at (0,0), so they should have negative values
80
+ const shapesWithNeg = ['square', 'diamond', 'hexagon', 'circle', 'pentagon', 'star', 'octagon'];
81
+ for (const shape of shapesWithNeg) {
82
+ const path = nodeShapePath(shape, 10);
83
+ assert.ok(path.includes('-'), `Path for ${shape} should contain negative coordinates (centered at 0,0)`);
84
+ }
85
+ });
86
+ });
87
+
88
+ describe('shape-color-label consistency', () => {
89
+ it('every node type should have a color, shape, and label', () => {
90
+ const allTypes = [
91
+ 'feature', 'component', 'data_entity', 'decision', 'tech_choice',
92
+ 'non_functional_requirement', 'design_token', 'design_pattern',
93
+ 'user_role', 'flow', 'assumption', 'open_question',
94
+ ];
95
+
96
+ for (const type of allTypes) {
97
+ assert.ok(NODE_COLORS[type], `${type} should have a color`);
98
+ assert.ok(NODE_SHAPES[type], `${type} should have a shape`);
99
+ assert.ok(TYPE_LABELS[type], `${type} should have a label`);
100
+ }
101
+ });
102
+ });
103
+ });
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Unit tests for pipeline state persistence logic (feat_054).
3
+ * Tests data transformation and serialization without requiring Drizzle/Turso.
4
+ */
5
+ import { describe, it } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+
8
+ describe('pipeline-state-store data transformations', () => {
9
+ it('save serializes tasks and toolCalls to JSON', () => {
10
+ const state = {
11
+ status: 'developing',
12
+ cycle: 1,
13
+ tasks: {
14
+ completed: 2,
15
+ total: 5,
16
+ items: [
17
+ { name: 'Task A', status: 'completed' },
18
+ { name: 'Task B', status: 'completed' },
19
+ { name: 'Task C', status: 'in_progress' },
20
+ { name: 'Task D', status: 'pending' },
21
+ { name: 'Task E', status: 'pending' },
22
+ ],
23
+ },
24
+ toolCalls: { total: 10, write: 2, edit: 3, read: 4, bash: 1 },
25
+ error: null,
26
+ };
27
+
28
+ const tasksJson = state.tasks ? JSON.stringify(state.tasks.items || []) : null;
29
+ const toolCallsJson = state.toolCalls ? JSON.stringify(state.toolCalls) : null;
30
+
31
+ assert.equal(JSON.parse(tasksJson).length, 5);
32
+ assert.equal(JSON.parse(toolCallsJson).total, 10);
33
+ });
34
+
35
+ it('load deserializes tasks and computes completed count', () => {
36
+ const tasksJson = JSON.stringify([
37
+ { name: 'Task A', status: 'completed' },
38
+ { name: 'Task B', status: 'completed' },
39
+ { name: 'Task C', status: 'in_progress' },
40
+ ]);
41
+
42
+ const tasks = JSON.parse(tasksJson);
43
+ const result = {
44
+ completed: tasks.filter(t => t.status === 'completed').length,
45
+ total: tasks.length,
46
+ items: tasks,
47
+ };
48
+
49
+ assert.equal(result.completed, 2);
50
+ assert.equal(result.total, 3);
51
+ assert.equal(result.items.length, 3);
52
+ });
53
+
54
+ it('load returns undefined tasks when tasksJson is null', () => {
55
+ const tasksJson = null;
56
+ const tasks = tasksJson ? JSON.parse(tasksJson) : [];
57
+ const result = tasks.length > 0 ? {
58
+ completed: tasks.filter(t => t.status === 'completed').length,
59
+ total: tasks.length,
60
+ items: tasks,
61
+ } : undefined;
62
+
63
+ assert.equal(result, undefined);
64
+ });
65
+
66
+ it('load returns undefined toolCalls when toolCallsJson is null', () => {
67
+ const toolCallsJson = null;
68
+ const toolCalls = toolCallsJson ? JSON.parse(toolCallsJson) : null;
69
+ const result = toolCalls && toolCalls.total > 0 ? toolCalls : undefined;
70
+
71
+ assert.equal(result, undefined);
72
+ });
73
+
74
+ it('load returns undefined toolCalls when total is 0', () => {
75
+ const toolCallsJson = JSON.stringify({ total: 0, write: 0, edit: 0, read: 0, bash: 0 });
76
+ const toolCalls = JSON.parse(toolCallsJson);
77
+ const result = toolCalls && toolCalls.total > 0 ? toolCalls : undefined;
78
+
79
+ assert.equal(result, undefined);
80
+ });
81
+
82
+ it('save handles null tasks gracefully', () => {
83
+ const state = {
84
+ status: 'completed',
85
+ cycle: 2,
86
+ tasks: null,
87
+ toolCalls: null,
88
+ error: null,
89
+ };
90
+
91
+ const tasksJson = state.tasks ? JSON.stringify(state.tasks.items || []) : null;
92
+ const toolCallsJson = state.toolCalls ? JSON.stringify(state.toolCalls) : null;
93
+
94
+ assert.equal(tasksJson, null);
95
+ assert.equal(toolCallsJson, null);
96
+ });
97
+
98
+ it('save includes error when present', () => {
99
+ const state = {
100
+ status: 'failed',
101
+ cycle: 1,
102
+ error: 'Developer failed: process exited with code 1',
103
+ };
104
+
105
+ const row = {
106
+ featureId: 'feat_010',
107
+ status: state.status,
108
+ cycle: state.cycle || 1,
109
+ tasksJson: null,
110
+ toolCallsJson: null,
111
+ error: state.error || null,
112
+ updatedAt: new Date().toISOString(),
113
+ };
114
+
115
+ assert.equal(row.error, 'Developer failed: process exited with code 1');
116
+ assert.equal(row.status, 'failed');
117
+ });
118
+ });
119
+
120
+ describe('pipeline-state-store markInterrupted logic', () => {
121
+ it('identifies active statuses correctly', () => {
122
+ const activeStatuses = ['developing', 'reviewing', 'fixing_review', 'fixing_merge', 'merging', 'debugging', 'creating_worktree'];
123
+ const terminalStatuses = ['completed', 'failed', 'blocked', 'interrupted', 'idle'];
124
+
125
+ for (const status of activeStatuses) {
126
+ assert.ok(activeStatuses.includes(status), `${status} should be active`);
127
+ }
128
+ for (const status of terminalStatuses) {
129
+ assert.ok(!activeStatuses.includes(status), `${status} should not be active`);
130
+ }
131
+ });
132
+
133
+ it('markInterrupted transforms active rows to interrupted', () => {
134
+ const rows = [
135
+ { featureId: 'feat_001', status: 'developing' },
136
+ { featureId: 'feat_002', status: 'completed' },
137
+ { featureId: 'feat_003', status: 'reviewing' },
138
+ { featureId: 'feat_004', status: 'failed' },
139
+ { featureId: 'feat_005', status: 'merging' },
140
+ ];
141
+
142
+ const activeStatuses = ['developing', 'reviewing', 'fixing_review', 'fixing_merge', 'merging', 'debugging', 'creating_worktree'];
143
+ const toInterrupt = rows.filter(r => activeStatuses.includes(r.status));
144
+
145
+ assert.equal(toInterrupt.length, 3);
146
+ assert.deepEqual(toInterrupt.map(r => r.featureId), ['feat_001', 'feat_003', 'feat_005']);
147
+ });
148
+ });
149
+
150
+ describe('pipeline getStatus with DB fallback', () => {
151
+ it('returns DB state when in-memory state is absent', () => {
152
+ const dbState = {
153
+ status: 'interrupted',
154
+ cycle: 2,
155
+ feature_id: 'feat_010',
156
+ error: null,
157
+ tasks: {
158
+ completed: 3,
159
+ total: 5,
160
+ items: [
161
+ { name: 'Task A', status: 'completed' },
162
+ { name: 'Task B', status: 'completed' },
163
+ { name: 'Task C', status: 'completed' },
164
+ { name: 'Task D', status: 'in_progress' },
165
+ { name: 'Task E', status: 'pending' },
166
+ ],
167
+ },
168
+ toolCalls: { total: 15, write: 3, edit: 5, read: 5, bash: 2 },
169
+ };
170
+
171
+ const pipelines = new Map();
172
+ const state = pipelines.get('feat_010');
173
+ assert.equal(state, undefined);
174
+
175
+ // Simulates the fallback
176
+ const result = state ? state : dbState || { status: 'idle', feature_id: 'feat_010' };
177
+ assert.equal(result.status, 'interrupted');
178
+ assert.equal(result.tasks.completed, 3);
179
+ assert.equal(result.toolCalls.total, 15);
180
+ });
181
+
182
+ it('returns idle when neither in-memory nor DB has state', () => {
183
+ const pipelines = new Map();
184
+ const state = pipelines.get('feat_999');
185
+ const dbState = null;
186
+
187
+ const result = state ? state : dbState || { status: 'idle', feature_id: 'feat_999' };
188
+ assert.equal(result.status, 'idle');
189
+ });
190
+
191
+ it('prefers in-memory state over DB state', () => {
192
+ const pipelines = new Map();
193
+ pipelines.set('feat_010', {
194
+ status: 'developing',
195
+ cycle: 1,
196
+ tasks: { completed: 1, total: 3, items: [] },
197
+ });
198
+
199
+ const state = pipelines.get('feat_010');
200
+ assert.equal(state.status, 'developing');
201
+ });
202
+ });
203
+
204
+ describe('pipeline persistState integration with pipeline.js', () => {
205
+ it('persistState is called at each status transition', async () => {
206
+ const persistCalls = [];
207
+ const mockStateStore = {
208
+ save: async (featureId, state) => {
209
+ persistCalls.push({ featureId, status: state.status });
210
+ },
211
+ };
212
+
213
+ // Simulate the persistState helper from pipeline.js
214
+ const pipelines = new Map();
215
+ const persistState = (featureId) => {
216
+ if (!mockStateStore) return;
217
+ const state = pipelines.get(featureId);
218
+ if (!state) return;
219
+ mockStateStore.save(featureId, state);
220
+ };
221
+
222
+ pipelines.set('feat_010', { status: 'developing', cycle: 1 });
223
+ persistState('feat_010');
224
+
225
+ pipelines.get('feat_010').status = 'reviewing';
226
+ persistState('feat_010');
227
+
228
+ pipelines.get('feat_010').status = 'completed';
229
+ persistState('feat_010');
230
+
231
+ // Wait for async operations
232
+ await new Promise(r => setTimeout(r, 10));
233
+
234
+ assert.equal(persistCalls.length, 3);
235
+ assert.equal(persistCalls[0].status, 'developing');
236
+ assert.equal(persistCalls[1].status, 'reviewing');
237
+ assert.equal(persistCalls[2].status, 'completed');
238
+ });
239
+
240
+ it('persistState handles stateStore failure gracefully', async () => {
241
+ const errors = [];
242
+ const mockStateStore = {
243
+ save: async () => {
244
+ throw new Error('DB connection lost');
245
+ },
246
+ };
247
+
248
+ const pipelines = new Map();
249
+ const log = (tag, msg) => { if (tag === 'PERSIST') errors.push(msg); };
250
+
251
+ const persistState = (featureId) => {
252
+ if (!mockStateStore) return;
253
+ const state = pipelines.get(featureId);
254
+ if (!state) return;
255
+ mockStateStore.save(featureId, state).catch(err => {
256
+ log('PERSIST', `Failed to persist state for ${featureId}: ${err.message}`);
257
+ });
258
+ };
259
+
260
+ pipelines.set('feat_010', { status: 'developing', cycle: 1 });
261
+ persistState('feat_010');
262
+
263
+ await new Promise(r => setTimeout(r, 10));
264
+
265
+ assert.equal(errors.length, 1);
266
+ assert.ok(errors[0].includes('DB connection lost'));
267
+ });
268
+ });