@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,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph-aware relevance search with ranking.
|
|
3
|
+
*
|
|
4
|
+
* Algorithm:
|
|
5
|
+
* 1. Direct matches — keyword substring in node name or body
|
|
6
|
+
* 2. Graph expansion — 1-hop neighbors of each direct match via edges
|
|
7
|
+
* 3. Rank by edge type weight — strong architectural signals score higher
|
|
8
|
+
* 4. Deduplicate — keep highest relevance per node
|
|
9
|
+
* 5. Cold start fallback — if zero direct matches, return top-5 most-connected per type
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Edge types that carry strong architectural signal. */
|
|
13
|
+
const STRONG_EDGES = new Set([
|
|
14
|
+
'depends_on',
|
|
15
|
+
'implemented_with',
|
|
16
|
+
'governed_by',
|
|
17
|
+
'contains',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} RankedResult
|
|
22
|
+
* @property {string} id
|
|
23
|
+
* @property {string} name
|
|
24
|
+
* @property {string} type
|
|
25
|
+
* @property {string} status
|
|
26
|
+
* @property {number} completeness
|
|
27
|
+
* @property {number} open_questions_count
|
|
28
|
+
* @property {string} relevance - 'direct' | 'via <sourceId> → <edgeType>' description
|
|
29
|
+
* @property {number} _score - internal sort score (higher = more relevant)
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Run a relevance-ranked search over the graph.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} keyword - search term (case-insensitive substring match)
|
|
36
|
+
* @param {Array} allNodes - full node list from readGraph().nodes
|
|
37
|
+
* @param {Array} allEdges - full edge list from readGraph().edges
|
|
38
|
+
* @param {Function} getBody - async (nodeId) => string|null — fetches body content
|
|
39
|
+
* @returns {Promise<RankedResult[]>} ranked results, direct first then neighbors
|
|
40
|
+
*/
|
|
41
|
+
export const relevanceSearch = async (keyword, allNodes, allEdges, getBody) => {
|
|
42
|
+
const q = keyword.toLowerCase();
|
|
43
|
+
|
|
44
|
+
// --- Step 1: direct matches ---
|
|
45
|
+
const directMatches = [];
|
|
46
|
+
for (const n of allNodes) {
|
|
47
|
+
if (n.name.toLowerCase().includes(q)) {
|
|
48
|
+
directMatches.push(n);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const body = await getBody(n.id);
|
|
52
|
+
if (body && body.toLowerCase().includes(q)) {
|
|
53
|
+
directMatches.push(n);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Cold start fallback ---
|
|
58
|
+
if (directMatches.length === 0) {
|
|
59
|
+
return coldStartFallback(allNodes, allEdges);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Build a map for quick lookup
|
|
63
|
+
const nodeMap = new Map(allNodes.map(n => [n.id, n]));
|
|
64
|
+
const directIds = new Set(directMatches.map(n => n.id));
|
|
65
|
+
|
|
66
|
+
// Results keyed by node id → best RankedResult
|
|
67
|
+
const resultsMap = new Map();
|
|
68
|
+
|
|
69
|
+
// Add direct matches with score 100
|
|
70
|
+
for (const n of directMatches) {
|
|
71
|
+
resultsMap.set(n.id, toResult(n, 'direct', 100));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Step 2: 1-hop graph expansion ---
|
|
75
|
+
for (const match of directMatches) {
|
|
76
|
+
const neighbors = getNeighbors(match.id, allEdges);
|
|
77
|
+
for (const { nodeId, edgeType, direction } of neighbors) {
|
|
78
|
+
if (!nodeMap.has(nodeId)) continue;
|
|
79
|
+
const isStrong = STRONG_EDGES.has(edgeType);
|
|
80
|
+
const score = isStrong ? 50 : 20;
|
|
81
|
+
const label = `via ${match.id} ${direction} ${edgeType}`;
|
|
82
|
+
|
|
83
|
+
const existing = resultsMap.get(nodeId);
|
|
84
|
+
if (!existing || existing._score < score) {
|
|
85
|
+
resultsMap.set(nodeId, toResult(nodeMap.get(nodeId), label, score));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// --- Step 3: sort by score descending, then by id for stability ---
|
|
91
|
+
const results = [...resultsMap.values()];
|
|
92
|
+
results.sort((a, b) => b._score - a._score || a.id.localeCompare(b.id));
|
|
93
|
+
|
|
94
|
+
return results;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Cold start fallback: return top 5 most-connected nodes per type as structural overview.
|
|
99
|
+
*/
|
|
100
|
+
export const coldStartFallback = (allNodes, allEdges) => {
|
|
101
|
+
// Count edges per node
|
|
102
|
+
const edgeCounts = new Map();
|
|
103
|
+
for (const e of allEdges) {
|
|
104
|
+
edgeCounts.set(e.from, (edgeCounts.get(e.from) || 0) + 1);
|
|
105
|
+
edgeCounts.set(e.to, (edgeCounts.get(e.to) || 0) + 1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Group by type
|
|
109
|
+
const byType = new Map();
|
|
110
|
+
for (const n of allNodes) {
|
|
111
|
+
if (!byType.has(n.type)) byType.set(n.type, []);
|
|
112
|
+
byType.get(n.type).push(n);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const [type, typeNodes] of byType) {
|
|
117
|
+
// Sort by edge count descending
|
|
118
|
+
typeNodes.sort((a, b) => (edgeCounts.get(b.id) || 0) - (edgeCounts.get(a.id) || 0));
|
|
119
|
+
const top5 = typeNodes.slice(0, 5);
|
|
120
|
+
for (const n of top5) {
|
|
121
|
+
const count = edgeCounts.get(n.id) || 0;
|
|
122
|
+
results.push(toResult(n, `structural overview (${count} connections)`, count));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Sort by edge count descending overall, then by id
|
|
127
|
+
results.sort((a, b) => b._score - a._score || a.id.localeCompare(b.id));
|
|
128
|
+
return results;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get 1-hop neighbors of a node from the edge list.
|
|
133
|
+
* Returns both outbound (node → neighbor) and inbound (neighbor → node).
|
|
134
|
+
*/
|
|
135
|
+
const getNeighbors = (nodeId, edges) => {
|
|
136
|
+
const neighbors = [];
|
|
137
|
+
for (const e of edges) {
|
|
138
|
+
if (e.from === nodeId) {
|
|
139
|
+
neighbors.push({ nodeId: e.to, edgeType: e.relation, direction: '→' });
|
|
140
|
+
} else if (e.to === nodeId) {
|
|
141
|
+
neighbors.push({ nodeId: e.from, edgeType: e.relation, direction: '←' });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return neighbors;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Create a ranked result object from a node.
|
|
149
|
+
*/
|
|
150
|
+
const toResult = (node, relevance, score) => ({
|
|
151
|
+
id: node.id,
|
|
152
|
+
name: node.name,
|
|
153
|
+
type: node.type,
|
|
154
|
+
status: node.status,
|
|
155
|
+
completeness: node.completeness,
|
|
156
|
+
open_questions_count: node.open_questions_count,
|
|
157
|
+
relevance,
|
|
158
|
+
_score: score,
|
|
159
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session operations backed by Drizzle/Turso.
|
|
3
|
+
* Replaces direct session file I/O in tools.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { eq, desc, max, isNotNull, gte, and } from 'drizzle-orm';
|
|
7
|
+
import { getDb } from './db.js';
|
|
8
|
+
import { sessions, nodes } from '../db/schema.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the next session number (max existing + 1).
|
|
12
|
+
*/
|
|
13
|
+
export const getNextSessionNumber = async (projectId?: string) => {
|
|
14
|
+
const db = getDb();
|
|
15
|
+
const query = projectId
|
|
16
|
+
? db.select({ maxNum: max(sessions.sessionNumber) }).from(sessions).where(eq(sessions.projectId, projectId))
|
|
17
|
+
: db.select({ maxNum: max(sessions.sessionNumber) }).from(sessions);
|
|
18
|
+
const rows = await query;
|
|
19
|
+
return (rows[0]?.maxNum || 0) + 1;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a new session record.
|
|
24
|
+
*/
|
|
25
|
+
export const createSession = async (sessionNum, body, projectId?: string) => {
|
|
26
|
+
const db = getDb();
|
|
27
|
+
const now = new Date().toISOString();
|
|
28
|
+
|
|
29
|
+
await db.insert(sessions).values({
|
|
30
|
+
sessionNumber: sessionNum,
|
|
31
|
+
startedAt: now,
|
|
32
|
+
endedAt: null,
|
|
33
|
+
summary: null,
|
|
34
|
+
nodesTouched: '[]',
|
|
35
|
+
questionsResolved: 0,
|
|
36
|
+
body: body || '',
|
|
37
|
+
projectId: projectId || null,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return { session: sessionNum, started_at: now };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the latest (most recent) session.
|
|
45
|
+
*/
|
|
46
|
+
export const getLatestSession = async (projectId?: string) => {
|
|
47
|
+
const db = getDb();
|
|
48
|
+
const query = projectId
|
|
49
|
+
? db.select().from(sessions).where(eq(sessions.projectId, projectId)).orderBy(desc(sessions.sessionNumber)).limit(1)
|
|
50
|
+
: db.select().from(sessions).orderBy(desc(sessions.sessionNumber)).limit(1);
|
|
51
|
+
const rows = await query;
|
|
52
|
+
return rows[0];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Update/end a session with summary and metadata.
|
|
57
|
+
*/
|
|
58
|
+
export const endSession = async (sessionNum, updates) => {
|
|
59
|
+
const db = getDb();
|
|
60
|
+
|
|
61
|
+
const updateFields = {};
|
|
62
|
+
if (updates.ended_at) updateFields.endedAt = updates.ended_at;
|
|
63
|
+
if (updates.summary) updateFields.summary = updates.summary;
|
|
64
|
+
if (updates.nodes_touched) updateFields.nodesTouched = JSON.stringify(updates.nodes_touched);
|
|
65
|
+
if (updates.questions_resolved !== undefined) updateFields.questionsResolved = updates.questions_resolved;
|
|
66
|
+
if (updates.body !== undefined) updateFields.body = updates.body;
|
|
67
|
+
|
|
68
|
+
await db.update(sessions).set(updateFields).where(eq(sessions.sessionNumber, sessionNum));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get a session by number.
|
|
73
|
+
*/
|
|
74
|
+
export const getSession = async (sessionNum) => {
|
|
75
|
+
const db = getDb();
|
|
76
|
+
const rows = await db.select().from(sessions).where(eq(sessions.sessionNumber, sessionNum));
|
|
77
|
+
return rows[0];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the last N ended sessions (most recent first).
|
|
82
|
+
*/
|
|
83
|
+
export const getRecentSessions = async (count = 3, projectId?: string) => {
|
|
84
|
+
const db = getDb();
|
|
85
|
+
const conditions = [isNotNull(sessions.endedAt)];
|
|
86
|
+
if (projectId) conditions.push(eq(sessions.projectId, projectId));
|
|
87
|
+
const rows = await db.select()
|
|
88
|
+
.from(sessions)
|
|
89
|
+
.where(and(...conditions))
|
|
90
|
+
.orderBy(desc(sessions.sessionNumber))
|
|
91
|
+
.limit(count);
|
|
92
|
+
return rows;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get the N most recently modified nodes (by updated_at).
|
|
97
|
+
*/
|
|
98
|
+
export const getRecentlyModifiedNodes = async (count = 10, projectId?: string) => {
|
|
99
|
+
const db = getDb();
|
|
100
|
+
const query = projectId
|
|
101
|
+
? db.select({
|
|
102
|
+
id: nodes.id,
|
|
103
|
+
name: nodes.name,
|
|
104
|
+
type: nodes.type,
|
|
105
|
+
status: nodes.status,
|
|
106
|
+
completeness: nodes.completeness,
|
|
107
|
+
updatedAt: nodes.updatedAt,
|
|
108
|
+
})
|
|
109
|
+
.from(nodes)
|
|
110
|
+
.where(eq(nodes.projectId, projectId))
|
|
111
|
+
.orderBy(desc(nodes.updatedAt))
|
|
112
|
+
.limit(count)
|
|
113
|
+
: db.select({
|
|
114
|
+
id: nodes.id,
|
|
115
|
+
name: nodes.name,
|
|
116
|
+
type: nodes.type,
|
|
117
|
+
status: nodes.status,
|
|
118
|
+
completeness: nodes.completeness,
|
|
119
|
+
updatedAt: nodes.updatedAt,
|
|
120
|
+
})
|
|
121
|
+
.from(nodes)
|
|
122
|
+
.orderBy(desc(nodes.updatedAt))
|
|
123
|
+
.limit(count);
|
|
124
|
+
const rows = await query;
|
|
125
|
+
return rows;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get decision nodes updated within the timeframe of recent sessions.
|
|
130
|
+
* Uses the earliest startedAt from recentSessions as the cutoff.
|
|
131
|
+
*/
|
|
132
|
+
export const getRecentDecisions = async (recentSessions) => {
|
|
133
|
+
if (!recentSessions || recentSessions.length === 0) return [];
|
|
134
|
+
|
|
135
|
+
const earliest = recentSessions[recentSessions.length - 1].startedAt;
|
|
136
|
+
const db = getDb();
|
|
137
|
+
const rows = await db.select({
|
|
138
|
+
id: nodes.id,
|
|
139
|
+
name: nodes.name,
|
|
140
|
+
status: nodes.status,
|
|
141
|
+
body: nodes.body,
|
|
142
|
+
updatedAt: nodes.updatedAt,
|
|
143
|
+
})
|
|
144
|
+
.from(nodes)
|
|
145
|
+
.where(
|
|
146
|
+
eq(nodes.type, 'decision'),
|
|
147
|
+
)
|
|
148
|
+
.orderBy(desc(nodes.updatedAt));
|
|
149
|
+
|
|
150
|
+
// Filter in JS since drizzle-orm sqlite doesn't support combined where with gte on text dates reliably
|
|
151
|
+
return rows.filter(r => r.updatedAt >= earliest);
|
|
152
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invariant checks for the interview system knowledge graph.
|
|
3
|
+
* All assertion functions throw on failure.
|
|
4
|
+
* Backed by Drizzle/Turso queries.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { eq, and } from 'drizzle-orm';
|
|
8
|
+
import { NODE_TYPES, VALID_RELATIONS } from './constants.js';
|
|
9
|
+
import { getDb } from './db.js';
|
|
10
|
+
import { nodes, edges } from '../db/schema.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Assert that a node with the given ID exists in the database.
|
|
14
|
+
*/
|
|
15
|
+
export const assertNodeExists = async (id) => {
|
|
16
|
+
const db = getDb();
|
|
17
|
+
const rows = await db.select({ id: nodes.id }).from(nodes).where(eq(nodes.id, id));
|
|
18
|
+
if (!rows[0]) {
|
|
19
|
+
throw new Error(`Node does not exist: ${id}`);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Assert that a type string is a valid node type.
|
|
25
|
+
*/
|
|
26
|
+
export const assertValidType = (type) => {
|
|
27
|
+
if (!NODE_TYPES[type]) {
|
|
28
|
+
const valid = Object.keys(NODE_TYPES).join(', ');
|
|
29
|
+
throw new Error(`Invalid node type "${type}". Valid types: ${valid}`);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Assert that a relation string is a valid edge relation.
|
|
35
|
+
*/
|
|
36
|
+
export const assertValidRelation = (relation) => {
|
|
37
|
+
if (!VALID_RELATIONS.includes(relation)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Invalid relation "${relation}". Valid relations: ${VALID_RELATIONS.join(', ')}`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Assert that an edge doesn't already exist in the database.
|
|
46
|
+
*/
|
|
47
|
+
export const assertNoDuplicateEdge = async (from, relation, to) => {
|
|
48
|
+
const db = getDb();
|
|
49
|
+
const rows = await db.select().from(edges).where(
|
|
50
|
+
and(
|
|
51
|
+
eq(edges.fromId, from),
|
|
52
|
+
eq(edges.relation, relation),
|
|
53
|
+
eq(edges.toId, to),
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
if (rows[0]) {
|
|
57
|
+
throw new Error(`Duplicate edge: ${from} --${relation}--> ${to}`);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Assert that adding a depends_on edge from → to won't create a cycle.
|
|
63
|
+
* Uses BFS from `to` following depends_on edges to check if `from` is reachable.
|
|
64
|
+
*/
|
|
65
|
+
export const assertNoCycle = async (from, to, projectId?: string) => {
|
|
66
|
+
const db = getDb();
|
|
67
|
+
|
|
68
|
+
// Get all depends_on edges
|
|
69
|
+
const conditions = [eq(edges.relation, 'depends_on')];
|
|
70
|
+
if (projectId) conditions.push(eq(edges.projectId, projectId));
|
|
71
|
+
const depEdges = await db.select().from(edges).where(and(...conditions));
|
|
72
|
+
|
|
73
|
+
// Build adjacency list
|
|
74
|
+
const adj = new Map();
|
|
75
|
+
for (const edge of depEdges) {
|
|
76
|
+
if (!adj.has(edge.fromId)) adj.set(edge.fromId, []);
|
|
77
|
+
adj.get(edge.fromId).push(edge.toId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// BFS from `to` — if we can reach `from`, adding from→to creates a cycle
|
|
81
|
+
const visited = new Set();
|
|
82
|
+
const queue = [to];
|
|
83
|
+
|
|
84
|
+
while (queue.length > 0) {
|
|
85
|
+
const current = queue.shift();
|
|
86
|
+
if (current === from) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Cycle detected: adding ${from} --depends_on--> ${to} would create a circular dependency`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (visited.has(current)) continue;
|
|
92
|
+
visited.add(current);
|
|
93
|
+
|
|
94
|
+
const neighbors = adj.get(current) || [];
|
|
95
|
+
for (const neighbor of neighbors) {
|
|
96
|
+
if (!visited.has(neighbor)) {
|
|
97
|
+
queue.push(neighbor);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Assert that no other node of the same type has the same name.
|
|
105
|
+
*/
|
|
106
|
+
export const assertUniqueName = async (type, name, projectId?: string) => {
|
|
107
|
+
const db = getDb();
|
|
108
|
+
const conditions = [eq(nodes.type, type)];
|
|
109
|
+
if (projectId) conditions.push(eq(nodes.projectId, projectId));
|
|
110
|
+
const allOfType = await db.select().from(nodes).where(and(...conditions));
|
|
111
|
+
const duplicate = allOfType.find(n => n.name.toLowerCase() === name.toLowerCase());
|
|
112
|
+
if (duplicate) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`A ${type} node named "${name}" already exists (${duplicate.id})`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkSummaryParser — extracts structured work summary blocks from
|
|
3
|
+
* developer Claude output and git diff --name-status output.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface WorkSummary {
|
|
7
|
+
cycle: number;
|
|
8
|
+
filesCreated: string[];
|
|
9
|
+
filesUpdated: string[];
|
|
10
|
+
filesDeleted: string[];
|
|
11
|
+
approach: string;
|
|
12
|
+
decisions: string[];
|
|
13
|
+
timestamp: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SUMMARY_START = '## Work Summary';
|
|
17
|
+
const APPROACH_HEADER = '### Approach';
|
|
18
|
+
const DECISIONS_HEADER = '### Decisions';
|
|
19
|
+
|
|
20
|
+
export class WorkSummaryParser {
|
|
21
|
+
/**
|
|
22
|
+
* Parse the structured work summary block from developer Claude's output.
|
|
23
|
+
* Expected format:
|
|
24
|
+
* ## Work Summary
|
|
25
|
+
* ### Approach
|
|
26
|
+
* <paragraph>
|
|
27
|
+
* ### Decisions
|
|
28
|
+
* - <decision 1>
|
|
29
|
+
* - <decision 2>
|
|
30
|
+
*/
|
|
31
|
+
parseFromOutput = (output: string): { approach: string; decisions: string[] } => {
|
|
32
|
+
const summaryIdx = output.indexOf(SUMMARY_START);
|
|
33
|
+
if (summaryIdx === -1) {
|
|
34
|
+
return { approach: '', decisions: [] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const summaryBlock = output.slice(summaryIdx + SUMMARY_START.length);
|
|
38
|
+
|
|
39
|
+
// Extract approach
|
|
40
|
+
const approachIdx = summaryBlock.indexOf(APPROACH_HEADER);
|
|
41
|
+
const decisionsIdx = summaryBlock.indexOf(DECISIONS_HEADER);
|
|
42
|
+
|
|
43
|
+
let approach = '';
|
|
44
|
+
if (approachIdx !== -1) {
|
|
45
|
+
const approachStart = approachIdx + APPROACH_HEADER.length;
|
|
46
|
+
const approachEnd = decisionsIdx !== -1 ? decisionsIdx : summaryBlock.length;
|
|
47
|
+
approach = summaryBlock.slice(approachStart, approachEnd).trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Extract decisions
|
|
51
|
+
const decisions: string[] = [];
|
|
52
|
+
if (decisionsIdx !== -1) {
|
|
53
|
+
const decisionsBlock = summaryBlock.slice(decisionsIdx + DECISIONS_HEADER.length);
|
|
54
|
+
// Stop at the next ## heading or end of text
|
|
55
|
+
const nextHeadingIdx = decisionsBlock.indexOf('\n## ');
|
|
56
|
+
const decisionsText = nextHeadingIdx !== -1
|
|
57
|
+
? decisionsBlock.slice(0, nextHeadingIdx)
|
|
58
|
+
: decisionsBlock;
|
|
59
|
+
|
|
60
|
+
for (const line of decisionsText.split('\n')) {
|
|
61
|
+
const trimmed = line.trim();
|
|
62
|
+
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
|
|
63
|
+
decisions.push(trimmed.slice(2).trim());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { approach, decisions };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse git diff --name-status output into categorized file lists.
|
|
73
|
+
* Each line has a status letter (A/M/D) followed by a tab and the file path.
|
|
74
|
+
* Example input:
|
|
75
|
+
* A\tsrc/new_file.ts
|
|
76
|
+
* M\tsrc/existing.ts
|
|
77
|
+
* D\tsrc/removed.ts
|
|
78
|
+
*/
|
|
79
|
+
parseDiffNameStatus = (diffOutput: string): { filesCreated: string[]; filesUpdated: string[]; filesDeleted: string[] } => {
|
|
80
|
+
const filesCreated: string[] = [];
|
|
81
|
+
const filesUpdated: string[] = [];
|
|
82
|
+
const filesDeleted: string[] = [];
|
|
83
|
+
|
|
84
|
+
if (!diffOutput.trim()) {
|
|
85
|
+
return { filesCreated, filesUpdated, filesDeleted };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const line of diffOutput.trim().split('\n')) {
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
if (!trimmed) continue;
|
|
91
|
+
|
|
92
|
+
const status = trimmed[0];
|
|
93
|
+
const filePath = trimmed.slice(1).trim();
|
|
94
|
+
if (!filePath) continue;
|
|
95
|
+
|
|
96
|
+
switch (status) {
|
|
97
|
+
case 'A':
|
|
98
|
+
filesCreated.push(filePath);
|
|
99
|
+
break;
|
|
100
|
+
case 'D':
|
|
101
|
+
filesDeleted.push(filePath);
|
|
102
|
+
break;
|
|
103
|
+
case 'M':
|
|
104
|
+
default:
|
|
105
|
+
filesUpdated.push(filePath);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { filesCreated, filesUpdated, filesDeleted };
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Build a complete WorkSummary from cycle info, git diff --name-status, and Claude output.
|
|
115
|
+
*/
|
|
116
|
+
buildSummary = (cycle: number, diffNameStatusOutput: string, claudeOutput: string): WorkSummary => {
|
|
117
|
+
const { filesCreated, filesUpdated, filesDeleted } = this.parseDiffNameStatus(diffNameStatusOutput);
|
|
118
|
+
const { approach, decisions } = this.parseFromOutput(claudeOutput);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
cycle,
|
|
122
|
+
filesCreated,
|
|
123
|
+
filesUpdated,
|
|
124
|
+
filesDeleted,
|
|
125
|
+
approach: approach || 'No approach narrative provided.',
|
|
126
|
+
decisions,
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@interview-system/shared",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "echo 'Shared runs via tsx — no build step needed'",
|
|
9
|
+
"clean": "echo 'No build artifacts'",
|
|
10
|
+
"db:migrate": "tsx db/migrate.ts",
|
|
11
|
+
"db:rollback": "drizzle-kit rollback",
|
|
12
|
+
"db:generate": "drizzle-kit generate",
|
|
13
|
+
"db:assign-project": "npx tsx scripts/assign-project.ts --"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@libsql/client": "^0.17.0",
|
|
17
|
+
"chalk": "^5.3.0",
|
|
18
|
+
"commander": "^11.0.0",
|
|
19
|
+
"dotenv": "^17.3.1",
|
|
20
|
+
"drizzle-orm": "^0.45.1",
|
|
21
|
+
"glob": "11.1.0",
|
|
22
|
+
"gray-matter": "^4.0.3"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.3.3",
|
|
26
|
+
"drizzle-kit": "^0.31.9",
|
|
27
|
+
"tsx": "^4.21.0",
|
|
28
|
+
"typescript": "^5.9.3"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assign all rows with NULL project_id to a given project.
|
|
3
|
+
* Usage: npx tsx scripts/assign-project.ts <project_id>
|
|
4
|
+
* Example: npx tsx scripts/assign-project.ts proj_bcb96324
|
|
5
|
+
*/
|
|
6
|
+
import { config } from 'dotenv';
|
|
7
|
+
config({ path: '../../.env' });
|
|
8
|
+
|
|
9
|
+
import { createClient } from '@libsql/client';
|
|
10
|
+
|
|
11
|
+
const TABLES = [
|
|
12
|
+
'nodes',
|
|
13
|
+
'edges',
|
|
14
|
+
'kanban',
|
|
15
|
+
'coherence_reviews',
|
|
16
|
+
'pipeline_state',
|
|
17
|
+
'review_meta',
|
|
18
|
+
'sessions',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const projectId = process.argv[2];
|
|
22
|
+
if (!projectId) {
|
|
23
|
+
console.error('Usage: npx tsx scripts/assign-project.ts <project_id>');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
const client = createClient({
|
|
29
|
+
url: process.env.TURSO_DATABASE_URL!,
|
|
30
|
+
authToken: process.env.TURSO_AUTH_TOKEN!,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
console.log(`Assigning unassigned rows to project: ${projectId}\n`);
|
|
34
|
+
|
|
35
|
+
let total = 0;
|
|
36
|
+
for (const table of TABLES) {
|
|
37
|
+
const result = await client.execute({
|
|
38
|
+
sql: `UPDATE ${table} SET project_id = ? WHERE project_id IS NULL`,
|
|
39
|
+
args: [projectId],
|
|
40
|
+
});
|
|
41
|
+
console.log(` ${table}: ${result.rowsAffected} rows`);
|
|
42
|
+
total += result.rowsAffected;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
client.close();
|
|
46
|
+
console.log(`\nDone — ${total} total rows assigned.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main().catch((err) => {
|
|
50
|
+
console.error('Failed:', err);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* add_edge — Creates a typed relationship between two nodes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { program } from 'commander';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { readGraph, addEdge as graphAddEdge } from '../lib/graph.js';
|
|
10
|
+
import { readNode, writeNode, appendToSection } from '../lib/markdown.js';
|
|
11
|
+
import {
|
|
12
|
+
assertNodeExists,
|
|
13
|
+
assertValidRelation,
|
|
14
|
+
assertNoDuplicateEdge,
|
|
15
|
+
assertNoCycle,
|
|
16
|
+
} from '../lib/validator.js';
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.argument('<from>', 'Source node ID')
|
|
20
|
+
.argument('<relation>', 'Relation type')
|
|
21
|
+
.argument('<to>', 'Target node ID')
|
|
22
|
+
.requiredOption('--project-id <id>', 'Project ID')
|
|
23
|
+
.parse();
|
|
24
|
+
|
|
25
|
+
const [from, relation, to] = program.args;
|
|
26
|
+
const opts = program.opts();
|
|
27
|
+
|
|
28
|
+
(async () => {
|
|
29
|
+
try {
|
|
30
|
+
await assertNodeExists(from);
|
|
31
|
+
await assertNodeExists(to);
|
|
32
|
+
assertValidRelation(relation);
|
|
33
|
+
await assertNoDuplicateEdge(from, relation, to);
|
|
34
|
+
|
|
35
|
+
if (relation === 'depends_on') {
|
|
36
|
+
await assertNoCycle(from, to, opts.projectId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Add edge to DB
|
|
40
|
+
await graphAddEdge(from, relation, to, opts.projectId);
|
|
41
|
+
|
|
42
|
+
// Append relation references to both nodes' body content
|
|
43
|
+
const graph = await readGraph();
|
|
44
|
+
const fromNode = graph.nodes.find(n => n.id === from);
|
|
45
|
+
const toNode = graph.nodes.find(n => n.id === to);
|
|
46
|
+
|
|
47
|
+
const fromData = await readNode(from);
|
|
48
|
+
appendToSection(fromData.sections, 'Relations', `- ${relation} → ${to} (${toNode.name})`);
|
|
49
|
+
await writeNode(from, fromData.frontmatter, fromData.sections);
|
|
50
|
+
|
|
51
|
+
const toData = await readNode(to);
|
|
52
|
+
appendToSection(toData.sections, 'Relations', `- ${relation} ← ${from} (${fromNode.name})`);
|
|
53
|
+
await writeNode(to, toData.frontmatter, toData.sections);
|
|
54
|
+
|
|
55
|
+
console.log(chalk.green(`✓ Added edge: ${from} --${relation}--> ${to}`));
|
|
56
|
+
console.log(JSON.stringify({ from, relation, to }));
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
})();
|