Release 0.2.0: object tree, refs/modules options, docs

Add query object (--clear), modules -n, composite refs labels,
FillChecking/TypeSet parsing, VERSION/CHANGELOG and --version.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mihailkudravcev
2026-07-16 16:37:41 +03:00
parent 88a3737078
commit a9d1215820
9 changed files with 837 additions and 117 deletions
+92 -23
View File
@@ -123,14 +123,18 @@ def search_modules(
*,
configs: list[str] | None = None,
limit: int = 30,
names_only: bool = False,
) -> list[dict[str, Any]]:
fts = _fts_query(query)
sql = """
snip_col = "'' AS snip" if names_only else "snippet(modules_fts, 4, '[', ']', '', 16) AS snip"
sql = f"""
SELECT m.config, m.rel_path, m.owner, m.module_kind, m.symbols, m.line_count,
snippet(modules_fts, 4, '[', ']', '', 16) AS snip,
o.synonym AS owner_synonym,
{snip_col},
bm25(modules_fts, 2.0, 5.0, 2.0, 8.0, 1.0) AS score
FROM modules_fts
JOIN modules m ON m.id = modules_fts.module_id
LEFT JOIN objects o ON o.config = m.config AND o.full_name = m.owner
WHERE modules_fts MATCH ?
"""
params: list[Any] = [fts]
@@ -144,13 +148,15 @@ def search_modules(
except sqlite3.OperationalError:
like = f"%{query}%"
sql2 = """
SELECT config, rel_path, owner, module_kind, symbols, line_count, '' AS snip, 0 AS score
FROM modules
WHERE rel_path LIKE ? OR owner LIKE ? OR symbols LIKE ?
SELECT m.config, m.rel_path, m.owner, m.module_kind, m.symbols, m.line_count,
o.synonym AS owner_synonym, '' AS snip, 0 AS score
FROM modules m
LEFT JOIN objects o ON o.config = m.config AND o.full_name = m.owner
WHERE m.rel_path LIKE ? OR m.owner LIKE ? OR m.symbols LIKE ?
"""
params2: list[Any] = [like, like, like]
if configs:
sql2 += f" AND config IN ({','.join('?' * len(configs))})"
sql2 += f" AND m.config IN ({','.join('?' * len(configs))})"
params2.extend(configs)
sql2 += " LIMIT ?"
params2.append(limit)
@@ -163,7 +169,13 @@ def find_refs_to(
*,
configs: list[str] | None = None,
limit: int = 100,
) -> list[dict[str, Any]]:
) -> tuple[list[dict[str, Any]], dict[str, int]]:
"""Обратные ссылки на объект.
Возвращает (строки, totals_by_config).
При поиске по нескольким конфигурациям ``limit`` — максимум **на каждую**
конфигурацию (чтобы crm3-26 не вытеснял crm3-dev из-за ORDER BY + LIMIT).
"""
kind = ""
name = target
if "." in target:
@@ -183,25 +195,82 @@ def find_refs_to(
}:
kind_ref = kind + "Ref"
where = "r.to_name = ?"
params: list[Any] = [name]
if kind_ref:
sql = """
SELECT config, from_full_name, from_field, to_kind, to_name
FROM refs
WHERE to_name = ? AND (to_kind = ? OR to_kind = ?)
"""
params: list[Any] = [name, kind_ref, kind]
else:
sql = """
SELECT config, from_full_name, from_field, to_kind, to_name
FROM refs WHERE to_name = ?
"""
params = [name]
where += " AND (r.to_kind = ? OR r.to_kind = ?)"
params.extend([kind_ref, kind])
if configs:
sql += f" AND config IN ({','.join('?' * len(configs))})"
where += f" AND r.config IN ({','.join('?' * len(configs))})"
params.extend(configs)
sql += " ORDER BY config, from_full_name LIMIT ?"
params.append(limit)
return [dict(r) for r in conn.execute(sql, params)]
# Итоги по конфигурациям (без LIMIT)
totals: dict[str, int] = {
r["config"]: int(r["n"])
for r in conn.execute(
f"SELECT r.config, COUNT(*) AS n FROM refs r WHERE {where} GROUP BY r.config",
params,
)
}
# limit на каждую конфигурацию — иначе алфавитный ORDER BY отрезает остальные
sql = f"""
WITH matched AS (
SELECT
r.config,
r.from_full_name,
r.from_field,
r.to_kind,
r.to_name,
f.types AS field_types,
f.refs_json AS field_refs_json,
ROW_NUMBER() OVER (
PARTITION BY r.config
ORDER BY r.from_full_name, r.from_field
) AS rn
FROM refs r
LEFT JOIN objects o
ON o.config = r.config AND o.full_name = r.from_full_name
LEFT JOIN fields f
ON f.object_id = o.id
AND (
(COALESCE(f.tabular_section, '') = '' AND f.name = r.from_field)
OR (f.tabular_section || '.' || f.name = r.from_field)
)
WHERE {where}
)
SELECT *
FROM matched
WHERE rn <= ?
ORDER BY config, from_full_name, from_field
"""
rows = [dict(r) for r in conn.execute(sql, [*params, limit])]
for row in rows:
types_s = row.get("field_types") or ""
refs_raw = row.get("field_refs_json") or "[]"
try:
refs_list = json.loads(refs_raw) if refs_raw else []
except json.JSONDecodeError:
refs_list = []
type_parts = [t.strip() for t in types_s.split(",") if t.strip()]
# составной: несколько cfg:* или несколько ссылок в типе
composite = len(refs_list) > 1 or len(type_parts) > 1
row["is_composite"] = composite
others: list[str] = []
target_full = f"{row['to_kind']}.{row['to_name']}"
for ref in refs_list:
label = f"{ref.get('kind')}.{ref.get('name')}"
if label != target_full and label not in others:
others.append(label)
for t in type_parts:
# cfg:DocumentRef.X → DocumentRef.X
short = t[4:] if t.startswith("cfg:") else t
if short != target_full and short not in others and "Ref." in short:
others.append(short)
row["other_types"] = others
return rows, totals
def get_object(