@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,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
+ })();