Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
- [第 4 篇:知识写入方式全景](https://mp.weixin.qq.com/s/V90mghqB5wttKd25eXA-Pw)(2026-08-09)
- [第 5 篇:OKF 0.2 规范介绍和实战](https://mp.weixin.qq.com/s/Dt748cHQCa7mfz1PEvgS6g)(2026-08)
- [第 6 篇:借助 HOOKS 机制实现跨会话记忆和任务管理](https://mp.weixin.qq.com/s/flsqORauNo0Th1v8G4Ceng)(2026-08)
- [第 7 篇:记忆/经验分层提取——自生长的团队知识库](https://mp.weixin.qq.com/s/s253xe5LiUmgdfDo3XxAbg)(2026-08)
- [第 8 篇:四维代码评审——让踩过的坑自动变成 CHECKLIST](https://mp.weixin.qq.com/s/wH_mjG5IL-0qo_qDFpODuw)(2026-08)



### 这个项目是什么?
Expand Down Expand Up @@ -766,6 +769,10 @@ CodeWiki-Plus 的核心工具链(Tree-sitter AST 解析、依赖图构建、
}
```

<p align="center">
<img src="img/thankyou.png" alt="Thank You" width="700" />
</p>

---

<a id="en"></a>
Expand Down
8 changes: 4 additions & 4 deletions codewiki/mcp/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -1894,16 +1894,16 @@ def _parse_row(r: sqlite3.Row) -> Tuple[Set[str], Optional[List], Optional[List]
return deps, bc, params

def _extract_title(content: str) -> Optional[str]:
for l in content.splitlines()[:30]:
s = l.strip()
for line in content.splitlines()[:30]:
s = line.strip()
if s.startswith("# "): return s[2:].strip()
return None

def _extract_frontmatter(content: str, key: str) -> Optional[str]:
if not content.startswith("---"): return None
try:
end = content.index("---", 3)
for l in content[3:end].splitlines():
if l.startswith(f"{key}:"): return l[len(key)+1:].strip().strip('"').strip("'")
for line in content[3:end].splitlines():
if line.startswith(f"{key}:"): return line[len(key)+1:].strip().strip('"').strip("'")
except ValueError: pass
return None
19 changes: 10 additions & 9 deletions codewiki/mcp/tools/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@

from __future__ import annotations

import json, logging, os
import json
import logging
import os
from pathlib import Path
from typing import Any, Dict, List, Optional, Set

from codewiki.mcp.cache import AnalysisCache, ComponentMeta, LazyComponentStore
from codewiki.mcp.session import SessionState, SessionStore
from codewiki.mcp.session import SessionStore
from codewiki.mcp.workspace import SessionWorkspace

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -92,7 +94,6 @@ def handle_analyze_repo(arguments: Dict[str, Any], store: SessionStore) -> str:
logger.warning("Route removal for %s failed (non-fatal): %s", cf, e)

# Compute set of unchanged files to skip during parsing
changed_set = set(changed)
all_cached_paths = cache.get_cached_file_paths()
# These are the relative paths of files that are still cached and unchanged
unchanged_rel_paths = all_cached_paths # After remove_by_file, only unchanged remain
Expand Down Expand Up @@ -148,16 +149,16 @@ def handle_analyze_repo(arguments: Dict[str, Any], store: SessionStore) -> str:
if not available_types & valid_types:
valid_types.add("function")
leaf_nodes = [
l for l in raw_leafs
if isinstance(l, str) and l in components
and components[l].component_type in valid_types
n for n in raw_leafs
if isinstance(n, str) and n in components
and components[n].component_type in valid_types
]
logger.info("Recomputed %d leaf nodes on merged graph", len(leaf_nodes))
except Exception as e:
logger.warning("Leaf-node recompute failed, merging with cached list: %s", e)
old_leafs = cache.get_leaf_nodes()
merged = [l for l in old_leafs if l in components]
merged.extend(l for l in leaf_nodes if l not in merged)
merged = [n for n in old_leafs if n in components]
merged.extend(n for n in leaf_nodes if n not in merged)
leaf_nodes = merged

# Write to SQLite cache (incremental mode if we had cached components)
Expand Down Expand Up @@ -868,7 +869,7 @@ def _extract_overview_refs(output_dir: Path) -> Set[str]:

def _save_overview_refs(output_dir: Path, refs: Set[str]):
"""Save overview refs to .meta/overview_refs.json."""
from codewiki.src.config import meta_join, META_DIR
from codewiki.src.config import meta_join
meta_dir = Path(meta_join(output_dir, ""))
meta_dir.mkdir(parents=True, exist_ok=True)
refs_path = meta_dir / "overview_refs.json"
Expand Down
4 changes: 2 additions & 2 deletions codewiki/mcp/tools/cbm_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ def merge_cbm_and_local_results(
local_links = local_topology.get("links", [])

existing_keys = {
f"{l.get('client_repo')}:{l.get('server_repo')}:{l.get('route_key')}"
for l in local_links
f"{link.get('client_repo')}:{link.get('server_repo')}:{link.get('route_key')}"
for link in local_links
}

for path_entry in cbm_paths:
Expand Down
20 changes: 10 additions & 10 deletions codewiki/mcp/tools/cross_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import json
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -113,41 +113,41 @@ def _format_all(links: List[Dict], routes: List[Dict]) -> Dict:
"unmatched_routes": [
r for r in routes
if not any(
l.get("route_key") == r.get("route_key") for l in links
link.get("route_key") == r.get("route_key") for link in links
)
],
}


def _filter_by_service(links: List[Dict], service: str) -> Dict:
matching = [
l for l in links
if service.lower() in l.get("client_repo", "").lower()
or service.lower() in l.get("server_repo", "").lower()
link for link in links
if service.lower() in link.get("client_repo", "").lower()
or service.lower() in link.get("server_repo", "").lower()
]
return {
"service": service,
"count": len(matching),
"as_client": [l for l in matching if service.lower() in l.get("client_repo", "").lower()],
"as_server": [l for l in matching if service.lower() in l.get("server_repo", "").lower()],
"as_client": [link for link in matching if service.lower() in link.get("client_repo", "").lower()],
"as_server": [link for link in matching if service.lower() in link.get("server_repo", "").lower()],
}


def _filter_by_method(links: List[Dict], method: str) -> Dict:
# MQ links serialize method as null — guard against None before .upper()
matching = [l for l in links if (l.get("method") or "").upper() == method.upper()]
matching = [link for link in links if (link.get("method") or "").upper() == method.upper()]
return {"method": method.upper(), "count": len(matching), "links": matching}


def _filter_by_path(links: List[Dict], path_prefix: str) -> Dict:
prefix_lower = path_prefix.lower()
matching = [l for l in links if (l.get("path") or "").lower().startswith(prefix_lower)]
matching = [link for link in links if (link.get("path") or "").lower().startswith(prefix_lower)]
return {"path_prefix": path_prefix, "count": len(matching), "links": matching}


def _trace_route(links: List[Dict], routes: List[Dict], route_key: str) -> Dict:
"""Trace a specific route: find all clients and servers involved."""
matching_links = [l for l in links if l.get("route_key") == route_key]
matching_links = [link for link in links if link.get("route_key") == route_key]
matching_routes = [r for r in routes if r.get("route_key") == route_key]

clients = []
Expand Down
4 changes: 2 additions & 2 deletions codewiki/mcp/tools/telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import os
from datetime import date, datetime
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
from typing import Dict, List, Set, Tuple

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -127,7 +127,7 @@ def _atomic_write_lines(path: Path, lines: List[str]) -> None:
def _read_lines(path: Path) -> List[str]:
"""Non-empty lines of a jsonl file; missing file → [] (never raises)."""
try:
return [l for l in path.read_text(encoding="utf-8", errors="replace").splitlines() if l.strip()]
return [line for line in path.read_text(encoding="utf-8", errors="replace").splitlines() if line.strip()]
except OSError:
return []

Expand Down
10 changes: 5 additions & 5 deletions codewiki/mcp/tools/watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,11 @@ def _incremental_refresh(
if not available_types & valid_types:
valid_types.add("function")
leaf_nodes = [
l
for l in raw_leafs
if isinstance(l, str)
and l in components
and components[l].component_type in valid_types
n
for n in raw_leafs
if isinstance(n, str)
and n in components
and components[n].component_type in valid_types
]
except Exception as exc:
logger.warning("watch: leaf-node recompute failed, keeping builder list: %s", exc)
Expand Down
8 changes: 4 additions & 4 deletions codewiki/mcp/tools/wiki_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,17 @@ def _extract_fm(ct, key):
if not ct.startswith("---"): return None
try:
end = ct.index("---", 3)
for l in ct[3:end].splitlines():
if l.startswith(f"{key}:"): return l[len(key)+1:].strip().strip('"').strip("'")
for line in ct[3:end].splitlines():
if line.startswith(f"{key}:"): return line[len(key)+1:].strip().strip('"').strip("'")
except ValueError: pass
return None

# Strip markdown links from H1 titles: "[JwtUtil](../src/JwtUtil.java)" -> "JwtUtil"
_MD_LINK_RE = re.compile(r"\[([^\]]*)\]\([^)]*\)")

def _extract_title(ct):
for l in ct.splitlines()[:30]:
s = l.strip()
for line in ct.splitlines()[:30]:
s = line.strip()
if s.startswith("# "):
title = _MD_LINK_RE.sub(lambda m: m.group(1), s[2:]).strip()
return title or None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ def generate_route_table(self, topology: WorkspaceTopology) -> str:
"| Method | Path | Client Service | Client Function | Server Service | Server Function |",
"|--------|------|----------------|-----------------|----------------|-----------------|",
]
for link in sorted(topology.links, key=lambda l: (l.client_repo, l.path)):
for link in sorted(topology.links, key=lambda link: (link.client_repo, link.path)):
method = link.method or "—"
path = link.path or "—"
client_func = link.client_function or "—"
Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,11 @@ target-version = "py312"
# which most of the existing codebase does not satisfy — see CI failures on
# c22fc26/99e4c44). Widen deliberately, not by upgrade accident.
select = ["E4", "E7", "E9", "F"]
# Relaxed: cosmetic E7 sub-rules and E501 are style-only; keep F/E9/E4 for bugs
ignore = ["E741", "E731", "E701", "E702", "E501"]

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["F401"]
# E701/E702 (multiple statements on one line) are style-only with 250+ legacy
# occurrences — relaxed in #17 until a dedicated cleanup pass.
# E741/E731 were re-tightened after fixing all violations (post-#17 cleanup);
# E501 is never active under this select, so it needs no ignore.
ignore = ["E701", "E702"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
13 changes: 0 additions & 13 deletions scripts/_tmp2.py

This file was deleted.

1 change: 0 additions & 1 deletion tests/okf_regression_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
handle_edit_doc_file,
handle_write_doc_file,
_inject_lightweight_frontmatter,
_resync_source_refs,
)
from codewiki.mcp.tools.knowledge_loop import ( # noqa: E402
handle_confirm_note,
Expand Down
4 changes: 1 addition & 3 deletions tests/smoke_test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@

import asyncio
import json
import os
import sys
import tempfile
from pathlib import Path

# Ensure codewiki is importable
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from codewiki.mcp.session import SessionStore, SessionState
from codewiki.mcp.session import SessionStore
from codewiki.mcp.tools.analysis import handle_analyze_repo
from codewiki.mcp.tools.code_reader import handle_read_code_components
from codewiki.mcp.tools.doc_writer import handle_write_doc_file, handle_edit_doc_file
Expand Down Expand Up @@ -89,7 +88,6 @@ def main():
check("summary has total_components", "total_components" in summary, str(summary.keys()))
check("summary has total_leaf_nodes", "total_leaf_nodes" in summary, str(summary.keys()))
check("summary has languages", "languages" in summary, str(summary.keys()))
total_leaf = summary["total_leaf_nodes"]

# Use list_components tool to get component index
lc_result = json.loads(handle_list_components({
Expand Down
9 changes: 3 additions & 6 deletions tests/test_adoption.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,13 @@
from __future__ import annotations

import json
import math
from pathlib import Path

import pytest

from codewiki.mcp.cache import USAGE_RANKING_DEFAULTS, compute_usage_heat
from codewiki.mcp.session import SessionStore
from codewiki.mcp.tools.adoption import (
extract_adopted_docs,
load_adoption_counts,
looks_like_search_happened,
record_adoption_events,
)
from codewiki.mcp.tools.capture_conversation import handle_capture_conversation
Expand Down Expand Up @@ -90,8 +86,9 @@ def test_existence_filter(self):
turns = _turns(
'<!-- codewiki:referenced-docs: ["exists.md", "missing.md"] -->',
)
exists = lambda p: p == "exists.md"
assert extract_adopted_docs(turns, existing=exists) == ["exists.md"]
def _exists(p):
return p == "exists.md"
assert extract_adopted_docs(turns, existing=_exists) == ["exists.md"]

def test_prose_mention_does_not_match(self):
turns = _turns(
Expand Down
2 changes: 1 addition & 1 deletion tests/test_change_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from __future__ import annotations

import json
from typing import Any, Dict, List
from typing import Any, Dict

import git
import pytest
Expand Down
1 change: 0 additions & 1 deletion tests/test_distill_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import json
from pathlib import Path

import pytest

from codewiki.mcp.session import SessionStore
from codewiki.mcp.tools import distill_conversation as distill
Expand Down
1 change: 0 additions & 1 deletion tests/test_freshness.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from datetime import datetime, timedelta
from pathlib import Path

import pytest
import yaml

from codewiki.mcp.session import SessionStore
Expand Down
1 change: 0 additions & 1 deletion tests/test_friction.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import sys
from pathlib import Path

import pytest

from codewiki.mcp.session import SessionStore
from codewiki.mcp.tools import capture_conversation as capture
Expand Down
1 change: 0 additions & 1 deletion tests/test_index_freshness.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import time
from pathlib import Path

import pytest

from codewiki.mcp.session import SessionStore
from codewiki.mcp.tools import index_freshness as fr
Expand Down
4 changes: 2 additions & 2 deletions tests/test_low_adoption.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ def _append_events(od: Path, events: list) -> None:
if p.exists():
import json as _json
existing = [
_json.loads(l) for l in p.read_text(encoding="utf-8").splitlines()
if l.strip()
_json.loads(line) for line in p.read_text(encoding="utf-8").splitlines()
if line.strip()
]
write_telemetry(od, "tester", existing + events)

Expand Down
3 changes: 1 addition & 2 deletions tests/test_openviking_borrowings.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

Expand All @@ -20,7 +19,7 @@

def test_v4_table_single_source():
from codewiki.mcp.tools.note_types import (
DEFAULT_NOTE_TYPES, valid_note_types, promotion_targets,
DEFAULT_NOTE_TYPES, promotion_targets,
freshness_windows, merge_fields_for, validate_note_types,
)
from codewiki.mcp.tools.distill_conversation import _VALID_NOTE_TYPES
Expand Down
Loading
Loading