@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.
@@ -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@1"
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
- print(f"[code-index][semantic] processed {file_number}/{len(records)} files", flush=True)
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
- entities.append(_entity(feature_id, "feature", feature, file_record, 1, 1,
46
- "heuristic", "path_feature_inference"))
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
- bindings.append(_binding(feature_id, symbol["id"], "static_inferred", "heuristic", path, symbol.get("line_start", 1)))
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
- _extract_api(line, path, file_record, line_no, entities, flows)
54
- _extract_db(line, path, file_record, line_no, entities, flows)
55
- _extract_ui(line, path, file_record, line_no, entities, flows, state_ids)
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
- 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, "metadata": metadata or {}}
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
- 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, "metadata": metadata or {}}
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, 2, workspace_state.get("repositoryRevision", ""), workspace_state.get("contentManifestHash", ""),
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
- structural_entities = [
217
- {"id": item["id"], "entity_type": item.get("kind", "symbol"), "name": item.get("name", item["id"]),
218
- "codebase": item.get("codebase"), "path": item.get("path"), "line_start": item.get("line_start"),
219
- "line_end": item.get("line_end"), "content_hash": item.get("content_hash"), "confidence": "exact",
220
- "extractor": item.get("parser", "index"), "metadata": item.get("metadata", {})}
221
- for item in [*files, *symbols]
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
- row["metadata_json"] = json_dumps(row.pop("metadata", {}))
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
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.77";
2
+ export const PRODUCT_VERSION = "0.1.79";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5