@hupan56/wlkj 2.6.0 → 2.7.1

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 (125) hide show
  1. package/bin/cli.js +269 -11
  2. package/package.json +1 -1
  3. package/templates/qoder/agents/insight-planning.md +67 -0
  4. package/templates/qoder/agents/insight-research.md +61 -0
  5. package/templates/qoder/agents/prd-reference.md +8 -2
  6. package/templates/qoder/commands/optional/wl-insight.md +275 -0
  7. package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
  8. package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
  9. package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
  10. package/templates/qoder/commands/wl-code.md +10 -2
  11. package/templates/qoder/commands/wl-commit.md +1 -1
  12. package/templates/qoder/commands/wl-design-draw.md +78 -0
  13. package/templates/qoder/commands/wl-design-scan.md +108 -0
  14. package/templates/qoder/commands/wl-design-spec.md +154 -0
  15. package/templates/qoder/commands/wl-design.md +32 -0
  16. package/templates/qoder/commands/wl-init.md +24 -3
  17. package/templates/qoder/commands/wl-prd-full.md +226 -0
  18. package/templates/qoder/commands/wl-prd-quick.md +134 -0
  19. package/templates/qoder/commands/wl-prd-review.md +104 -0
  20. package/templates/qoder/commands/wl-prd.md +17 -311
  21. package/templates/qoder/commands/wl-search.md +66 -30
  22. package/templates/qoder/commands/wl-task.md +290 -59
  23. package/templates/qoder/commands/wl-test.md +92 -24
  24. package/templates/qoder/config.yaml +51 -15
  25. package/templates/qoder/hooks/inject-workflow-state.py +2 -2
  26. package/templates/qoder/hooks/session-start.py +82 -51
  27. package/templates/qoder/rules/wl-pipeline.md +216 -105
  28. package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
  29. package/templates/qoder/scripts/archive_prd.py +377 -0
  30. package/templates/qoder/scripts/autotest.py +1715 -0
  31. package/templates/qoder/scripts/autotest_batch.py +224 -0
  32. package/templates/qoder/scripts/autotest_run.py +297 -0
  33. package/templates/qoder/scripts/benchmark.py +210 -209
  34. package/templates/qoder/scripts/build_style_index.py +444 -4
  35. package/templates/qoder/scripts/check_carriers.py +238 -0
  36. package/templates/qoder/scripts/check_mcp.py +298 -0
  37. package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
  38. package/templates/qoder/scripts/common/events.py +46 -0
  39. package/templates/qoder/scripts/common/extract.py +419 -0
  40. package/templates/qoder/scripts/common/graph_traverse.py +533 -0
  41. package/templates/qoder/scripts/common/paths.py +89 -0
  42. package/templates/qoder/scripts/common/pip_install.py +144 -0
  43. package/templates/qoder/scripts/common/platform_guard.py +61 -0
  44. package/templates/qoder/scripts/common/search_engine.py +205 -205
  45. package/templates/qoder/scripts/common/terms.py +57 -0
  46. package/templates/qoder/scripts/common/ts_extract.py +536 -0
  47. package/templates/qoder/scripts/context_pack.py +73 -13
  48. package/templates/qoder/scripts/enrich_prompt.py +226 -0
  49. package/templates/qoder/scripts/eval_prd.py +318 -231
  50. package/templates/qoder/scripts/export.py +487 -487
  51. package/templates/qoder/scripts/extract_api_params.py +246 -0
  52. package/templates/qoder/scripts/extract_routes.py +54 -0
  53. package/templates/qoder/scripts/extract_routes_tree.py +78 -0
  54. package/templates/qoder/scripts/fill_prototype.py +707 -0
  55. package/templates/qoder/scripts/gen_design_doc.py +394 -0
  56. package/templates/qoder/scripts/git_sync.py +27 -15
  57. package/templates/qoder/scripts/init_doctor.py +292 -40
  58. package/templates/qoder/scripts/install_qoderwork.py +366 -9
  59. package/templates/qoder/scripts/kg.py +708 -0
  60. package/templates/qoder/scripts/kg_auto_login.py +196 -0
  61. package/templates/qoder/scripts/kg_build.py +612 -0
  62. package/templates/qoder/scripts/kg_build_db.py +327 -0
  63. package/templates/qoder/scripts/kg_duckdb.py +549 -0
  64. package/templates/qoder/scripts/kg_incremental.py +393 -0
  65. package/templates/qoder/scripts/kg_link_db.py +224 -0
  66. package/templates/qoder/scripts/kg_mcp_server.py +801 -0
  67. package/templates/qoder/scripts/kg_semantic.py +150 -0
  68. package/templates/qoder/scripts/kg_test_runner.py +241 -0
  69. package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
  70. package/templates/qoder/scripts/learn.py +118 -39
  71. package/templates/qoder/scripts/learn_aggregate.py +201 -0
  72. package/templates/qoder/scripts/mcp_launcher.py +359 -0
  73. package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
  74. package/templates/qoder/scripts/repo_root.py +106 -0
  75. package/templates/qoder/scripts/role.py +12 -0
  76. package/templates/qoder/scripts/run_weekly_update.bat +5 -0
  77. package/templates/qoder/scripts/run_weekly_update.sh +5 -0
  78. package/templates/qoder/scripts/search_index.py +307 -60
  79. package/templates/qoder/scripts/secure-ls.js +5640 -0
  80. package/templates/qoder/scripts/setup.py +706 -641
  81. package/templates/qoder/scripts/setup_lanhu.py +963 -0
  82. package/templates/qoder/scripts/status.py +250 -11
  83. package/templates/qoder/scripts/sync_carriers.py +259 -0
  84. package/templates/qoder/scripts/syncgate.py +2 -2
  85. package/templates/qoder/scripts/task.py +75 -0
  86. package/templates/qoder/scripts/team_sync.py +60 -4
  87. package/templates/qoder/scripts/workspace_init.py +1 -1
  88. package/templates/qoder/skills/design-import/SKILL.md +226 -0
  89. package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
  90. package/templates/qoder/skills/design-review/SKILL.md +82 -25
  91. package/templates/qoder/skills/prd-generator/SKILL.md +184 -60
  92. package/templates/qoder/skills/prd-review/SKILL.md +18 -1
  93. package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
  94. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
  95. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  96. package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
  97. package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
  98. package/templates/qoder/skills/test-generator/SKILL.md +15 -2
  99. package/templates/qoder/skills/wl-code/SKILL.md +55 -36
  100. package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
  101. package/templates/qoder/skills/wl-design/SKILL.md +55 -0
  102. package/templates/qoder/skills/wl-init/SKILL.md +76 -67
  103. package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
  104. package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
  105. package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
  106. package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
  107. package/templates/qoder/skills/wl-report/SKILL.md +131 -107
  108. package/templates/qoder/skills/wl-search/SKILL.md +141 -75
  109. package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
  110. package/templates/qoder/skills/wl-status/SKILL.md +83 -61
  111. package/templates/qoder/skills/wl-task/SKILL.md +132 -58
  112. package/templates/qoder/skills/wl-test/SKILL.md +406 -40
  113. package/templates/qoder/templates/prototype-app.html +13 -8
  114. package/templates/qoder/templates/prototype-web.html +376 -93
  115. package/templates/root/AGENTS.md +89 -34
  116. package/templates/root/requirements.txt +21 -0
  117. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
  118. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
  119. package/templates/qoder/agents/prd-planning.md +0 -57
  120. package/templates/qoder/agents/prd-research.md +0 -34
  121. package/templates/qoder/commands/wl-insight.md +0 -51
  122. package/templates/qoder/scripts/__pycache__/init_doctor.cpython-39.pyc +0 -0
  123. package/templates/qoder/scripts/__pycache__/setup.cpython-39.pyc +0 -0
  124. package/templates/qoder/scripts/common/__pycache__/developer.cpython-39.pyc +0 -0
  125. package/templates/qoder/skills/wl-prd/SKILL.md +0 -92
