@guanlin4924/devflow-pi 0.1.77 → 0.1.79
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/package.json +9 -2
- package/plugins/devflow-intel/extensions/devflow-intel.ts +12 -1
- package/plugins/devflow-intel/skills/code-index-query/SKILL.md +8 -0
- package/plugins/devflow-intel/skills/workflow-code-index/SKILL.md +5 -0
- package/plugins/devflow-intel/skills/workflow-code-index/references/adapter-contract.md +8 -0
- package/plugins/devflow-intel/skills/workflow-code-index/references/data-model.md +1 -1
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/capabilities.py +26 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/generic_text.py +3 -1
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/helpers/tree_sitter_wasm_helper.mjs +50 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/parser-capabilities.json +491 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/registry.py +20 -31
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/tree_sitter_wasm.py +253 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +173 -32
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/semantic.py +93 -12
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/writers/sqlite_writer.py +26 -13
- package/src/constants.ts +1 -1
|
@@ -5,31 +5,58 @@ does not replace framework parsers; it gives the relational index a stable
|
|
|
5
5
|
schema so API, database, UI and feature facts can be linked incrementally.
|
|
6
6
|
"""
|
|
7
7
|
import re
|
|
8
|
+
import time
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Dict, Iterable, List, Tuple
|
|
10
11
|
|
|
11
12
|
from core.schema import short_hash, stable_key
|
|
12
13
|
from core.hashing import file_sha256
|
|
13
14
|
|
|
14
|
-
SEMANTIC_EXTRACTOR_VERSION = "semantic-linker@
|
|
15
|
+
SEMANTIC_EXTRACTOR_VERSION = "semantic-linker@2"
|
|
16
|
+
SEMANTIC_MAX_BYTES = 512_000
|
|
17
|
+
SEMANTIC_FILE_BUDGET_SECONDS = 2.0
|
|
18
|
+
SEMANTIC_MAX_LINE_CHARS = 100_000
|
|
19
|
+
SEMANTIC_LANGUAGES = {
|
|
20
|
+
"python", "javascript", "typescript", "vue", "svelte", "go", "rust",
|
|
21
|
+
"php", "java", "kotlin", "dart", "c", "cpp", "csharp", "ruby",
|
|
22
|
+
"swift", "scala", "lua", "shell", "sql", "html", "css",
|
|
23
|
+
}
|
|
24
|
+
SEMANTIC_LOCK_FILES = {
|
|
25
|
+
"package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock",
|
|
26
|
+
"composer.lock", "cargo.lock", "poetry.lock", "pdm.lock", "go.sum",
|
|
27
|
+
}
|
|
28
|
+
SEMANTIC_GENERATED_DIRS = {
|
|
29
|
+
"node_modules", "vendor", "dist", "build", "coverage", "target", ".next",
|
|
30
|
+
".nuxt", "_nuxt", ".cache", "generated", ".generated", "__generated__", "snapshots",
|
|
31
|
+
}
|
|
15
32
|
|
|
16
33
|
|
|
17
34
|
def build_semantic_facts(root: Path, files: Iterable[dict], symbols: Iterable[dict],
|
|
18
|
-
relations: Iterable[dict]) -> Tuple[List[dict], List[dict], List[dict]]:
|
|
35
|
+
relations: Iterable[dict], stats: Dict | None = None) -> Tuple[List[dict], List[dict], List[dict]]:
|
|
19
36
|
entities: List[dict] = []
|
|
20
37
|
flows: List[dict] = []
|
|
21
38
|
bindings: List[dict] = []
|
|
22
39
|
records = list(files)
|
|
40
|
+
summary = {"processed": 0, "timedOut": 0, "skipped": {}}
|
|
23
41
|
symbol_by_path = {}
|
|
24
42
|
for symbol in symbols:
|
|
25
43
|
symbol_by_path.setdefault(symbol.get("path"), []).append(symbol)
|
|
26
44
|
|
|
27
45
|
for file_number, file_record in enumerate(records, 1):
|
|
28
46
|
if file_number == 1 or file_number % 250 == 0 or file_number == len(records):
|
|
29
|
-
|
|
47
|
+
skipped_total = sum(summary["skipped"].values())
|
|
48
|
+
print(f"[code-index][semantic] visited {file_number}/{len(records)} files; processed={summary['processed']} skipped={skipped_total} timed_out={summary['timedOut']}", flush=True)
|
|
30
49
|
path = file_record["path"]
|
|
50
|
+
skip_reason = semantic_skip_reason(file_record)
|
|
51
|
+
if skip_reason:
|
|
52
|
+
summary["skipped"][skip_reason] = summary["skipped"].get(skip_reason, 0) + 1
|
|
53
|
+
continue
|
|
31
54
|
source = _read(root, path)
|
|
32
55
|
if source is None:
|
|
56
|
+
summary["skipped"]["unreadable"] = summary["skipped"].get("unreadable", 0) + 1
|
|
57
|
+
continue
|
|
58
|
+
if _looks_minified_or_compressed(path, source):
|
|
59
|
+
summary["skipped"]["minified_or_compressed"] = summary["skipped"].get("minified_or_compressed", 0) + 1
|
|
33
60
|
continue
|
|
34
61
|
try:
|
|
35
62
|
if file_sha256(root / path) != file_record.get("content_hash"):
|
|
@@ -38,21 +65,36 @@ def build_semantic_facts(root: Path, files: Iterable[dict], symbols: Iterable[di
|
|
|
38
65
|
continue
|
|
39
66
|
except OSError:
|
|
40
67
|
continue
|
|
68
|
+
deadline = time.monotonic() + SEMANTIC_FILE_BUDGET_SECONDS
|
|
69
|
+
file_entities: List[dict] = []
|
|
70
|
+
file_flows: List[dict] = []
|
|
71
|
+
file_bindings: List[dict] = []
|
|
41
72
|
language = file_record.get("language", "")
|
|
42
73
|
feature = _feature_from_path(path)
|
|
43
74
|
if feature:
|
|
44
75
|
feature_id = f"feature:{stable_key(feature)}"
|
|
45
|
-
|
|
46
|
-
|
|
76
|
+
file_entities.append(_entity(feature_id, "feature", feature, file_record, 1, 1,
|
|
77
|
+
"heuristic", "path_feature_inference"))
|
|
47
78
|
for symbol in symbol_by_path.get(path, []):
|
|
48
79
|
if symbol.get("kind") in {"route", "component", "class", "function", "method"}:
|
|
49
|
-
|
|
80
|
+
file_bindings.append(_binding(feature_id, symbol["id"], "static_inferred", "heuristic", path, symbol.get("line_start", 1)))
|
|
50
81
|
|
|
51
82
|
state_ids = set()
|
|
83
|
+
timed_out = False
|
|
52
84
|
for line_no, line in enumerate(source.splitlines(), 1):
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
85
|
+
if time.monotonic() > deadline:
|
|
86
|
+
timed_out = True
|
|
87
|
+
break
|
|
88
|
+
_extract_api(line, path, file_record, line_no, file_entities, file_flows)
|
|
89
|
+
_extract_db(line, path, file_record, line_no, file_entities, file_flows)
|
|
90
|
+
_extract_ui(line, path, file_record, line_no, file_entities, file_flows, state_ids)
|
|
91
|
+
if timed_out:
|
|
92
|
+
summary["timedOut"] += 1
|
|
93
|
+
continue
|
|
94
|
+
entities.extend(file_entities)
|
|
95
|
+
flows.extend(file_flows)
|
|
96
|
+
bindings.extend(file_bindings)
|
|
97
|
+
summary["processed"] += 1
|
|
56
98
|
|
|
57
99
|
# Existing structural relations remain the source of method call edges;
|
|
58
100
|
# expose them in the semantic graph without changing their meaning.
|
|
@@ -67,11 +109,48 @@ def build_semantic_facts(root: Path, files: Iterable[dict], symbols: Iterable[di
|
|
|
67
109
|
"line_start": relation.get("evidence_line"),
|
|
68
110
|
"confidence": relation.get("confidence", "heuristic"),
|
|
69
111
|
"evidence_level": "syntax",
|
|
112
|
+
"extractor": relation.get("parser", "unknown"),
|
|
70
113
|
"metadata": relation.get("metadata", {}),
|
|
71
114
|
})
|
|
115
|
+
if stats is not None:
|
|
116
|
+
stats.clear()
|
|
117
|
+
stats.update(summary)
|
|
72
118
|
return _dedupe_entities(entities), _dedupe_flows(flows), _dedupe_bindings(bindings)
|
|
73
119
|
|
|
74
120
|
|
|
121
|
+
def semantic_skip_reason(record: dict):
|
|
122
|
+
path = Path(record.get("path", ""))
|
|
123
|
+
parts = tuple(part.lower() for part in path.parts)
|
|
124
|
+
name = path.name.lower()
|
|
125
|
+
if name in SEMANTIC_LOCK_FILES or name.endswith((".lock", ".lockb")):
|
|
126
|
+
return "lock_file"
|
|
127
|
+
if any(part in SEMANTIC_GENERATED_DIRS for part in parts):
|
|
128
|
+
return "generated_path"
|
|
129
|
+
path_text = "/".join(parts)
|
|
130
|
+
if any(marker in path_text for marker in ("public/assets/", "public/build/", "public/_nuxt/")):
|
|
131
|
+
return "generated_path"
|
|
132
|
+
if "public" in parts:
|
|
133
|
+
public_index = parts.index("public")
|
|
134
|
+
if any(part in {"assets", "build", "dist", "static", "_nuxt"} for part in parts[public_index + 1:-1]):
|
|
135
|
+
return "generated_path"
|
|
136
|
+
if name.endswith((".min.js", ".min.css", ".bundle.js", ".chunk.js", ".map", ".g.dart", ".freezed.dart", ".pb.go", ".pb.cc", "_pb2.py", ".designer.cs")) or ".generated." in name:
|
|
137
|
+
return "generated_asset"
|
|
138
|
+
if int(record.get("size_bytes") or 0) > SEMANTIC_MAX_BYTES:
|
|
139
|
+
return "too_large"
|
|
140
|
+
if record.get("language", "") not in SEMANTIC_LANGUAGES:
|
|
141
|
+
return "unsupported_language"
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _looks_minified_or_compressed(path: str, source: str) -> bool:
|
|
146
|
+
if not source:
|
|
147
|
+
return False
|
|
148
|
+
lines = source.splitlines()
|
|
149
|
+
if any(len(line) > SEMANTIC_MAX_LINE_CHARS for line in lines):
|
|
150
|
+
return True
|
|
151
|
+
return len(source) > 100_000 and len(lines) <= 5
|
|
152
|
+
|
|
153
|
+
|
|
75
154
|
def _extract_api(line, path, record, line_no, entities, flows):
|
|
76
155
|
# Framework-neutral route declarations and client URLs.
|
|
77
156
|
match = re.search(r"(?:@(?:app|router|api)\.(get|post|put|patch|delete)\s*\(|(?:GET|POST|PUT|PATCH|DELETE)\s+)(['\"])(/[^'\"]+)\2", line, re.I)
|
|
@@ -144,15 +223,17 @@ def _extract_ui(line, path, record, line_no, entities, flows, state_ids=None):
|
|
|
144
223
|
|
|
145
224
|
|
|
146
225
|
def _entity(entity_id, entity_type, name, record, start, end, confidence, extractor, metadata=None):
|
|
147
|
-
|
|
226
|
+
extractor_id = extractor if "@" in extractor else f"{SEMANTIC_EXTRACTOR_VERSION}:{extractor}"
|
|
227
|
+
return {"id": entity_id, "entity_type": entity_type, "name": name, "codebase": record["codebase"], "path": record["path"], "line_start": start, "line_end": end, "content_hash": record["content_hash"], "confidence": confidence, "extractor": extractor_id, "metadata": metadata or {}}
|
|
148
228
|
|
|
149
229
|
|
|
150
230
|
def _flow(source_id, target_id, flow_type, path, line, confidence, extractor, metadata=None):
|
|
151
|
-
|
|
231
|
+
extractor_id = extractor if "@" in extractor else f"{SEMANTIC_EXTRACTOR_VERSION}:{extractor}"
|
|
232
|
+
return {"source_id": source_id, "target_id": target_id, "flow_type": flow_type, "source_path": path, "line_start": line, "confidence": confidence, "evidence_level": "static_source", "extractor": extractor_id, "metadata": metadata or {}}
|
|
152
233
|
|
|
153
234
|
|
|
154
235
|
def _binding(feature_id, entity_id, source, confidence, path, line):
|
|
155
|
-
return {"feature_id": feature_id, "entity_id": entity_id, "binding_type": source, "confidence": confidence, "source_path": path, "line_start": line}
|
|
236
|
+
return {"feature_id": feature_id, "entity_id": entity_id, "binding_type": source, "confidence": confidence, "source_path": path, "line_start": line, "extractor": f"{SEMANTIC_EXTRACTOR_VERSION}:path_feature_binding"}
|
|
156
237
|
|
|
157
238
|
|
|
158
239
|
def _read(root, path):
|
|
@@ -2,6 +2,7 @@ import sqlite3
|
|
|
2
2
|
import os
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
from adapters.capabilities import confidence_for_parser
|
|
5
6
|
from core.schema import json_dumps
|
|
6
7
|
|
|
7
8
|
|
|
@@ -31,6 +32,7 @@ CREATE TABLE IF NOT EXISTS symbols (
|
|
|
31
32
|
language TEXT NOT NULL,
|
|
32
33
|
content_hash TEXT NOT NULL,
|
|
33
34
|
visibility TEXT,
|
|
35
|
+
parser TEXT NOT NULL,
|
|
34
36
|
metadata_json TEXT
|
|
35
37
|
);
|
|
36
38
|
|
|
@@ -113,6 +115,7 @@ CREATE TABLE IF NOT EXISTS feature_bindings (
|
|
|
113
115
|
confidence TEXT NOT NULL,
|
|
114
116
|
source_path TEXT,
|
|
115
117
|
line_start INTEGER,
|
|
118
|
+
extractor TEXT NOT NULL,
|
|
116
119
|
PRIMARY KEY(snapshot_id, feature_id, entity_id)
|
|
117
120
|
);
|
|
118
121
|
|
|
@@ -207,19 +210,27 @@ def write_sqlite(path: Path, files, symbols, relations, diagnostics, snapshot_id
|
|
|
207
210
|
parsed_count = sum(1 for item in files if item.get("path") not in parse_failed_paths)
|
|
208
211
|
conn.execute(
|
|
209
212
|
"INSERT INTO analysis_snapshots(snapshot_id, schema_version, source_revision, content_manifest_hash, extractor_set_hash, created_at, status) VALUES(?,?,?,?,?,?,?)",
|
|
210
|
-
(snapshot_id,
|
|
213
|
+
(snapshot_id, 3, workspace_state.get("repositoryRevision", ""), workspace_state.get("contentManifestHash", ""),
|
|
211
214
|
workspace_state.get("extractorSetHash", ""), workspace_state.get("generatedAt", ""), "current"),
|
|
212
215
|
)
|
|
213
216
|
# Structural symbols and files are first-class graph nodes as well.
|
|
214
217
|
# This prevents semantic flows and feature bindings from dangling when
|
|
215
218
|
# a framework linker points at an existing code symbol.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
parser_by_path = {item.get("path"): item.get("parser", "unknown") for item in files}
|
|
220
|
+
structural_entities = []
|
|
221
|
+
for item in [*files, *symbols]:
|
|
222
|
+
is_file = "size_bytes" in item
|
|
223
|
+
metadata = dict(item.get("metadata", {}))
|
|
224
|
+
parser = item.get("parser") or metadata.get("parser") or parser_by_path.get(item.get("path"), "unknown")
|
|
225
|
+
metadata.setdefault("parser", parser)
|
|
226
|
+
structural_entities.append({
|
|
227
|
+
"id": item["id"], "entity_type": item.get("kind", "file" if is_file else "symbol"),
|
|
228
|
+
"name": item.get("name", item.get("path", item["id"])), "codebase": item.get("codebase"),
|
|
229
|
+
"path": item.get("path"), "line_start": item.get("line_start", 1 if is_file else None),
|
|
230
|
+
"line_end": item.get("line_end", 1 if is_file else None), "content_hash": item.get("content_hash"),
|
|
231
|
+
"confidence": confidence_for_parser(parser, "file" if is_file else "symbol"),
|
|
232
|
+
"extractor": parser, "metadata": metadata,
|
|
233
|
+
})
|
|
223
234
|
by_id = {item["id"]: item for item in semantic_entities}
|
|
224
235
|
for item in structural_entities:
|
|
225
236
|
by_id.setdefault(item["id"], item)
|
|
@@ -247,8 +258,8 @@ def write_sqlite(path: Path, files, symbols, relations, diagnostics, snapshot_id
|
|
|
247
258
|
[(snapshot_id, item["source_id"], item["target_id"], item["flow_type"], item.get("source_path"), item.get("line_start"), item["confidence"], item.get("evidence_level", "static_source"), item.get("extractor", "unknown"), json_dumps(item.get("metadata", {}))) for item in semantic_flows],
|
|
248
259
|
)
|
|
249
260
|
conn.executemany(
|
|
250
|
-
"INSERT INTO feature_bindings(snapshot_id, feature_id, entity_id, binding_type, confidence, source_path, line_start) VALUES(
|
|
251
|
-
[(snapshot_id, item["feature_id"], item["entity_id"], item["binding_type"], item["confidence"], item.get("source_path"), item.get("line_start")) for item in feature_bindings],
|
|
261
|
+
"INSERT INTO feature_bindings(snapshot_id, feature_id, entity_id, binding_type, confidence, source_path, line_start, extractor) VALUES(?,?,?,?,?,?,?,?)",
|
|
262
|
+
[(snapshot_id, item["feature_id"], item["entity_id"], item["binding_type"], item["confidence"], item.get("source_path"), item.get("line_start"), item.get("extractor", "unknown")) for item in feature_bindings],
|
|
252
263
|
)
|
|
253
264
|
conn.execute(
|
|
254
265
|
"INSERT INTO coverage_metrics(snapshot_id, files_scanned, files_parsed, diagnostics, semantic_entities, semantic_flows, dynamic_limitations, metadata_json) VALUES(?,?,?,?,?,?,?,?)",
|
|
@@ -264,9 +275,9 @@ def write_sqlite(path: Path, files, symbols, relations, diagnostics, snapshot_id
|
|
|
264
275
|
conn.executemany(
|
|
265
276
|
"""
|
|
266
277
|
INSERT INTO symbols(id, codebase, kind, name, qualified_name, container_id, path, line_start, line_end,
|
|
267
|
-
signature, language, content_hash, visibility, metadata_json)
|
|
278
|
+
signature, language, content_hash, visibility, parser, metadata_json)
|
|
268
279
|
VALUES(:id, :codebase, :kind, :name, :qualified_name, :container_id, :path, :line_start, :line_end,
|
|
269
|
-
:signature, :language, :content_hash, :visibility, :metadata_json)
|
|
280
|
+
:signature, :language, :content_hash, :visibility, :parser, :metadata_json)
|
|
270
281
|
""",
|
|
271
282
|
[_symbol_row(record) for record in symbols],
|
|
272
283
|
)
|
|
@@ -302,7 +313,9 @@ def write_sqlite(path: Path, files, symbols, relations, diagnostics, snapshot_id
|
|
|
302
313
|
|
|
303
314
|
def _symbol_row(record):
|
|
304
315
|
row = dict(record)
|
|
305
|
-
|
|
316
|
+
metadata = row.pop("metadata", {})
|
|
317
|
+
row["parser"] = row.get("parser") or metadata.get("parser") or "unknown"
|
|
318
|
+
row["metadata_json"] = json_dumps(metadata)
|
|
306
319
|
return row
|
|
307
320
|
|
|
308
321
|
|
package/src/constants.ts
CHANGED