@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,137 @@
|
|
|
1
|
+
import { sqliteTable, text, real, integer, primaryKey } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
|
|
3
|
+
// --- nodes table ---
|
|
4
|
+
export const nodes = sqliteTable('nodes', {
|
|
5
|
+
id: text('id').primaryKey(),
|
|
6
|
+
type: text('type').notNull(),
|
|
7
|
+
name: text('name').notNull(),
|
|
8
|
+
status: text('status').notNull(),
|
|
9
|
+
priority: text('priority').notNull(),
|
|
10
|
+
completeness: real('completeness').notNull().default(0),
|
|
11
|
+
openQuestionsCount: integer('open_questions_count').notNull().default(0),
|
|
12
|
+
kind: text('kind'),
|
|
13
|
+
createdAt: text('created_at').notNull(),
|
|
14
|
+
updatedAt: text('updated_at').notNull(),
|
|
15
|
+
body: text('body'),
|
|
16
|
+
projectId: text('project_id'),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// --- edges table ---
|
|
20
|
+
export const edges = sqliteTable('edges', {
|
|
21
|
+
fromId: text('from_id').notNull(),
|
|
22
|
+
relation: text('relation').notNull(),
|
|
23
|
+
toId: text('to_id').notNull(),
|
|
24
|
+
projectId: text('project_id'),
|
|
25
|
+
}, (table) => [
|
|
26
|
+
primaryKey({ columns: [table.fromId, table.relation, table.toId] }),
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
// --- kanban table ---
|
|
30
|
+
export const kanban = sqliteTable('kanban', {
|
|
31
|
+
nodeId: text('node_id').primaryKey().references(() => nodes.id),
|
|
32
|
+
columnName: text('column_name').notNull(),
|
|
33
|
+
position: integer('position').notNull(),
|
|
34
|
+
projectId: text('project_id'),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// --- sessions table ---
|
|
38
|
+
export const sessions = sqliteTable('sessions', {
|
|
39
|
+
sessionNumber: integer('session_number').primaryKey(),
|
|
40
|
+
startedAt: text('started_at').notNull(),
|
|
41
|
+
endedAt: text('ended_at'),
|
|
42
|
+
summary: text('summary'),
|
|
43
|
+
nodesTouched: text('nodes_touched'),
|
|
44
|
+
questionsResolved: integer('questions_resolved').notNull().default(0),
|
|
45
|
+
body: text('body'),
|
|
46
|
+
projectId: text('project_id'),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// --- coherence_reviews table ---
|
|
50
|
+
export const coherenceReviews = sqliteTable('coherence_reviews', {
|
|
51
|
+
id: text('id').primaryKey(),
|
|
52
|
+
type: text('type').notNull(), // add_edge, add_node, remove_edge, deprecate_node, update_description
|
|
53
|
+
fromId: text('from_id'),
|
|
54
|
+
toId: text('to_id'),
|
|
55
|
+
relation: text('relation'),
|
|
56
|
+
proposedNodeType: text('proposed_node_type'),
|
|
57
|
+
proposedNodeName: text('proposed_node_name'),
|
|
58
|
+
targetNodeId: text('target_node_id'), // for deprecate_node and update_description
|
|
59
|
+
proposedDescription: text('proposed_description'), // for update_description
|
|
60
|
+
confidence: text('confidence'), // high, medium, low
|
|
61
|
+
batchId: text('batch_id'), // groups related proposals
|
|
62
|
+
reasoning: text('reasoning').notNull(),
|
|
63
|
+
status: text('status').notNull(),
|
|
64
|
+
createdAt: text('created_at').notNull(),
|
|
65
|
+
resolvedAt: text('resolved_at'),
|
|
66
|
+
projectId: text('project_id'),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// --- review_meta table ---
|
|
70
|
+
export const reviewMeta = sqliteTable('review_meta', {
|
|
71
|
+
key: text('key').primaryKey(),
|
|
72
|
+
value: text('value').notNull(),
|
|
73
|
+
projectId: text('project_id'),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// --- users table ---
|
|
77
|
+
export const users = sqliteTable('users', {
|
|
78
|
+
id: text('id').primaryKey(),
|
|
79
|
+
email: text('email').notNull().unique(),
|
|
80
|
+
passwordHash: text('password_hash').notNull(),
|
|
81
|
+
role: text('role').notNull().default('user'),
|
|
82
|
+
createdAt: text('created_at').notNull(),
|
|
83
|
+
updatedAt: text('updated_at').notNull(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// --- refresh_tokens table ---
|
|
87
|
+
export const refreshTokens = sqliteTable('refresh_tokens', {
|
|
88
|
+
id: text('id').primaryKey(),
|
|
89
|
+
userId: text('user_id').notNull().references(() => users.id),
|
|
90
|
+
tokenHash: text('token_hash').notNull(),
|
|
91
|
+
expiresAt: text('expires_at').notNull(),
|
|
92
|
+
createdAt: text('created_at').notNull(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// --- password_reset_tokens table ---
|
|
96
|
+
export const passwordResetTokens = sqliteTable('password_reset_tokens', {
|
|
97
|
+
id: text('id').primaryKey(),
|
|
98
|
+
userId: text('user_id').notNull().references(() => users.id),
|
|
99
|
+
tokenHash: text('token_hash').notNull(),
|
|
100
|
+
expiresAt: text('expires_at').notNull(),
|
|
101
|
+
createdAt: text('created_at').notNull(),
|
|
102
|
+
usedAt: text('used_at'),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// --- invitations table ---
|
|
106
|
+
export const invitations = sqliteTable('invitations', {
|
|
107
|
+
id: text('id').primaryKey(),
|
|
108
|
+
email: text('email').notNull(),
|
|
109
|
+
tokenHash: text('token_hash').notNull(),
|
|
110
|
+
invitedBy: text('invited_by').notNull().references(() => users.id),
|
|
111
|
+
expiresAt: text('expires_at').notNull(),
|
|
112
|
+
acceptedAt: text('accepted_at'),
|
|
113
|
+
createdAt: text('created_at').notNull(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// --- projects table ---
|
|
117
|
+
export const projects = sqliteTable('projects', {
|
|
118
|
+
id: text('id').primaryKey(),
|
|
119
|
+
name: text('name').notNull(),
|
|
120
|
+
isDefault: integer('is_default').notNull().default(0),
|
|
121
|
+
archivedAt: text('archived_at'),
|
|
122
|
+
createdAt: text('created_at').notNull(),
|
|
123
|
+
updatedAt: text('updated_at').notNull(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// --- pipeline_state table ---
|
|
127
|
+
export const pipelineState = sqliteTable('pipeline_state', {
|
|
128
|
+
featureId: text('feature_id').primaryKey(),
|
|
129
|
+
status: text('status').notNull(),
|
|
130
|
+
cycle: integer('cycle').notNull().default(1),
|
|
131
|
+
tasksJson: text('tasks_json'),
|
|
132
|
+
toolCallsJson: text('tool_calls_json'),
|
|
133
|
+
workSummariesJson: text('work_summaries_json'),
|
|
134
|
+
error: text('error'),
|
|
135
|
+
updatedAt: text('updated_at').notNull(),
|
|
136
|
+
projectId: text('project_id'),
|
|
137
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from 'drizzle-kit';
|
|
2
|
+
import { config } from 'dotenv';
|
|
3
|
+
|
|
4
|
+
config({ path: '../../.env' });
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
schema: './db/schema.ts',
|
|
8
|
+
out: './db/migrations',
|
|
9
|
+
dialect: 'turso',
|
|
10
|
+
dbCredentials: {
|
|
11
|
+
url: process.env.TURSO_DATABASE_URL,
|
|
12
|
+
authToken: process.env.TURSO_AUTH_TOKEN,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude CLI Invocation Service.
|
|
3
|
+
* Encapsulates all Claude subprocess spawning and stream-json parsing.
|
|
4
|
+
* This is the mockable boundary for testing pipeline logic.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generic subprocess helper.
|
|
11
|
+
* @param {string} cmd - command to run
|
|
12
|
+
* @param {string[]} args - command arguments
|
|
13
|
+
* @param {string} cwd - working directory
|
|
14
|
+
* @param {function} log - logging function
|
|
15
|
+
* @returns {Promise<string>} stdout
|
|
16
|
+
*/
|
|
17
|
+
const spawnCommand = (cmd, args, cwd, log) => new Promise((resolve, reject) => {
|
|
18
|
+
const cmdStr = `${cmd} ${args.join(' ')}`;
|
|
19
|
+
log('SPAWN', `Running: ${cmdStr} (cwd: ${cwd})`);
|
|
20
|
+
const child = spawn(cmd, args, { cwd, env: process.env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
21
|
+
let stdout = '';
|
|
22
|
+
let stderr = '';
|
|
23
|
+
child.stdout.on('data', d => { stdout += d; });
|
|
24
|
+
child.stderr.on('data', d => { stderr += d; });
|
|
25
|
+
child.on('close', code => {
|
|
26
|
+
if (code === 0) {
|
|
27
|
+
log('SPAWN', `OK: ${cmdStr} (stdout: ${stdout.slice(0, 200).trim()})`);
|
|
28
|
+
resolve(stdout);
|
|
29
|
+
} else {
|
|
30
|
+
log('SPAWN', `FAIL: ${cmdStr} exit=${code} stderr=${stderr.slice(0, 300).trim()}`);
|
|
31
|
+
reject(new Error(`${cmd} exited with code ${code}: ${stderr}`));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
child.on('error', (err) => {
|
|
35
|
+
log('SPAWN', `ERROR: ${cmdStr} — ${err.message}`);
|
|
36
|
+
reject(err);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a stream-json line and log tool/text activity for verbose mode.
|
|
42
|
+
*/
|
|
43
|
+
const formatStreamEvent = (jsonStr, tag) => {
|
|
44
|
+
try {
|
|
45
|
+
const event = JSON.parse(jsonStr);
|
|
46
|
+
|
|
47
|
+
if (event.type === 'assistant' && event.message?.content) {
|
|
48
|
+
for (const block of event.message.content) {
|
|
49
|
+
if (block.type === 'text' && block.text) {
|
|
50
|
+
const preview = block.text.slice(0, 200).replace(/\n/g, '\\n');
|
|
51
|
+
process.stdout.write(` [${tag}] 💬 ${preview}${block.text.length > 200 ? '...' : ''}\n`);
|
|
52
|
+
}
|
|
53
|
+
if (block.type === 'tool_use') {
|
|
54
|
+
const name = block.name || 'unknown';
|
|
55
|
+
const input = block.input || {};
|
|
56
|
+
if (name === 'Read' || name === 'Glob') {
|
|
57
|
+
process.stdout.write(` [${tag}] 📖 ${name}: ${input.file_path || input.pattern || ''}\n`);
|
|
58
|
+
} else if (name === 'Edit') {
|
|
59
|
+
process.stdout.write(` [${tag}] ✏️ Edit: ${input.file_path || ''}\n`);
|
|
60
|
+
} else if (name === 'Write') {
|
|
61
|
+
process.stdout.write(` [${tag}] 📝 Write: ${input.file_path || ''}\n`);
|
|
62
|
+
} else if (name === 'Bash') {
|
|
63
|
+
const cmd = (input.command || '').slice(0, 120);
|
|
64
|
+
process.stdout.write(` [${tag}] 🔧 Bash: ${cmd}${(input.command || '').length > 120 ? '...' : ''}\n`);
|
|
65
|
+
} else if (name === 'Grep') {
|
|
66
|
+
process.stdout.write(` [${tag}] 🔍 Grep: "${input.pattern || ''}" ${input.path || ''}\n`);
|
|
67
|
+
} else if (name === 'Task') {
|
|
68
|
+
process.stdout.write(` [${tag}] 🤖 Task: ${input.description || input.prompt?.slice(0, 80) || ''}\n`);
|
|
69
|
+
} else {
|
|
70
|
+
process.stdout.write(` [${tag}] 🔧 ${name}\n`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (event.type === 'result') {
|
|
77
|
+
const cost = event.cost_usd != null ? ` ($${event.cost_usd.toFixed(4)})` : '';
|
|
78
|
+
const duration = event.duration_ms != null ? ` ${(event.duration_ms / 1000).toFixed(1)}s` : '';
|
|
79
|
+
process.stdout.write(` [${tag}] ✅ Done${duration}${cost}\n`);
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Not valid JSON — skip silently
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract the final text result from stream-json output.
|
|
88
|
+
*/
|
|
89
|
+
const extractResultText = (raw) => {
|
|
90
|
+
const lines = raw.split('\n');
|
|
91
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
92
|
+
const line = lines[i].trim();
|
|
93
|
+
if (!line) continue;
|
|
94
|
+
try {
|
|
95
|
+
const event = JSON.parse(line);
|
|
96
|
+
if (event.type === 'result' && event.result != null) {
|
|
97
|
+
return event.result;
|
|
98
|
+
}
|
|
99
|
+
} catch { /* skip */ }
|
|
100
|
+
}
|
|
101
|
+
let text = '';
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
try {
|
|
104
|
+
const event = JSON.parse(line.trim());
|
|
105
|
+
if (event.type === 'assistant' && event.message?.content) {
|
|
106
|
+
for (const block of event.message.content) {
|
|
107
|
+
if (block.type === 'text') text += block.text;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch { /* skip */ }
|
|
111
|
+
}
|
|
112
|
+
return text;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extract tool_use events from a stream-json line and invoke a callback.
|
|
117
|
+
* Only fires for tool_use blocks found in assistant messages.
|
|
118
|
+
*/
|
|
119
|
+
export const emitToolUseEvents = (jsonStr, callback) => {
|
|
120
|
+
try {
|
|
121
|
+
const event = JSON.parse(jsonStr);
|
|
122
|
+
if (event.type === 'assistant' && event.message?.content) {
|
|
123
|
+
for (const block of event.message.content) {
|
|
124
|
+
if (block.type === 'tool_use') {
|
|
125
|
+
callback(block.name || 'unknown', block.input || {});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Not valid JSON — skip silently
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a Claude CLI invocation service.
|
|
136
|
+
* @param {{ verbose: boolean, log: function }} opts
|
|
137
|
+
* @returns {{ spawnClaude: function, spawnCommand: function }}
|
|
138
|
+
*/
|
|
139
|
+
export const createClaudeService = ({ verbose = false, log: logFn }) => {
|
|
140
|
+
const spawnClaude = (prompt, cwd, label = 'claude', { onToolUse } = {}) => new Promise((resolve, reject) => {
|
|
141
|
+
const promptPreview = prompt.slice(0, 100).replace(/\n/g, '\\n');
|
|
142
|
+
logFn('CLAUDE', `Spawning ${label} in ${cwd} — prompt length: ${prompt.length} chars`);
|
|
143
|
+
logFn('CLAUDE', `Prompt preview: "${promptPreview}..."`);
|
|
144
|
+
|
|
145
|
+
const claudeArgs = ['--dangerously-skip-permissions', '-p', '-'];
|
|
146
|
+
if (verbose) {
|
|
147
|
+
claudeArgs.push('--verbose', '--output-format', 'stream-json');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const child = spawn('claude', claudeArgs, {
|
|
151
|
+
cwd,
|
|
152
|
+
env: process.env,
|
|
153
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
154
|
+
});
|
|
155
|
+
let stdout = '';
|
|
156
|
+
let stderr = '';
|
|
157
|
+
let lineBuf = '';
|
|
158
|
+
|
|
159
|
+
const tag = `${label.toUpperCase()}`;
|
|
160
|
+
|
|
161
|
+
child.stdout.on('data', d => {
|
|
162
|
+
const chunk = d.toString();
|
|
163
|
+
stdout += chunk;
|
|
164
|
+
if (verbose) {
|
|
165
|
+
lineBuf += chunk;
|
|
166
|
+
const lines = lineBuf.split('\n');
|
|
167
|
+
lineBuf = lines.pop();
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
if (line.trim()) {
|
|
170
|
+
formatStreamEvent(line.trim(), tag);
|
|
171
|
+
if (onToolUse) emitToolUseEvents(line.trim(), onToolUse);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
child.stderr.on('data', d => {
|
|
178
|
+
const chunk = d.toString();
|
|
179
|
+
stderr += chunk;
|
|
180
|
+
if (verbose) {
|
|
181
|
+
chunk.split('\n').forEach(line => {
|
|
182
|
+
if (line.trim()) process.stderr.write(` [${tag}:err] ${line}\n`);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
child.stdin.write(prompt);
|
|
188
|
+
child.stdin.end();
|
|
189
|
+
logFn('CLAUDE', `Prompt written to stdin, waiting for ${label} to complete...`);
|
|
190
|
+
|
|
191
|
+
child.on('close', code => {
|
|
192
|
+
if (verbose && lineBuf.trim()) {
|
|
193
|
+
formatStreamEvent(lineBuf.trim(), tag);
|
|
194
|
+
if (onToolUse) emitToolUseEvents(lineBuf.trim(), onToolUse);
|
|
195
|
+
}
|
|
196
|
+
if (code === 0) {
|
|
197
|
+
const result = verbose ? extractResultText(stdout) : stdout;
|
|
198
|
+
logFn('CLAUDE', `${label} OK — result length: ${result.length} chars`);
|
|
199
|
+
resolve(result);
|
|
200
|
+
} else {
|
|
201
|
+
logFn('CLAUDE', `${label} FAIL exit=${code} stderr=${stderr.slice(0, 500).trim()}`);
|
|
202
|
+
reject(new Error(`claude exited with code ${code}: ${stderr}`));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
child.on('error', (err) => {
|
|
206
|
+
logFn('CLAUDE', `${label} ERROR: ${err.message}`);
|
|
207
|
+
reject(err);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
spawnClaude,
|
|
213
|
+
spawnCommand: (cmd, args, cwd) => spawnCommand(cmd, args, cwd, logFn),
|
|
214
|
+
};
|
|
215
|
+
};
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coherence review operations backed by Drizzle/Turso.
|
|
3
|
+
* Supports 5 proposal types: add_edge, add_node, remove_edge, deprecate_node, update_description.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { eq } from 'drizzle-orm';
|
|
7
|
+
import { getDb } from './db.js';
|
|
8
|
+
import { coherenceReviews, reviewMeta } from '../db/schema.js';
|
|
9
|
+
|
|
10
|
+
const META_KEY = 'coherence:state';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Valid proposal types ordered by processing priority.
|
|
14
|
+
* remove actions first (cascading), then updates, then additions.
|
|
15
|
+
*/
|
|
16
|
+
export const PROPOSAL_TYPE_ORDER = [
|
|
17
|
+
'deprecate_node',
|
|
18
|
+
'remove_edge',
|
|
19
|
+
'update_description',
|
|
20
|
+
'add_edge',
|
|
21
|
+
'add_node',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Map a DB row to the API proposal shape.
|
|
26
|
+
*/
|
|
27
|
+
const rowToProposal = (r) => ({
|
|
28
|
+
id: r.id,
|
|
29
|
+
type: r.type,
|
|
30
|
+
...(r.fromId ? { from_id: r.fromId } : {}),
|
|
31
|
+
...(r.toId ? { to_id: r.toId } : {}),
|
|
32
|
+
...(r.relation ? { relation: r.relation } : {}),
|
|
33
|
+
...(r.proposedNodeType ? { proposed_node_type: r.proposedNodeType } : {}),
|
|
34
|
+
...(r.proposedNodeName ? { proposed_node_name: r.proposedNodeName } : {}),
|
|
35
|
+
...(r.targetNodeId ? { target_node_id: r.targetNodeId } : {}),
|
|
36
|
+
...(r.proposedDescription ? { proposed_description: r.proposedDescription } : {}),
|
|
37
|
+
...(r.confidence ? { confidence: r.confidence } : {}),
|
|
38
|
+
...(r.batchId ? { batch_id: r.batchId } : {}),
|
|
39
|
+
reasoning: r.reasoning,
|
|
40
|
+
status: r.status,
|
|
41
|
+
created_at: r.createdAt,
|
|
42
|
+
resolved_at: r.resolvedAt,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read coherence review state and proposals from the database.
|
|
47
|
+
* Returns: { last_review_timestamp, review_status, partial_run, proposals[] }
|
|
48
|
+
*/
|
|
49
|
+
export const loadCoherence = async (projectId?: string) => {
|
|
50
|
+
const db = getDb();
|
|
51
|
+
|
|
52
|
+
const metaKey = projectId ? `${META_KEY}:${projectId}` : META_KEY;
|
|
53
|
+
const metaRows = await db.select().from(reviewMeta)
|
|
54
|
+
.where(eq(reviewMeta.key, metaKey));
|
|
55
|
+
const meta = metaRows[0]
|
|
56
|
+
? JSON.parse(metaRows[0].value)
|
|
57
|
+
: { last_review_timestamp: null, review_status: 'idle', partial_run: false };
|
|
58
|
+
|
|
59
|
+
const rows = projectId
|
|
60
|
+
? await db.select().from(coherenceReviews).where(eq(coherenceReviews.projectId, projectId))
|
|
61
|
+
: await db.select().from(coherenceReviews);
|
|
62
|
+
const proposals = rows.map(rowToProposal);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
last_review_timestamp: meta.last_review_timestamp,
|
|
66
|
+
review_status: meta.review_status,
|
|
67
|
+
partial_run: meta.partial_run || false,
|
|
68
|
+
proposals,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Save coherence review state and proposals to the database.
|
|
74
|
+
*/
|
|
75
|
+
export const saveCoherence = async (data, projectId?: string) => {
|
|
76
|
+
const db = getDb();
|
|
77
|
+
|
|
78
|
+
const metaKey = projectId ? `${META_KEY}:${projectId}` : META_KEY;
|
|
79
|
+
const metaValue = JSON.stringify({
|
|
80
|
+
last_review_timestamp: data.last_review_timestamp,
|
|
81
|
+
review_status: data.review_status,
|
|
82
|
+
partial_run: data.partial_run || false,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const existing = await db.select().from(reviewMeta)
|
|
86
|
+
.where(eq(reviewMeta.key, metaKey));
|
|
87
|
+
if (existing[0]) {
|
|
88
|
+
await db.update(reviewMeta).set({ value: metaValue })
|
|
89
|
+
.where(eq(reviewMeta.key, metaKey));
|
|
90
|
+
} else {
|
|
91
|
+
await db.insert(reviewMeta).values({ key: metaKey, value: metaValue, projectId: projectId || null });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Sync proposals — delete all and re-insert
|
|
95
|
+
if (projectId) {
|
|
96
|
+
await db.delete(coherenceReviews).where(eq(coherenceReviews.projectId, projectId));
|
|
97
|
+
} else {
|
|
98
|
+
await db.delete(coherenceReviews);
|
|
99
|
+
}
|
|
100
|
+
for (const p of data.proposals) {
|
|
101
|
+
await db.insert(coherenceReviews).values({
|
|
102
|
+
id: p.id,
|
|
103
|
+
type: p.type,
|
|
104
|
+
fromId: p.from_id || null,
|
|
105
|
+
toId: p.to_id || null,
|
|
106
|
+
relation: p.relation || null,
|
|
107
|
+
proposedNodeType: p.proposed_node_type || null,
|
|
108
|
+
proposedNodeName: p.proposed_node_name || null,
|
|
109
|
+
targetNodeId: p.target_node_id || null,
|
|
110
|
+
proposedDescription: p.proposed_description || null,
|
|
111
|
+
confidence: p.confidence || null,
|
|
112
|
+
batchId: p.batch_id || null,
|
|
113
|
+
reasoning: p.reasoning,
|
|
114
|
+
status: p.status,
|
|
115
|
+
createdAt: p.created_at,
|
|
116
|
+
resolvedAt: p.resolved_at || null,
|
|
117
|
+
projectId: projectId || null,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get a single proposal by ID.
|
|
124
|
+
*/
|
|
125
|
+
export const getProposal = async (proposalId) => {
|
|
126
|
+
const db = getDb();
|
|
127
|
+
const rows = await db.select().from(coherenceReviews)
|
|
128
|
+
.where(eq(coherenceReviews.id, proposalId));
|
|
129
|
+
if (!rows[0]) return null;
|
|
130
|
+
return rowToProposal(rows[0]);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Update a proposal's status.
|
|
135
|
+
*/
|
|
136
|
+
export const updateProposalStatus = async (proposalId, status, resolvedAt = null) => {
|
|
137
|
+
const db = getDb();
|
|
138
|
+
const updates = { status };
|
|
139
|
+
if (resolvedAt) updates.resolvedAt = resolvedAt;
|
|
140
|
+
await db.update(coherenceReviews).set(updates)
|
|
141
|
+
.where(eq(coherenceReviews.id, proposalId));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Update coherence state metadata (last_review_timestamp, review_status, partial_run).
|
|
146
|
+
*/
|
|
147
|
+
export const updateCoherenceState = async (state) => {
|
|
148
|
+
const db = getDb();
|
|
149
|
+
const metaValue = JSON.stringify(state);
|
|
150
|
+
|
|
151
|
+
const existing = await db.select().from(reviewMeta)
|
|
152
|
+
.where(eq(reviewMeta.key, META_KEY));
|
|
153
|
+
if (existing[0]) {
|
|
154
|
+
await db.update(reviewMeta).set({ value: metaValue })
|
|
155
|
+
.where(eq(reviewMeta.key, META_KEY));
|
|
156
|
+
} else {
|
|
157
|
+
await db.insert(reviewMeta).values({ key: META_KEY, value: metaValue });
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Add new proposals to the database.
|
|
163
|
+
*/
|
|
164
|
+
export const addProposals = async (proposals, projectId?: string) => {
|
|
165
|
+
const db = getDb();
|
|
166
|
+
for (const p of proposals) {
|
|
167
|
+
await db.insert(coherenceReviews).values({
|
|
168
|
+
id: p.id,
|
|
169
|
+
type: p.type,
|
|
170
|
+
fromId: p.from_id || null,
|
|
171
|
+
toId: p.to_id || null,
|
|
172
|
+
relation: p.relation || null,
|
|
173
|
+
proposedNodeType: p.proposed_node_type || null,
|
|
174
|
+
proposedNodeName: p.proposed_node_name || null,
|
|
175
|
+
targetNodeId: p.target_node_id || null,
|
|
176
|
+
proposedDescription: p.proposed_description || null,
|
|
177
|
+
confidence: p.confidence || null,
|
|
178
|
+
batchId: p.batch_id || null,
|
|
179
|
+
reasoning: p.reasoning,
|
|
180
|
+
status: p.status,
|
|
181
|
+
createdAt: p.created_at,
|
|
182
|
+
resolvedAt: p.resolved_at || null,
|
|
183
|
+
projectId: projectId || null,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build a dismissal key for a proposal to prevent re-proposing.
|
|
190
|
+
* Uses exact tuple matching as specified.
|
|
191
|
+
*/
|
|
192
|
+
export const getDismissalKey = (proposal) => {
|
|
193
|
+
switch (proposal.type) {
|
|
194
|
+
case 'add_edge':
|
|
195
|
+
case 'remove_edge':
|
|
196
|
+
return `${proposal.type}:${proposal.from_id}:${proposal.relation}:${proposal.to_id}`;
|
|
197
|
+
case 'add_node':
|
|
198
|
+
return `${proposal.type}:${proposal.proposed_node_type}:${proposal.proposed_node_name}`;
|
|
199
|
+
case 'deprecate_node':
|
|
200
|
+
return `${proposal.type}:${proposal.target_node_id}`;
|
|
201
|
+
case 'update_description':
|
|
202
|
+
return `${proposal.type}:${proposal.target_node_id}`;
|
|
203
|
+
default:
|
|
204
|
+
return `${proposal.type}:${proposal.id}`;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Sort proposals by type priority order, then by batch.
|
|
210
|
+
*/
|
|
211
|
+
export const sortProposals = (proposals) => {
|
|
212
|
+
return [...proposals].sort((a, b) => {
|
|
213
|
+
const aIdx = PROPOSAL_TYPE_ORDER.indexOf(a.type);
|
|
214
|
+
const bIdx = PROPOSAL_TYPE_ORDER.indexOf(b.type);
|
|
215
|
+
if (aIdx !== bIdx) return aIdx - bIdx;
|
|
216
|
+
// Within same type, keep batch members together
|
|
217
|
+
if (a.batch_id && b.batch_id && a.batch_id === b.batch_id) return 0;
|
|
218
|
+
if (a.batch_id && !b.batch_id) return -1;
|
|
219
|
+
if (!a.batch_id && b.batch_id) return 1;
|
|
220
|
+
return 0;
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Detect conflicting proposals.
|
|
226
|
+
* Returns a Map<proposalId, Set<conflictingProposalId>>.
|
|
227
|
+
*/
|
|
228
|
+
export const detectConflicts = (proposals) => {
|
|
229
|
+
const conflicts = new Map();
|
|
230
|
+
const pending = proposals.filter(p => p.status === 'pending');
|
|
231
|
+
|
|
232
|
+
// Nodes being deprecated
|
|
233
|
+
const deprecatedNodeIds = new Set(
|
|
234
|
+
pending.filter(p => p.type === 'deprecate_node').map(p => p.target_node_id)
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
for (const p of pending) {
|
|
238
|
+
const pConflicts = new Set();
|
|
239
|
+
|
|
240
|
+
if (p.type === 'add_edge') {
|
|
241
|
+
// Conflict: add edge to/from a node being deprecated
|
|
242
|
+
if (deprecatedNodeIds.has(p.from_id) || deprecatedNodeIds.has(p.to_id)) {
|
|
243
|
+
const deprecator = pending.find(d =>
|
|
244
|
+
d.type === 'deprecate_node' &&
|
|
245
|
+
(d.target_node_id === p.from_id || d.target_node_id === p.to_id)
|
|
246
|
+
);
|
|
247
|
+
if (deprecator) pConflicts.add(deprecator.id);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (p.type === 'remove_edge') {
|
|
252
|
+
// Conflict: removing an edge while also adding the same edge
|
|
253
|
+
const addDup = pending.find(a =>
|
|
254
|
+
a.type === 'add_edge' &&
|
|
255
|
+
a.from_id === p.from_id && a.relation === p.relation && a.to_id === p.to_id
|
|
256
|
+
);
|
|
257
|
+
if (addDup) pConflicts.add(addDup.id);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (p.type === 'update_description' && deprecatedNodeIds.has(p.target_node_id)) {
|
|
261
|
+
const deprecator = pending.find(d =>
|
|
262
|
+
d.type === 'deprecate_node' && d.target_node_id === p.target_node_id
|
|
263
|
+
);
|
|
264
|
+
if (deprecator) pConflicts.add(deprecator.id);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (pConflicts.size > 0) {
|
|
268
|
+
conflicts.set(p.id, pConflicts);
|
|
269
|
+
// Add reverse conflicts
|
|
270
|
+
for (const cId of pConflicts) {
|
|
271
|
+
if (!conflicts.has(cId)) conflicts.set(cId, new Set());
|
|
272
|
+
conflicts.get(cId).add(p.id);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return conflicts;
|
|
278
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Completeness scoring for nodes.
|
|
3
|
+
* Calculates a 0–1 score based on the node type's scoring rules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCORING_RULES } from './constants.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Score a node's completeness from 0 to 1 based on its type and current content.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} type - The node type (e.g., 'feature', 'component')
|
|
12
|
+
* @param {object} frontmatter - Parsed frontmatter object
|
|
13
|
+
* @param {object} sections - Map of section name → content string
|
|
14
|
+
* @returns {number} Score between 0 and 1, rounded to 2 decimal places
|
|
15
|
+
*/
|
|
16
|
+
export function scoreNode(type, frontmatter, sections) {
|
|
17
|
+
const rules = SCORING_RULES[type];
|
|
18
|
+
if (!rules) {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let score = 0;
|
|
23
|
+
for (const rule of rules) {
|
|
24
|
+
if (rule.check(frontmatter, sections)) {
|
|
25
|
+
score += rule.weight;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Math.round(score * 100) / 100;
|
|
30
|
+
}
|