@@ -0,0 +1,549 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ DuckDB knowledge graph store — replaces the JSON flat files with a single
4
+ embedded database for fast incremental updates.
5
+
6
+ Why DuckDB (not JSON):
7
+ - Weekly 1000+ commits need incremental UPSERT, not full rebuild
8
+ - Single-file storage (kg.duckdb) with ACID transactions
9
+ - Recursive CTE for multi-hop graph traversal (replaces Python BFS)
10
+ - pip install, no server, PM-friendly
11
+
12
+ Schema:
13
+ entities(id TEXT PK, type TEXT, canonical TEXT, cn TEXT, props JSON)
14
+ aliases(alias TEXT PK, entity_id TEXT)
15
+ edges(from_id TEXT, to_id TEXT, edge_type TEXT, seq INT, props JSON)
16
+ build_meta(key TEXT PK, value TEXT)
17
+ """
18
+ import json
19
+ import os
20
+ import sys
21
+
22
+ import duckdb
23
+
24
+ SCRIPTS = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.qoder', 'scripts')
25
+ sys.path.insert(0, SCRIPTS)
26
+ from common.paths import DATA_INDEX_DIR
27
+
28
+ DB_PATH = str(DATA_INDEX_DIR / 'kg.duckdb')
29
+
30
+
31
+ def get_db():
32
+ """Open DuckDB connection (creates file if not exists)."""
33
+ os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
34
+ return duckdb.connect(DB_PATH)
35
+
36
+
37
+ def init_schema(con):
38
+ """Create tables if not exist."""
39
+ con.execute("""
40
+ CREATE TABLE IF NOT EXISTS entities (
41
+ id TEXT PRIMARY KEY,
42
+ type TEXT NOT NULL,
43
+ canonical TEXT NOT NULL,
44
+ cn TEXT DEFAULT '',
45
+ props JSON
46
+ )
47
+ """)
48
+ con.execute("""
49
+ CREATE TABLE IF NOT EXISTS aliases (
50
+ alias TEXT PRIMARY KEY,
51
+ entity_id TEXT NOT NULL
52
+ )
53
+ """)
54
+ con.execute("""
55
+ CREATE TABLE IF NOT EXISTS edges (
56
+ from_id TEXT NOT NULL,
57
+ to_id TEXT NOT NULL,
58
+ edge_type TEXT NOT NULL,
59
+ seq INTEGER DEFAULT 0,
60
+ props JSON
61
+ )
62
+ """)
63
+ con.execute("""
64
+ CREATE TABLE IF NOT EXISTS build_meta (
65
+ key TEXT PRIMARY KEY,
66
+ value TEXT
67
+ )
68
+ """)
69
+ # ── 搜索层表 ──
70
+ con.execute("CREATE TABLE IF NOT EXISTS keywords(keyword TEXT, file_path TEXT)")
71
+ con.execute("CREATE TABLE IF NOT EXISTS endpoints(endpoint TEXT PRIMARY KEY, controller_file TEXT)")
72
+ con.execute("CREATE TABLE IF NOT EXISTS prds(file TEXT PRIMARY KEY, title TEXT, keywords JSON, features JSON, cn_terms JSON)")
73
+ con.execute("CREATE TABLE IF NOT EXISTS wiki(keyword TEXT, project TEXT, title TEXT, md_path TEXT, description TEXT)")
74
+ # ── UI层表 ──
75
+ con.execute("CREATE TABLE IF NOT EXISTS pages(file TEXT PRIMARY KEY, project TEXT, page_type TEXT, components JSON, table_columns JSON, form_fields JSON, layout JSON)")
76
+ con.execute("CREATE TABLE IF NOT EXISTS fields(field TEXT, title TEXT, count INTEGER, sample_files JSON)")
77
+ con.execute("CREATE TABLE IF NOT EXISTS modules(dir TEXT, cn TEXT, project TEXT, path TEXT)")
78
+ # ── trace层表 ──
79
+ con.execute("CREATE TABLE IF NOT EXISTS traces(vue_file TEXT, button TEXT, handler TEXT, api_fn TEXT, endpoint TEXT, verb TEXT, confidence TEXT)")
80
+ con.execute("CREATE TABLE IF NOT EXISTS api_functions(fn_name TEXT PRIMARY KEY, url TEXT, verb TEXT, source_file TEXT)")
81
+ # ── test层表 ──
82
+ con.execute("CREATE TABLE IF NOT EXISTS tests(class TEXT, method TEXT, package TEXT, display_name TEXT, injected_deps JSON, assertions JSON, is_integration BOOLEAN)")
83
+ # ── workflow层表 ──
84
+ con.execute("CREATE TABLE IF NOT EXISTS workflows(module TEXT, state TEXT, seq INTEGER, button TEXT, endpoint TEXT)")
85
+ # ── style层表 ──
86
+ con.execute("CREATE TABLE IF NOT EXISTS style_meta(project TEXT PRIMARY KEY, stack TEXT, layout TEXT, access_mode TEXT, sidebar_width INTEGER, color_primary TEXT, color_success TEXT, color_warning TEXT, color_destructive TEXT, radius TEXT, vue_files INTEGER, common_components JSON, form_components JSON)")
87
+ # ── learning层表 (团队学习聚合, 同构代码图谱) ──
88
+ # 埋点事件 (每人写 journal/feedback.jsonl, 管理员聚合到此)
89
+ con.execute("""CREATE TABLE IF NOT EXISTS learning_events (
90
+ id TEXT PRIMARY KEY,
91
+ ts TEXT NOT NULL,
92
+ dev TEXT NOT NULL,
93
+ event TEXT NOT NULL,
94
+ data JSON
95
+ )""")
96
+ # 提炼后的模式 (团队共享, learn_aggregate 生成)
97
+ con.execute("""CREATE TABLE IF NOT EXISTS learning_patterns (
98
+ category TEXT NOT NULL,
99
+ pattern TEXT NOT NULL,
100
+ frequency INTEGER DEFAULT 1,
101
+ first_seen TEXT,
102
+ last_seen TEXT,
103
+ source_devs JSON
104
+ )""")
105
+ # 开发者统计快照 (聚合时刷新)
106
+ con.execute("""CREATE TABLE IF NOT EXISTS learning_stats (
107
+ dev TEXT PRIMARY KEY,
108
+ prds_generated INTEGER DEFAULT 0,
109
+ rejections INTEGER DEFAULT 0,
110
+ reviews_done INTEGER DEFAULT 0,
111
+ tasks_completed INTEGER DEFAULT 0,
112
+ commits_done INTEGER DEFAULT 0,
113
+ updated_at TEXT
114
+ )""")
115
+ # 自动补列: events.py 注册的新统计列, 表里没有就加 (ALTER ... ADD COLUMN IF NOT EXISTS)
116
+ # 这样 events.py 加埋点不用手动改表结构。
117
+ try:
118
+ from common.events import event_to_sql_filter
119
+ existing = {r[0] for r in con.execute(
120
+ "SELECT column_name FROM information_schema.columns "
121
+ "WHERE table_name = 'learning_stats'"
122
+ ).fetchall()}
123
+ for stat_col in event_to_sql_filter().keys():
124
+ if stat_col not in existing:
125
+ con.execute(
126
+ "ALTER TABLE learning_stats ADD COLUMN IF NOT EXISTS %s INTEGER DEFAULT 0"
127
+ % stat_col
128
+ )
129
+ except Exception:
130
+ pass # events.py 不可用 (循环导入等) = 退化为固定 5 列, 不影响
131
+ # Indexes
132
+ con.execute("CREATE INDEX IF NOT EXISTS idx_ent_type ON entities(type)")
133
+ con.execute("CREATE INDEX IF NOT EXISTS idx_alias ON aliases(alias)")
134
+ con.execute("CREATE INDEX IF NOT EXISTS idx_edge_from ON edges(from_id)")
135
+ con.execute("CREATE INDEX IF NOT EXISTS idx_edge_to ON edges(to_id)")
136
+ con.execute("CREATE INDEX IF NOT EXISTS idx_kw ON keywords(keyword)")
137
+ con.execute("CREATE INDEX IF NOT EXISTS idx_page_type ON pages(page_type)")
138
+ con.execute("CREATE INDEX IF NOT EXISTS idx_field ON fields(field)")
139
+ con.execute("CREATE INDEX IF NOT EXISTS idx_trace_ep ON traces(endpoint)")
140
+ con.execute("CREATE INDEX IF NOT EXISTS idx_trace_vue ON traces(vue_file)")
141
+ con.execute("CREATE INDEX IF NOT EXISTS idx_wiki_kw ON wiki(keyword)")
142
+ con.execute("CREATE INDEX IF NOT EXISTS idx_learn_event ON learning_events(event)")
143
+ con.execute("CREATE INDEX IF NOT EXISTS idx_learn_dev ON learning_events(dev)")
144
+ con.execute("CREATE INDEX IF NOT EXISTS idx_learn_pat_cat ON learning_patterns(category)")
145
+ con.execute("CREATE INDEX IF NOT EXISTS idx_wf_mod ON workflows(module)")
146
+
147
+
148
+ def _sql_escape(val):
149
+ """Escape a value for SQL string literal."""
150
+ return str(val).replace("'", "''")
151
+
152
+
153
+ def _batch_insert(con, table, rows, chunk_size=500, ignore=False):
154
+ """Batch insert rows using VALUES SQL. ignore=True for OR IGNORE (PK tables)."""
155
+ prefix = "INSERT OR IGNORE INTO " if ignore else "INSERT INTO "
156
+ for i in range(0, len(rows), chunk_size):
157
+ chunk = rows[i:i + chunk_size]
158
+ values_parts = []
159
+ for row in chunk:
160
+ escaped = ",".join("'" + _sql_escape(v) + "'" for v in row)
161
+ values_parts.append("(" + escaped + ")")
162
+ sql = prefix + table + " VALUES " + ",".join(values_parts)
163
+ con.execute(sql)
164
+
165
+
166
+ def upsert_entity(con, eid, etype, canonical, cn='', props=None):
167
+ """Insert or replace an entity."""
168
+ con.execute(
169
+ "INSERT OR REPLACE INTO entities VALUES (?, ?, ?, ?, ?)",
170
+ [eid, etype, canonical, cn, json.dumps(props or {}, ensure_ascii=False)]
171
+ )
172
+
173
+
174
+ def upsert_alias(con, alias, entity_id):
175
+ """Insert or replace an alias mapping."""
176
+ con.execute(
177
+ "INSERT OR REPLACE INTO aliases VALUES (?, ?)",
178
+ [alias.lower(), entity_id]
179
+ )
180
+
181
+
182
+ def upsert_edge(con, from_id, to_id, edge_type, seq=0, props=None):
183
+ """Insert an edge."""
184
+ con.execute(
185
+ "INSERT INTO edges VALUES (?, ?, ?, ?, ?)",
186
+ [from_id, to_id, edge_type, seq, json.dumps(props or {}, ensure_ascii=False)]
187
+ )
188
+
189
+
190
+ def delete_edges_from(con, from_id, edge_type=None):
191
+ """Delete all edges from a node (for incremental rebuild)."""
192
+ if edge_type:
193
+ con.execute("DELETE FROM edges WHERE from_id = ? AND edge_type = ?", [from_id, edge_type])
194
+ else:
195
+ con.execute("DELETE FROM edges WHERE from_id = ?", [from_id])
196
+
197
+
198
+ def resolve_entity(con, alias):
199
+ """Resolve any alias to its canonical entity ID."""
200
+ row = con.execute(
201
+ "SELECT entity_id FROM aliases WHERE alias = ?", [alias.lower()]
202
+ ).fetchone()
203
+ return row[0] if row else None
204
+
205
+
206
+ def get_neighbors(con, entity_id, edge_type=None, direction='out'):
207
+ """Get neighbors of an entity (one-hop)."""
208
+ if direction == 'out':
209
+ sql = "SELECT to_id FROM edges WHERE from_id = ?"
210
+ else:
211
+ sql = "SELECT from_id FROM edges WHERE to_id = ?"
212
+ params = [entity_id]
213
+ if edge_type:
214
+ sql += " AND edge_type = ?"
215
+ params.append(edge_type)
216
+ rows = con.execute(sql, params).fetchall()
217
+ return [r[0] for r in rows]
218
+
219
+
220
+ def multi_hop(con, start_id, edge_types=None, max_depth=5):
221
+ """Multi-hop traversal using recursive CTE."""
222
+ type_filter = ""
223
+ if edge_types:
224
+ placeholders = ",".join("'%s'" % t for t in edge_types)
225
+ type_filter = "AND edge_type IN (%s)" % placeholders
226
+
227
+ sql = """
228
+ WITH RECURSIVE reach(id, depth) AS (
229
+ SELECT '%s', 0
230
+ UNION ALL
231
+ SELECT e.to_id, r.depth + 1
232
+ FROM edges e
233
+ JOIN reach r ON e.from_id = r.id
234
+ WHERE r.depth < %d %s
235
+ )
236
+ SELECT id, MAX(depth) as max_depth
237
+ FROM reach
238
+ GROUP BY id
239
+ ORDER BY max_depth
240
+ """ % (start_id, max_depth, type_filter)
241
+
242
+ rows = con.execute(sql).fetchall()
243
+ return [(r[0], r[1]) for r in rows]
244
+
245
+
246
+ def bulk_import_from_json(con):
247
+ """One-time full import from existing JSON indexes into DuckDB."""
248
+ INDEX = str(DATA_INDEX_DIR / '_build_cache')
249
+
250
+ def load(name):
251
+ p = os.path.join(INDEX, name)
252
+ if os.path.isfile(p):
253
+ return json.load(open(p, encoding='utf-8'))
254
+ return {}
255
+
256
+ con.execute("DELETE FROM entities")
257
+ con.execute("DELETE FROM aliases")
258
+ con.execute("DELETE FROM edges")
259
+
260
+ er = load('entity-registry.json')
261
+ rel = load('relations.json')
262
+ edges_data = rel.get('edges', {})
263
+
264
+ entity_rows = []
265
+ alias_rows = []
266
+ edge_rows = []
267
+ ctrl_seen = set()
268
+
269
+ # Entities + aliases
270
+ for eid, ent in er.get('entities', {}).items():
271
+ props = ent.get('properties', {})
272
+ entity_rows.append((eid, ent['type'], ent['canonical'],
273
+ ent.get('cn', ''), json.dumps(props, ensure_ascii=False)))
274
+ for alias in ent.get('aliases', []):
275
+ alias_rows.append((alias.lower(), eid))
276
+
277
+ # Edges: click_to_api
278
+ for click in edges_data.get('click_to_api', []):
279
+ vue = click.get('vue_file', '')
280
+ vue_short = vue.split('/views/')[-1] if '/views/' in vue else vue
281
+ from_eid = 'B:%s:%s' % (vue_short, click.get('handler', ''))
282
+ to_eid = 'E:%s' % click.get('endpoint', '')
283
+ edge_rows.append((from_eid, to_eid, 'clicks', 0,
284
+ json.dumps({'button': click.get('button', ''),
285
+ 'confidence': click.get('confidence', '')},
286
+ ensure_ascii=False)))
287
+
288
+ # Edges: api_to_controller
289
+ for ep, ctrl_file in edges_data.get('api_to_controller', {}).items():
290
+ from_eid = 'E:%s' % ep
291
+ ctrl_name = ctrl_file.split('/')[-1].replace('.java', '')
292
+ to_eid = 'C:%s' % ctrl_name
293
+ if ctrl_name not in ctrl_seen:
294
+ entity_rows.append((to_eid, 'CONTROLLER', ctrl_name, '', '{}'))
295
+ alias_rows.append((ctrl_name.lower(), to_eid))
296
+ ctrl_seen.add(ctrl_name)
297
+ edge_rows.append((from_eid, to_eid, 'handled_by', 0, '{}'))
298
+
299
+ # Edges: controller_to_tests
300
+ for ctrl, tests in edges_data.get('controller_to_tests', {}).items():
301
+ ctrl_eid = 'C:%s' % ctrl
302
+ for t in tests[:5]:
303
+ test_eid = 'T:%s' % t.get('test', '')
304
+ edge_rows.append((ctrl_eid, test_eid, 'tested_by', 0,
305
+ json.dumps({'dep': t.get('dep', ''),
306
+ 'assertions': t.get('assertions', [])},
307
+ ensure_ascii=False)))
308
+
309
+ # Edges: feature adjacency
310
+ for feat_id, adj in er.get('feature_adjacency', {}).items():
311
+ for ep_eid in adj.get('endpoints', []):
312
+ edge_rows.append((feat_id, ep_eid, 'has_endpoint', 0, '{}'))
313
+ for btn_eid in adj.get('buttons', []):
314
+ edge_rows.append((feat_id, btn_eid, 'has_button', 0, '{}'))
315
+ for test_eid in adj.get('tests', []):
316
+ edge_rows.append((feat_id, test_eid, 'has_test', 0, '{}'))
317
+
318
+ # Batch insert
319
+ _batch_insert(con, 'entities', entity_rows, ignore=True)
320
+ _batch_insert(con, 'aliases', alias_rows, ignore=True)
321
+ _batch_insert(con, 'edges', edge_rows, ignore=False)
322
+
323
+ # Meta
324
+ con.execute("INSERT OR REPLACE INTO build_meta VALUES ('version', '1.0')")
325
+ con.execute("INSERT OR REPLACE INTO build_meta VALUES ('entities', ?)", [str(len(entity_rows))])
326
+ con.execute("INSERT OR REPLACE INTO build_meta VALUES ('edges', ?)", [str(len(edge_rows))])
327
+
328
+ return len(entity_rows), len(edge_rows)
329
+
330
+
331
+ def import_search_layer(con):
332
+ """导入搜索层: keywords/endpoints/prds/wiki → DuckDB 表。"""
333
+ INDEX = str(DATA_INDEX_DIR)
334
+ def load(name):
335
+ p = os.path.join(INDEX, name)
336
+ return json.load(open(p, encoding='utf-8')) if os.path.isfile(p) else {}
337
+
338
+ # keywords (code-keyword.json: {keyword: [files]})
339
+ kw = load('code-keyword.json')
340
+ kw_rows = [(k, f) for k, files in kw.items() for f in files]
341
+ con.execute("DELETE FROM keywords")
342
+ if kw_rows:
343
+ _batch_insert(con, 'keywords', kw_rows)
344
+
345
+ # endpoints (code-api.json: {endpoint: controller_file})
346
+ api = load('code-api.json')
347
+ con.execute("DELETE FROM endpoints")
348
+ ep_rows = [(ep, f) for ep, f in api.items()]
349
+ if ep_rows:
350
+ _batch_insert(con, 'endpoints', ep_rows, ignore=True)
351
+
352
+ # prds (prd-index.json)
353
+ prds = load('prd-index.json')
354
+ con.execute("DELETE FROM prds")
355
+ prd_rows = [(f, info.get('title', ''),
356
+ json.dumps(info.get('keywords', []), ensure_ascii=False),
357
+ json.dumps(info.get('features', []), ensure_ascii=False),
358
+ json.dumps(info.get('cn_terms', []), ensure_ascii=False))
359
+ for f, info in prds.items()]
360
+ if prd_rows:
361
+ _batch_insert(con, 'prds', prd_rows, ignore=True)
362
+
363
+ # wiki (wiki-index.json)
364
+ wiki = load('wiki-index.json')
365
+ con.execute("DELETE FROM wiki")
366
+ wiki_rows = []
367
+ for kw, entries in (wiki.get('index', {}) if isinstance(wiki, dict) else {}).items():
368
+ for e in (entries if isinstance(entries, list) else []):
369
+ wiki_rows.append((kw, e.get('project', ''), e.get('title', ''),
370
+ e.get('md_path', ''), e.get('description', '')))
371
+ if wiki_rows:
372
+ _batch_insert(con, 'wiki', wiki_rows)
373
+
374
+ return len(kw_rows), len(ep_rows), len(prd_rows), len(wiki_rows)
375
+
376
+
377
+ def import_ui_layer(con):
378
+ """导入UI层: pages/fields/modules → DuckDB 表。"""
379
+ INDEX = str(DATA_INDEX_DIR)
380
+ def load(name):
381
+ p = os.path.join(INDEX, name)
382
+ return json.load(open(p, encoding='utf-8')) if os.path.isfile(p) else {}
383
+
384
+ si = load('ui-style.json')
385
+
386
+ # pages (from ui-style projects.page_examples + page_types)
387
+ con.execute("DELETE FROM pages")
388
+ page_rows = []
389
+ for proj, pdata in si.get('projects', {}).items():
390
+ for ptype, examples in (pdata.get('page_examples') or {}).items():
391
+ for ex in examples:
392
+ if isinstance(ex, dict):
393
+ page_rows.append((ex.get('file', ''), proj, ptype,
394
+ json.dumps(ex.get('components', []), ensure_ascii=False),
395
+ json.dumps(ex.get('table_columns', []), ensure_ascii=False),
396
+ json.dumps(ex.get('form_fields', []), ensure_ascii=False),
397
+ json.dumps(ex.get('layout', {}), ensure_ascii=False)))
398
+ if page_rows:
399
+ _batch_insert(con, 'pages', page_rows, ignore=True)
400
+
401
+ # fields (from ui-style field_map)
402
+ con.execute("DELETE FROM fields")
403
+ fm = si.get('field_map', {})
404
+ field_rows = [(k, '/'.join(info.get('titles', [])[:2]), info.get('count', 1),
405
+ json.dumps(info.get('sample_files', [])[:3], ensure_ascii=False))
406
+ for k, info in fm.items()]
407
+ if field_rows:
408
+ _batch_insert(con, 'fields', field_rows)
409
+
410
+ # modules (from ui-modules.json)
411
+ mi = load('ui-modules.json')
412
+ con.execute("DELETE FROM modules")
413
+ mod_rows = [(m['dir'], m['cn'], proj, m['path'])
414
+ for proj, mods in mi.get('projects', {}).items() for m in mods]
415
+ if mod_rows:
416
+ _batch_insert(con, 'modules', mod_rows)
417
+
418
+ # style_meta (layout_fingerprint + component counts)
419
+ con.execute("DELETE FROM style_meta")
420
+ for proj, pdata in si.get('projects', {}).items():
421
+ fp = pdata.get('layout_fingerprint', {})
422
+ if fp:
423
+ con.execute("INSERT OR REPLACE INTO style_meta VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", [
424
+ proj, fp.get('stack', ''), fp.get('layout', ''), fp.get('access_mode', ''),
425
+ fp.get('sidebar_width'), fp.get('color_primary'), fp.get('color_success'),
426
+ fp.get('color_warning'), fp.get('color_destructive'), fp.get('radius'),
427
+ pdata.get('vue_files', 0),
428
+ json.dumps(si.get('common_components', {}), ensure_ascii=False),
429
+ json.dumps(si.get('form_components', {}), ensure_ascii=False),
430
+ ])
431
+
432
+ return len(page_rows), len(field_rows), len(mod_rows)
433
+
434
+
435
+ def import_trace_layer(con):
436
+ """导入trace层: traces/api_functions → DuckDB 表。"""
437
+ INDEX = str(DATA_INDEX_DIR)
438
+ tc = json.load(open(os.path.join(INDEX, 'trace-chain.json'), encoding='utf-8')) \
439
+ if os.path.isfile(os.path.join(INDEX, 'trace-chain.json')) else {}
440
+
441
+ con.execute("DELETE FROM traces")
442
+ trace_rows = [(
443
+ t.get('vue_file', '').replace('\\', '/'), t.get('button', ''),
444
+ t.get('handler', ''), t.get('api_fn', ''), t.get('endpoint', ''),
445
+ t.get('verb', ''), t.get('confidence', '')
446
+ ) for t in tc.get('traces', [])]
447
+ if trace_rows:
448
+ _batch_insert(con, 'traces', trace_rows)
449
+
450
+ con.execute("DELETE FROM api_functions")
451
+ fn_rows = [(fn, info.get('url', ''), info.get('verb', ''), info.get('file', ''))
452
+ for fn, info in tc.get('api_functions', {}).items()]
453
+ if fn_rows:
454
+ _batch_insert(con, 'api_functions', fn_rows, ignore=True)
455
+
456
+ return len(trace_rows), len(fn_rows)
457
+
458
+
459
+ def import_test_layer(con):
460
+ """导入test层: tests → DuckDB 表。"""
461
+ INDEX = str(DATA_INDEX_DIR)
462
+ ta = json.load(open(os.path.join(INDEX, 'test-assertions.json'), encoding='utf-8')) \
463
+ if os.path.isfile(os.path.join(INDEX, 'test-assertions.json')) else {}
464
+
465
+ con.execute("DELETE FROM tests")
466
+ test_rows = [(
467
+ t.get('class', ''), t.get('method', ''), t.get('package', ''),
468
+ t.get('display_name', ''),
469
+ json.dumps(t.get('injected_deps', []), ensure_ascii=False),
470
+ json.dumps(t.get('assertions', []), ensure_ascii=False),
471
+ t.get('is_integration', False)
472
+ ) for t in ta.get('tests', [])]
473
+ if test_rows:
474
+ _batch_insert(con, 'tests', test_rows)
475
+
476
+ return len(test_rows)
477
+
478
+
479
+ def import_workflow_layer(con):
480
+ """导入workflow层 → DuckDB 表。"""
481
+ INDEX = str(DATA_INDEX_DIR)
482
+ wf = json.load(open(os.path.join(INDEX, 'workflow-index.json'), encoding='utf-8')) \
483
+ if os.path.isfile(os.path.join(INDEX, 'workflow-index.json')) else {}
484
+
485
+ con.execute("DELETE FROM workflows")
486
+ wf_rows = []
487
+ for mod, mwf in wf.get('workflows', {}).items():
488
+ for seq, step in enumerate(mwf.get('chain', [])):
489
+ for op in step.get('operations', []):
490
+ wf_rows.append((mod, step['state'], seq,
491
+ op.get('button', ''), op.get('endpoint', '')))
492
+ if wf_rows:
493
+ _batch_insert(con, 'workflows', wf_rows)
494
+
495
+ return len(wf_rows)
496
+
497
+
498
+ def import_all(con):
499
+ """一键导入全部JSON数据到DuckDB (替代所有JSON索引)。"""
500
+ init_schema(con)
501
+ # 实体-关系层 (已有)
502
+ ent_n, edge_n = bulk_import_from_json(con)
503
+ # 搜索层
504
+ kw_n, ep_n, prd_n, wiki_n = import_search_layer(con)
505
+ # UI层
506
+ page_n, field_n, mod_n = import_ui_layer(con)
507
+ # trace层
508
+ trace_n, fn_n = import_trace_layer(con)
509
+ # test层
510
+ test_n = import_test_layer(con)
511
+ # workflow层
512
+ wf_n = import_workflow_layer(con)
513
+
514
+ return {
515
+ 'entities': ent_n, 'edges': edge_n,
516
+ 'keywords': kw_n, 'endpoints': ep_n, 'prds': prd_n, 'wiki': wiki_n,
517
+ 'pages': page_n, 'fields': field_n, 'modules': mod_n,
518
+ 'traces': trace_n, 'api_functions': fn_n,
519
+ 'tests': test_n, 'workflows': wf_n,
520
+ }
521
+
522
+
523
+ def get_stats(con):
524
+ """Get database statistics."""
525
+ ents = con.execute("SELECT COUNT(*) FROM entities").fetchone()[0]
526
+ aliases = con.execute("SELECT COUNT(*) FROM aliases").fetchone()[0]
527
+ edges = con.execute("SELECT COUNT(*) FROM edges").fetchone()[0]
528
+ by_type = con.execute(
529
+ "SELECT type, COUNT(*) FROM entities GROUP BY type ORDER BY COUNT(*) DESC"
530
+ ).fetchall()
531
+ return {
532
+ 'entities': ents,
533
+ 'aliases': aliases,
534
+ 'edges': edges,
535
+ 'by_type': dict(by_type),
536
+ }
537
+
538
+
539
+ if __name__ == '__main__':
540
+ con = get_db()
541
+ print('Importing ALL JSON indexes to DuckDB...')
542
+ stats = import_all(con)
543
+ con.close()
544
+ print()
545
+ print('=== DuckDB Knowledge Graph (unified) ===')
546
+ for k, v in stats.items():
547
+ print(' %-16s: %d' % (k, v))
548
+ print(' file: %s (%.1f KB)' % (DB_PATH, os.path.getsize(DB_PATH) / 1024))
549
+ con.close()