@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.
- package/dist/bin/create.d.ts +2 -0
- package/dist/bin/create.js +25 -0
- package/dist/bin/create.js.map +1 -0
- package/dist/src/scaffolder.d.ts +22 -0
- package/dist/src/scaffolder.js +120 -0
- package/dist/src/scaffolder.js.map +1 -0
- package/package.json +24 -0
- package/templates/product-system/.env.example +8 -0
- package/templates/product-system/CLAUDE.md +45 -0
- package/templates/product-system/package.json +32 -0
- package/templates/product-system/packages/backend/package.json +37 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.test.ts +86 -0
- package/templates/product-system/packages/backend/src/middleware/auth_middleware.ts +35 -0
- package/templates/product-system/packages/backend/src/routes/auth.ts +463 -0
- package/templates/product-system/packages/backend/src/routes/coherence.ts +187 -0
- package/templates/product-system/packages/backend/src/routes/graph.ts +67 -0
- package/templates/product-system/packages/backend/src/routes/kanban.ts +201 -0
- package/templates/product-system/packages/backend/src/routes/pipeline.ts +41 -0
- package/templates/product-system/packages/backend/src/routes/projects.ts +122 -0
- package/templates/product-system/packages/backend/src/routes/users.ts +97 -0
- package/templates/product-system/packages/backend/src/server.ts +159 -0
- package/templates/product-system/packages/backend/src/services/auth_service.test.ts +115 -0
- package/templates/product-system/packages/backend/src/services/auth_service.ts +82 -0
- package/templates/product-system/packages/backend/src/services/coherence-review.ts +339 -0
- package/templates/product-system/packages/backend/src/services/email_service.ts +75 -0
- package/templates/product-system/packages/backend/src/services/init.ts +80 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.test.ts +235 -0
- package/templates/product-system/packages/backend/src/services/invitation_service.ts +193 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.test.ts +151 -0
- package/templates/product-system/packages/backend/src/services/password_reset_service.ts +135 -0
- package/templates/product-system/packages/backend/src/services/project_service.test.ts +215 -0
- package/templates/product-system/packages/backend/src/services/project_service.ts +171 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.test.ts +88 -0
- package/templates/product-system/packages/backend/src/services/pty_session_manager.ts +279 -0
- package/templates/product-system/packages/backend/src/services/terminal_ws_handler.ts +133 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.test.ts +158 -0
- package/templates/product-system/packages/backend/src/services/user_management_service.ts +128 -0
- package/templates/product-system/packages/backend/tsconfig.json +22 -0
- package/templates/product-system/packages/frontend/index.html +13 -0
- package/templates/product-system/packages/frontend/package-lock.json +2666 -0
- package/templates/product-system/packages/frontend/package.json +30 -0
- package/templates/product-system/packages/frontend/public/favicon.svg +16 -0
- package/templates/product-system/packages/frontend/src/App.tsx +29 -0
- package/templates/product-system/packages/frontend/src/api/client.ts +386 -0
- package/templates/product-system/packages/frontend/src/api/client_projects.test.ts +104 -0
- package/templates/product-system/packages/frontend/src/api/client_refresh.test.ts +145 -0
- package/templates/product-system/packages/frontend/src/components/CoherenceView.tsx +414 -0
- package/templates/product-system/packages/frontend/src/components/GraphLegend.tsx +124 -0
- package/templates/product-system/packages/frontend/src/components/GraphSettings.tsx +112 -0
- package/templates/product-system/packages/frontend/src/components/GraphView.tsx +370 -0
- package/templates/product-system/packages/frontend/src/components/InviteUserDialog.tsx +85 -0
- package/templates/product-system/packages/frontend/src/components/KanbanView.tsx +470 -0
- package/templates/product-system/packages/frontend/src/components/LoginPage.tsx +116 -0
- package/templates/product-system/packages/frontend/src/components/ProjectSelector.tsx +187 -0
- package/templates/product-system/packages/frontend/src/components/QaIssueSheet.tsx +192 -0
- package/templates/product-system/packages/frontend/src/components/SidePanel.tsx +231 -0
- package/templates/product-system/packages/frontend/src/components/TerminalView.tsx +200 -0
- package/templates/product-system/packages/frontend/src/components/Toolbar.tsx +84 -0
- package/templates/product-system/packages/frontend/src/components/UsersView.tsx +249 -0
- package/templates/product-system/packages/frontend/src/constants/graph.ts +191 -0
- package/templates/product-system/packages/frontend/src/hooks/useAuth.tsx +54 -0
- package/templates/product-system/packages/frontend/src/hooks/useGraph.ts +27 -0
- package/templates/product-system/packages/frontend/src/hooks/useKanban.ts +21 -0
- package/templates/product-system/packages/frontend/src/hooks/useProjects.ts +86 -0
- package/templates/product-system/packages/frontend/src/hooks/useTheme.ts +26 -0
- package/templates/product-system/packages/frontend/src/hooks/useToast.tsx +62 -0
- package/templates/product-system/packages/frontend/src/hooks/use_projects_logic.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/main.tsx +12 -0
- package/templates/product-system/packages/frontend/src/pages/accept_invitation_page.tsx +167 -0
- package/templates/product-system/packages/frontend/src/pages/forgot_password_page.tsx +100 -0
- package/templates/product-system/packages/frontend/src/pages/register_page.tsx +137 -0
- package/templates/product-system/packages/frontend/src/pages/reset_password_page.tsx +146 -0
- package/templates/product-system/packages/frontend/src/routes/ProtectedRoute.tsx +12 -0
- package/templates/product-system/packages/frontend/src/routes/accept_invitation.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/dashboard.tsx +221 -0
- package/templates/product-system/packages/frontend/src/routes/forgot_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/routes/login.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/register.tsx +14 -0
- package/templates/product-system/packages/frontend/src/routes/reset_password.tsx +13 -0
- package/templates/product-system/packages/frontend/src/styles/index.css +3358 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.test.ts +51 -0
- package/templates/product-system/packages/frontend/src/utils/auth_validation.ts +19 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.test.ts +61 -0
- package/templates/product-system/packages/frontend/src/utils/login_validation.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/logout.test.ts +63 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.test.ts +62 -0
- package/templates/product-system/packages/frontend/src/utils/node_sizing.ts +24 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.test.ts +53 -0
- package/templates/product-system/packages/frontend/src/utils/task_status.ts +14 -0
- package/templates/product-system/packages/frontend/tsconfig.json +21 -0
- package/templates/product-system/packages/frontend/vite.config.ts +20 -0
- package/templates/product-system/packages/shared/.env.example +3 -0
- package/templates/product-system/packages/shared/README.md +1 -0
- package/templates/product-system/packages/shared/db/migrate.ts +32 -0
- package/templates/product-system/packages/shared/db/migrations/0000_dashing_gorgon.sql +128 -0
- package/templates/product-system/packages/shared/db/migrations/meta/0000_snapshot.json +819 -0
- package/templates/product-system/packages/shared/db/migrations/meta/_journal.json +13 -0
- package/templates/product-system/packages/shared/db/schema.ts +137 -0
- package/templates/product-system/packages/shared/drizzle.config.js +14 -0
- package/templates/product-system/packages/shared/lib/claude-service.ts +215 -0
- package/templates/product-system/packages/shared/lib/coherence.ts +278 -0
- package/templates/product-system/packages/shared/lib/completeness.ts +30 -0
- package/templates/product-system/packages/shared/lib/constants.ts +327 -0
- package/templates/product-system/packages/shared/lib/db.ts +81 -0
- package/templates/product-system/packages/shared/lib/git_workflow.ts +110 -0
- package/templates/product-system/packages/shared/lib/graph.ts +186 -0
- package/templates/product-system/packages/shared/lib/kanban.ts +161 -0
- package/templates/product-system/packages/shared/lib/markdown.ts +205 -0
- package/templates/product-system/packages/shared/lib/pipeline-state-store.ts +124 -0
- package/templates/product-system/packages/shared/lib/pipeline.ts +489 -0
- package/templates/product-system/packages/shared/lib/prompt_builder.ts +170 -0
- package/templates/product-system/packages/shared/lib/relevance_search.ts +159 -0
- package/templates/product-system/packages/shared/lib/session.ts +152 -0
- package/templates/product-system/packages/shared/lib/validator.ts +117 -0
- package/templates/product-system/packages/shared/lib/work_summary_parser.ts +130 -0
- package/templates/product-system/packages/shared/package.json +30 -0
- package/templates/product-system/packages/shared/scripts/assign-project.ts +52 -0
- package/templates/product-system/packages/shared/tools/add_edge.ts +61 -0
- package/templates/product-system/packages/shared/tools/add_node.ts +101 -0
- package/templates/product-system/packages/shared/tools/end_session.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_gaps.ts +87 -0
- package/templates/product-system/packages/shared/tools/get_kanban.ts +125 -0
- package/templates/product-system/packages/shared/tools/get_node.ts +78 -0
- package/templates/product-system/packages/shared/tools/get_status.ts +98 -0
- package/templates/product-system/packages/shared/tools/migrate_to_turso.ts +385 -0
- package/templates/product-system/packages/shared/tools/move_card.ts +143 -0
- package/templates/product-system/packages/shared/tools/rebuild_index.ts +77 -0
- package/templates/product-system/packages/shared/tools/remove_edge.ts +59 -0
- package/templates/product-system/packages/shared/tools/remove_node.ts +96 -0
- package/templates/product-system/packages/shared/tools/resolve_question.ts +75 -0
- package/templates/product-system/packages/shared/tools/search_nodes.ts +106 -0
- package/templates/product-system/packages/shared/tools/start_session.ts +144 -0
- package/templates/product-system/packages/shared/tools/update_node.ts +133 -0
- package/templates/product-system/packages/shared/tsconfig.json +24 -0
- package/templates/product-system/pnpm-workspace.yaml +2 -0
- package/templates/product-system/smoke_test.ts +219 -0
- package/templates/product-system/tests/coherence_review.test.ts +562 -0
- package/templates/product-system/tests/db_sqlite_fallback.test.ts +75 -0
- package/templates/product-system/tests/edge_type_color_coding.test.ts +147 -0
- package/templates/product-system/tests/emit-tool-use-events.test.ts +85 -0
- package/templates/product-system/tests/feature_kind.test.ts +139 -0
- package/templates/product-system/tests/gap_indicators.test.ts +199 -0
- package/templates/product-system/tests/graceful_init.test.ts +142 -0
- package/templates/product-system/tests/graph_legend.test.ts +314 -0
- package/templates/product-system/tests/graph_settings_sheet.test.ts +804 -0
- package/templates/product-system/tests/hide_defined_filter.test.ts +205 -0
- package/templates/product-system/tests/kanban.test.ts +529 -0
- package/templates/product-system/tests/neighborhood_focus.test.ts +132 -0
- package/templates/product-system/tests/node_search.test.ts +340 -0
- package/templates/product-system/tests/node_sizing.test.ts +170 -0
- package/templates/product-system/tests/node_type_toggle_filters.test.ts +285 -0
- package/templates/product-system/tests/node_type_visual_encoding.test.ts +103 -0
- package/templates/product-system/tests/pipeline-state-store.test.ts +268 -0
- package/templates/product-system/tests/pipeline-unit.test.ts +593 -0
- package/templates/product-system/tests/pipeline.test.ts +195 -0
- package/templates/product-system/tests/pipeline_stats_all_cards.test.ts +193 -0
- package/templates/product-system/tests/play_all.test.ts +296 -0
- package/templates/product-system/tests/qa_issue_sheet.test.ts +464 -0
- package/templates/product-system/tests/relevance_search.test.ts +186 -0
- package/templates/product-system/tests/search_reorder.test.ts +88 -0
- package/templates/product-system/tests/serve_ui.test.ts +281 -0
- package/templates/product-system/tests/serve_ui_drizzle.test.ts +114 -0
- package/templates/product-system/tests/session_context_recall.test.ts +135 -0
- package/templates/product-system/tests/side_panel.test.ts +345 -0
- package/templates/product-system/tests/spec_completeness_label.test.ts +69 -0
- package/templates/product-system/tests/url_routing_test.ts +122 -0
- package/templates/product-system/tests/user_login.test.ts +150 -0
- package/templates/product-system/tests/user_registration.test.ts +205 -0
- package/templates/product-system/tests/web_terminal.test.ts +572 -0
- package/templates/product-system/tests/work_summary.test.ts +211 -0
- package/templates/product-system/tests/zoom_pan.test.ts +43 -0
- package/templates/product-system/tsconfig.json +24 -0
- package/templates/skills/product-bootstrap/SKILL.md +312 -0
- package/templates/skills/product-code-reviewer/SKILL.md +147 -0
- package/templates/skills/product-debugger/SKILL.md +206 -0
- package/templates/skills/product-debugger/references/agent-browser.md +1156 -0
- package/templates/skills/product-developer/SKILL.md +182 -0
- package/templates/skills/product-interview/SKILL.md +220 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Node Search with real-time graph highlighting (feat_020 + feat_035).
|
|
3
|
+
* Uses node:test built-in runner.
|
|
4
|
+
* Tests search logic, type filtering, onQueryChange callback, and clear behavior.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, mock } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
const ALL_NODE_TYPES = [
|
|
9
|
+
'feature', 'component', 'data_entity', 'decision', 'tech_choice',
|
|
10
|
+
'non_functional_requirement', 'design_token', 'design_pattern',
|
|
11
|
+
'user_role', 'flow', 'assumption', 'open_question',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const TYPE_DISPLAY_NAMES: Record<string, string> = {
|
|
15
|
+
feature: 'Feature', component: 'Component', data_entity: 'Data Entity',
|
|
16
|
+
decision: 'Decision', tech_choice: 'Tech Choice',
|
|
17
|
+
non_functional_requirement: 'NFR', design_token: 'Design Token',
|
|
18
|
+
design_pattern: 'Design Pattern', user_role: 'User Role',
|
|
19
|
+
flow: 'Flow', assumption: 'Assumption', open_question: 'Open Question',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
class NodeSearch {
|
|
23
|
+
nodes: any[] = [];
|
|
24
|
+
query = '';
|
|
25
|
+
inputEl: any = null;
|
|
26
|
+
onSelect: any;
|
|
27
|
+
onQueryChange: any;
|
|
28
|
+
selectedType: string | null = null;
|
|
29
|
+
|
|
30
|
+
constructor(_container: any, _doc: any, onSelect: any, onQueryChange?: any) {
|
|
31
|
+
this.onSelect = onSelect;
|
|
32
|
+
this.onQueryChange = onQueryChange || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setNodes(nodes: any) {
|
|
36
|
+
this.nodes = nodes || [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
search(query: string | null, typeFilter: string | null): any[] {
|
|
40
|
+
if (!query || !query.trim()) return [];
|
|
41
|
+
const q = query.toLowerCase().trim();
|
|
42
|
+
let results = this.nodes.filter((n: any) => n.name.toLowerCase().includes(q));
|
|
43
|
+
if (typeFilter) results = results.filter((n: any) => n.type === typeFilter);
|
|
44
|
+
return results.slice(0, 20);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
handleInput(e: any) {
|
|
48
|
+
this.query = e.target.value;
|
|
49
|
+
if (this.onQueryChange) this.onQueryChange(this.query);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
handleTypeChange(e: any) {
|
|
53
|
+
this.selectedType = e.target.value;
|
|
54
|
+
if (this.onQueryChange) this.onQueryChange(this.query);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
handleKeydown(e: any) {
|
|
58
|
+
if (e.key === 'Escape') {
|
|
59
|
+
this.clear();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
clear() {
|
|
64
|
+
this.query = '';
|
|
65
|
+
if (this.inputEl) this.inputEl.value = '';
|
|
66
|
+
if (this.onQueryChange) this.onQueryChange('');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
closeResults() {
|
|
70
|
+
// no-op for backward compat
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Test fixtures
|
|
75
|
+
const makeNodes = () => [
|
|
76
|
+
{ id: 'feat_001', type: 'feature', name: 'Node CRUD Operations' },
|
|
77
|
+
{ id: 'feat_002', type: 'feature', name: 'Edge Management' },
|
|
78
|
+
{ id: 'comp_001', type: 'component', name: 'Graph Renderer' },
|
|
79
|
+
{ id: 'dec_001', type: 'decision', name: 'Force-Directed Layout' },
|
|
80
|
+
{ id: 'tech_001', type: 'tech_choice', name: 'D3.js Force Graph' },
|
|
81
|
+
{ id: 'nfr_001', type: 'non_functional_requirement', name: 'Development Guidelines' },
|
|
82
|
+
{ id: 'dpat_001', type: 'design_pattern', name: 'Full-Screen Graph Layout' },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Minimal DOM mock for NodeSearch constructor (init not called)
|
|
86
|
+
const makeMockContainer = () => ({
|
|
87
|
+
querySelector: mock.fn(() => null),
|
|
88
|
+
contains: mock.fn(() => false),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const makeMockDocument = () => ({
|
|
92
|
+
addEventListener: mock.fn(),
|
|
93
|
+
removeEventListener: mock.fn(),
|
|
94
|
+
createElement: mock.fn(() => ({
|
|
95
|
+
className: '',
|
|
96
|
+
classList: { add: mock.fn() },
|
|
97
|
+
textContent: '',
|
|
98
|
+
appendChild: mock.fn(),
|
|
99
|
+
addEventListener: mock.fn(),
|
|
100
|
+
style: {},
|
|
101
|
+
})),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('NodeSearch', () => {
|
|
105
|
+
|
|
106
|
+
describe('search method', () => {
|
|
107
|
+
it('returns matching nodes by partial name (case-insensitive)', () => {
|
|
108
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null);
|
|
109
|
+
search.setNodes(makeNodes());
|
|
110
|
+
|
|
111
|
+
const results = search.search('crud', null);
|
|
112
|
+
assert.equal(results.length, 1);
|
|
113
|
+
assert.equal(results[0].id, 'feat_001');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('matches partial names', () => {
|
|
117
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null);
|
|
118
|
+
search.setNodes(makeNodes());
|
|
119
|
+
|
|
120
|
+
const results = search.search('graph', null);
|
|
121
|
+
assert.equal(results.length, 3);
|
|
122
|
+
const ids = results.map(r => r.id).sort();
|
|
123
|
+
assert.deepEqual(ids, ['comp_001', 'dpat_001', 'tech_001']);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('is case-insensitive', () => {
|
|
127
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null);
|
|
128
|
+
search.setNodes(makeNodes());
|
|
129
|
+
|
|
130
|
+
const lower = search.search('d3.js', null);
|
|
131
|
+
const upper = search.search('D3.JS', null);
|
|
132
|
+
const mixed = search.search('d3.Js', null);
|
|
133
|
+
|
|
134
|
+
assert.equal(lower.length, 1);
|
|
135
|
+
assert.equal(upper.length, 1);
|
|
136
|
+
assert.equal(mixed.length, 1);
|
|
137
|
+
assert.equal(lower[0].id, 'tech_001');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns empty array for empty query', () => {
|
|
141
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null);
|
|
142
|
+
search.setNodes(makeNodes());
|
|
143
|
+
|
|
144
|
+
assert.deepEqual(search.search('', null), []);
|
|
145
|
+
assert.deepEqual(search.search(' ', null), []);
|
|
146
|
+
assert.deepEqual(search.search(null, null), []);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns empty array when no matches', () => {
|
|
150
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null);
|
|
151
|
+
search.setNodes(makeNodes());
|
|
152
|
+
|
|
153
|
+
const results = search.search('zzzzzzz', null);
|
|
154
|
+
assert.equal(results.length, 0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('limits results to 20', () => {
|
|
158
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null);
|
|
159
|
+
const manyNodes = Array.from({ length: 30 }, (_, i) => ({
|
|
160
|
+
id: `feat_${i}`, type: 'feature', name: `Feature ${i}`,
|
|
161
|
+
}));
|
|
162
|
+
search.setNodes(manyNodes);
|
|
163
|
+
|
|
164
|
+
const results = search.search('Feature', null);
|
|
165
|
+
assert.equal(results.length, 20);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('type filter', () => {
|
|
170
|
+
it('filters results by node type', () => {
|
|
171
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null);
|
|
172
|
+
search.setNodes(makeNodes());
|
|
173
|
+
|
|
174
|
+
const results = search.search('graph', 'component');
|
|
175
|
+
assert.equal(results.length, 1);
|
|
176
|
+
assert.equal(results[0].id, 'comp_001');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('returns no results when type filter excludes all matches', () => {
|
|
180
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null);
|
|
181
|
+
search.setNodes(makeNodes());
|
|
182
|
+
|
|
183
|
+
const results = search.search('crud', 'decision');
|
|
184
|
+
assert.equal(results.length, 0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('passes null type filter to show all types', () => {
|
|
188
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null);
|
|
189
|
+
search.setNodes(makeNodes());
|
|
190
|
+
|
|
191
|
+
const results = search.search('e', null);
|
|
192
|
+
// Multiple types should appear
|
|
193
|
+
const types = new Set(results.map(r => r.type));
|
|
194
|
+
assert.ok(types.size > 1, 'multiple types should appear');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('onQueryChange callback', () => {
|
|
199
|
+
it('fires onQueryChange on every input event', () => {
|
|
200
|
+
const queryCallback = mock.fn();
|
|
201
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null, queryCallback);
|
|
202
|
+
search.setNodes(makeNodes());
|
|
203
|
+
|
|
204
|
+
search.handleInput({ target: { value: 'gra' } });
|
|
205
|
+
assert.equal(queryCallback.mock.calls.length, 1);
|
|
206
|
+
assert.equal(queryCallback.mock.calls[0].arguments[0], 'gra');
|
|
207
|
+
|
|
208
|
+
search.handleInput({ target: { value: 'graph' } });
|
|
209
|
+
assert.equal(queryCallback.mock.calls.length, 2);
|
|
210
|
+
assert.equal(queryCallback.mock.calls[1].arguments[0], 'graph');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('fires onQueryChange with empty string when input is cleared', () => {
|
|
214
|
+
const queryCallback = mock.fn();
|
|
215
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null, queryCallback);
|
|
216
|
+
|
|
217
|
+
search.handleInput({ target: { value: '' } });
|
|
218
|
+
assert.equal(queryCallback.mock.calls.length, 1);
|
|
219
|
+
assert.equal(queryCallback.mock.calls[0].arguments[0], '');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('does not throw when onQueryChange is null', () => {
|
|
223
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null, null);
|
|
224
|
+
assert.doesNotThrow(() => {
|
|
225
|
+
search.handleInput({ target: { value: 'test' } });
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('fires onQueryChange on type filter change', () => {
|
|
230
|
+
const queryCallback = mock.fn();
|
|
231
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null, queryCallback);
|
|
232
|
+
search.query = 'graph';
|
|
233
|
+
|
|
234
|
+
search.handleTypeChange({ target: { value: 'component' } });
|
|
235
|
+
assert.equal(queryCallback.mock.calls.length, 1);
|
|
236
|
+
assert.equal(queryCallback.mock.calls[0].arguments[0], 'graph');
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('clear method', () => {
|
|
241
|
+
it('resets query and input value', () => {
|
|
242
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null, mock.fn());
|
|
243
|
+
search.inputEl = { value: 'test' };
|
|
244
|
+
search.query = 'test';
|
|
245
|
+
|
|
246
|
+
search.clear();
|
|
247
|
+
|
|
248
|
+
assert.equal(search.query, '');
|
|
249
|
+
assert.equal(search.inputEl.value, '');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('fires onQueryChange with empty string', () => {
|
|
253
|
+
const queryCallback = mock.fn();
|
|
254
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null, queryCallback);
|
|
255
|
+
search.inputEl = { value: 'test' };
|
|
256
|
+
search.query = 'test';
|
|
257
|
+
|
|
258
|
+
search.clear();
|
|
259
|
+
|
|
260
|
+
assert.equal(queryCallback.mock.calls.length, 1);
|
|
261
|
+
assert.equal(queryCallback.mock.calls[0].arguments[0], '');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('Escape key calls clear', () => {
|
|
265
|
+
const queryCallback = mock.fn();
|
|
266
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null, queryCallback);
|
|
267
|
+
search.inputEl = { value: 'test' };
|
|
268
|
+
search.query = 'test';
|
|
269
|
+
|
|
270
|
+
search.handleKeydown({ key: 'Escape' });
|
|
271
|
+
|
|
272
|
+
assert.equal(search.query, '');
|
|
273
|
+
assert.equal(queryCallback.mock.calls.length, 1);
|
|
274
|
+
assert.equal(queryCallback.mock.calls[0].arguments[0], '');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('keyboard handling', () => {
|
|
279
|
+
it('Escape clears the search', () => {
|
|
280
|
+
const queryCallback = mock.fn();
|
|
281
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null, queryCallback);
|
|
282
|
+
search.inputEl = { value: 'hello' };
|
|
283
|
+
search.query = 'hello';
|
|
284
|
+
|
|
285
|
+
search.handleKeydown({ key: 'Escape' });
|
|
286
|
+
|
|
287
|
+
assert.equal(search.query, '');
|
|
288
|
+
assert.equal(search.inputEl.value, '');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('non-Escape keys do not clear', () => {
|
|
292
|
+
const queryCallback = mock.fn();
|
|
293
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null, queryCallback);
|
|
294
|
+
search.query = 'hello';
|
|
295
|
+
|
|
296
|
+
search.handleKeydown({ key: 'Enter' });
|
|
297
|
+
|
|
298
|
+
assert.equal(search.query, 'hello');
|
|
299
|
+
assert.equal(queryCallback.mock.calls.length, 0);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
describe('constants', () => {
|
|
304
|
+
it('ALL_NODE_TYPES contains all expected types', () => {
|
|
305
|
+
assert.equal(ALL_NODE_TYPES.length, 12);
|
|
306
|
+
assert.ok(ALL_NODE_TYPES.includes('feature'));
|
|
307
|
+
assert.ok(ALL_NODE_TYPES.includes('decision'));
|
|
308
|
+
assert.ok(ALL_NODE_TYPES.includes('open_question'));
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('TYPE_DISPLAY_NAMES has an entry for every type', () => {
|
|
312
|
+
for (const type of ALL_NODE_TYPES) {
|
|
313
|
+
assert.ok(TYPE_DISPLAY_NAMES[type], `missing display name for ${type}`);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('setNodes', () => {
|
|
319
|
+
it('sets nodes for searching', () => {
|
|
320
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null);
|
|
321
|
+
assert.deepEqual(search.nodes, []);
|
|
322
|
+
|
|
323
|
+
search.setNodes(makeNodes());
|
|
324
|
+
assert.equal(search.nodes.length, 7);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('handles null nodes gracefully', () => {
|
|
328
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null);
|
|
329
|
+
search.setNodes(null);
|
|
330
|
+
assert.deepEqual(search.nodes, []);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('closeResults (backward compat)', () => {
|
|
335
|
+
it('is a no-op and does not throw', () => {
|
|
336
|
+
const search = new NodeSearch(makeMockContainer(), makeMockDocument(), null);
|
|
337
|
+
assert.doesNotThrow(() => search.closeResults());
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Node Sizing by Edge Count (feat_014).
|
|
3
|
+
* Uses node:test built-in runner.
|
|
4
|
+
* Tests that node radius scales proportionally to edge count,
|
|
5
|
+
* with min/max caps and perceptible size differences.
|
|
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 || 0) * 1.5, 20);
|
|
13
|
+
return base + scale;
|
|
14
|
+
},
|
|
15
|
+
buildNodes(graphData: any) {
|
|
16
|
+
const edgeCounts = new Map();
|
|
17
|
+
graphData.edges.forEach((e: any) => {
|
|
18
|
+
edgeCounts.set(e.from, (edgeCounts.get(e.from) || 0) + 1);
|
|
19
|
+
edgeCounts.set(e.to, (edgeCounts.get(e.to) || 0) + 1);
|
|
20
|
+
});
|
|
21
|
+
return graphData.nodes.map((n: any) => ({
|
|
22
|
+
...n,
|
|
23
|
+
edgeCount: edgeCounts.get(n.id) || 0,
|
|
24
|
+
}));
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const makeNode = (overrides = {}) => ({
|
|
29
|
+
id: 'feat_001',
|
|
30
|
+
type: 'feature',
|
|
31
|
+
name: 'Test Node',
|
|
32
|
+
completeness: 1.0,
|
|
33
|
+
open_questions_count: 0,
|
|
34
|
+
edgeCount: 0,
|
|
35
|
+
...overrides,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Node Sizing by Edge Count', () => {
|
|
39
|
+
describe('nodeRadius', () => {
|
|
40
|
+
const renderer = createRenderer();
|
|
41
|
+
|
|
42
|
+
it('returns base radius for nodes with zero edges', () => {
|
|
43
|
+
const node = makeNode({ edgeCount: 0 });
|
|
44
|
+
assert.equal(renderer.nodeRadius(node), 8);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('scales radius proportionally to edge count', () => {
|
|
48
|
+
const node1 = makeNode({ edgeCount: 1 });
|
|
49
|
+
const node2 = makeNode({ edgeCount: 4 });
|
|
50
|
+
const r1 = renderer.nodeRadius(node1);
|
|
51
|
+
const r2 = renderer.nodeRadius(node2);
|
|
52
|
+
|
|
53
|
+
assert.ok(r2 > r1, 'more edges should produce larger radius');
|
|
54
|
+
assert.equal(r1, 8 + 1.5); // 9.5
|
|
55
|
+
assert.equal(r2, 8 + 6); // 14
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('caps maximum radius to prevent extreme outliers', () => {
|
|
59
|
+
const node20 = makeNode({ edgeCount: 20 });
|
|
60
|
+
const node100 = makeNode({ edgeCount: 100 });
|
|
61
|
+
|
|
62
|
+
assert.equal(renderer.nodeRadius(node20), 28);
|
|
63
|
+
assert.equal(renderer.nodeRadius(node100), 28);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('achieves at least 2x radius ratio between least and most connected', () => {
|
|
67
|
+
const minNode = makeNode({ edgeCount: 0 });
|
|
68
|
+
const maxNode = makeNode({ edgeCount: 20 });
|
|
69
|
+
|
|
70
|
+
const minR = renderer.nodeRadius(minNode);
|
|
71
|
+
const maxR = renderer.nodeRadius(maxNode);
|
|
72
|
+
const ratio = maxR / minR;
|
|
73
|
+
|
|
74
|
+
assert.ok(ratio >= 2, `ratio ${ratio.toFixed(2)} should be >= 2`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('produces monotonically increasing radii up to the cap', () => {
|
|
78
|
+
const radii = [0, 1, 2, 3, 5, 8, 13].map(
|
|
79
|
+
ec => renderer.nodeRadius(makeNode({ edgeCount: ec }))
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
for (let i = 1; i < radii.length; i++) {
|
|
83
|
+
assert.ok(radii[i] >= radii[i - 1],
|
|
84
|
+
`radius at edgeCount=${[0, 1, 2, 3, 5, 8, 13][i]} should be >= previous`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('buildNodes edge count calculation', () => {
|
|
90
|
+
const renderer = createRenderer();
|
|
91
|
+
|
|
92
|
+
it('counts both inbound and outbound edges per node', () => {
|
|
93
|
+
const graphData = {
|
|
94
|
+
nodes: [
|
|
95
|
+
{ id: 'a', type: 'feature', name: 'A', completeness: 1, open_questions_count: 0 },
|
|
96
|
+
{ id: 'b', type: 'component', name: 'B', completeness: 1, open_questions_count: 0 },
|
|
97
|
+
{ id: 'c', type: 'decision', name: 'C', completeness: 1, open_questions_count: 0 },
|
|
98
|
+
],
|
|
99
|
+
edges: [
|
|
100
|
+
{ from: 'a', to: 'b', relation: 'contains' },
|
|
101
|
+
{ from: 'a', to: 'c', relation: 'governed_by' },
|
|
102
|
+
{ from: 'b', to: 'c', relation: 'depends_on' },
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const nodes = renderer.buildNodes(graphData);
|
|
107
|
+
const byId = Object.fromEntries(nodes.map(n => [n.id, n]));
|
|
108
|
+
|
|
109
|
+
assert.equal(byId['a'].edgeCount, 2); // 2 outbound
|
|
110
|
+
assert.equal(byId['b'].edgeCount, 2); // 1 inbound + 1 outbound
|
|
111
|
+
assert.equal(byId['c'].edgeCount, 2); // 2 inbound
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('assigns zero edge count to isolated nodes', () => {
|
|
115
|
+
const graphData = {
|
|
116
|
+
nodes: [
|
|
117
|
+
{ id: 'x', type: 'feature', name: 'X', completeness: 1, open_questions_count: 0 },
|
|
118
|
+
],
|
|
119
|
+
edges: [],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const nodes = renderer.buildNodes(graphData);
|
|
123
|
+
assert.equal(nodes[0].edgeCount, 0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('counts hub nodes with many connections correctly', () => {
|
|
127
|
+
const graphData = {
|
|
128
|
+
nodes: [
|
|
129
|
+
{ id: 'hub', type: 'feature', name: 'Hub', completeness: 1, open_questions_count: 0 },
|
|
130
|
+
{ id: 'a', type: 'component', name: 'A', completeness: 1, open_questions_count: 0 },
|
|
131
|
+
{ id: 'b', type: 'component', name: 'B', completeness: 1, open_questions_count: 0 },
|
|
132
|
+
{ id: 'c', type: 'component', name: 'C', completeness: 1, open_questions_count: 0 },
|
|
133
|
+
{ id: 'd', type: 'component', name: 'D', completeness: 1, open_questions_count: 0 },
|
|
134
|
+
{ id: 'e', type: 'component', name: 'E', completeness: 1, open_questions_count: 0 },
|
|
135
|
+
],
|
|
136
|
+
edges: [
|
|
137
|
+
{ from: 'hub', to: 'a', relation: 'contains' },
|
|
138
|
+
{ from: 'hub', to: 'b', relation: 'contains' },
|
|
139
|
+
{ from: 'hub', to: 'c', relation: 'contains' },
|
|
140
|
+
{ from: 'hub', to: 'd', relation: 'contains' },
|
|
141
|
+
{ from: 'hub', to: 'e', relation: 'contains' },
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const nodes = renderer.buildNodes(graphData);
|
|
146
|
+
const hub = nodes.find(n => n.id === 'hub');
|
|
147
|
+
const leaf = nodes.find(n => n.id === 'a');
|
|
148
|
+
|
|
149
|
+
assert.equal(hub.edgeCount, 5);
|
|
150
|
+
assert.equal(leaf.edgeCount, 1);
|
|
151
|
+
assert.ok(
|
|
152
|
+
renderer.nodeRadius(hub) > renderer.nodeRadius(leaf),
|
|
153
|
+
'hub should be visually larger than leaf'
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('collision force uses node radius', () => {
|
|
159
|
+
it('nodeRadius returns consistent values for force simulation', () => {
|
|
160
|
+
const renderer = createRenderer();
|
|
161
|
+
|
|
162
|
+
// Verify the radius function is deterministic
|
|
163
|
+
const node = makeNode({ edgeCount: 5 });
|
|
164
|
+
const r1 = renderer.nodeRadius(node);
|
|
165
|
+
const r2 = renderer.nodeRadius(node);
|
|
166
|
+
assert.equal(r1, r2);
|
|
167
|
+
assert.equal(r1, 8 + Math.min(5 * 1.5, 20)); // 15.5
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|