diff --git a/README.md b/README.md
index 2cde55f..990dd0a 100644
--- a/README.md
+++ b/README.md
@@ -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)
+
### 这个项目是什么?
@@ -766,6 +769,10 @@ CodeWiki-Plus 的核心工具链(Tree-sitter AST 解析、依赖图构建、
}
```
+
+
+
+
---
diff --git a/codewiki/mcp/cache.py b/codewiki/mcp/cache.py
index eb4725e..031ed88 100644
--- a/codewiki/mcp/cache.py
+++ b/codewiki/mcp/cache.py
@@ -1894,8 +1894,8 @@ 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
@@ -1903,7 +1903,7 @@ 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
diff --git a/codewiki/mcp/tools/analysis.py b/codewiki/mcp/tools/analysis.py
index 94638fc..ea7d8de 100644
--- a/codewiki/mcp/tools/analysis.py
+++ b/codewiki/mcp/tools/analysis.py
@@ -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__)
@@ -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
@@ -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)
@@ -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"
diff --git a/codewiki/mcp/tools/cbm_integration.py b/codewiki/mcp/tools/cbm_integration.py
index d0b4744..5fd8019 100644
--- a/codewiki/mcp/tools/cbm_integration.py
+++ b/codewiki/mcp/tools/cbm_integration.py
@@ -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:
diff --git a/codewiki/mcp/tools/cross_service.py b/codewiki/mcp/tools/cross_service.py
index 65ffeab..bdf953c 100644
--- a/codewiki/mcp/tools/cross_service.py
+++ b/codewiki/mcp/tools/cross_service.py
@@ -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__)
@@ -113,7 +113,7 @@ 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
)
],
}
@@ -121,33 +121,33 @@ def _format_all(links: List[Dict], routes: List[Dict]) -> Dict:
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 = []
diff --git a/codewiki/mcp/tools/telemetry.py b/codewiki/mcp/tools/telemetry.py
index 5596364..2d2a5b3 100644
--- a/codewiki/mcp/tools/telemetry.py
+++ b/codewiki/mcp/tools/telemetry.py
@@ -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__)
@@ -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 []
diff --git a/codewiki/mcp/tools/watch.py b/codewiki/mcp/tools/watch.py
index 1a90188..886b57a 100644
--- a/codewiki/mcp/tools/watch.py
+++ b/codewiki/mcp/tools/watch.py
@@ -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)
diff --git a/codewiki/mcp/tools/wiki_search.py b/codewiki/mcp/tools/wiki_search.py
index fb3cc08..346947c 100644
--- a/codewiki/mcp/tools/wiki_search.py
+++ b/codewiki/mcp/tools/wiki_search.py
@@ -171,8 +171,8 @@ 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
@@ -180,8 +180,8 @@ def _extract_fm(ct, key):
_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
diff --git a/codewiki/src/be/dependency_analyzer/analysis/topology_visualizer.py b/codewiki/src/be/dependency_analyzer/analysis/topology_visualizer.py
index f4d0fb8..3b085d6 100644
--- a/codewiki/src/be/dependency_analyzer/analysis/topology_visualizer.py
+++ b/codewiki/src/be/dependency_analyzer/analysis/topology_visualizer.py
@@ -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 "—"
diff --git a/pyproject.toml b/pyproject.toml
index 63ce276..a3f4b4b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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"]
diff --git a/scripts/_tmp2.py b/scripts/_tmp2.py
deleted file mode 100644
index c5fc20e..0000000
--- a/scripts/_tmp2.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import pathlib, re, subprocess
-out = subprocess.run(["git","-c","core.quotepath=false","status","--porcelain"], capture_output=True, text=True, encoding="utf-8").stdout
-deleted = [l[3:].strip() for l in out.splitlines() if len(l)>3 and "D" in l[:2] and "notes/" in l]
-arc_names = {p.name for p in pathlib.Path(".trash/notes-archive").glob("*.md")}
-missing = []
-for p in deleted:
- name = pathlib.Path(p).name
- if name not in arc_names:
- missing.append(p)
-print("deleted total:", len(deleted))
-print("not in archive (truly deleted):", len(missing))
-for m in missing:
- print(" -", m)
diff --git a/tests/okf_regression_test.py b/tests/okf_regression_test.py
index 0216a9a..c789e79 100644
--- a/tests/okf_regression_test.py
+++ b/tests/okf_regression_test.py
@@ -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,
diff --git a/tests/smoke_test_mcp.py b/tests/smoke_test_mcp.py
index 88e828e..a22484a 100644
--- a/tests/smoke_test_mcp.py
+++ b/tests/smoke_test_mcp.py
@@ -7,7 +7,6 @@
import asyncio
import json
-import os
import sys
import tempfile
from pathlib import Path
@@ -15,7 +14,7 @@
# 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
@@ -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({
diff --git a/tests/test_adoption.py b/tests/test_adoption.py
index ebfd740..3aea116 100644
--- a/tests/test_adoption.py
+++ b/tests/test_adoption.py
@@ -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
@@ -90,8 +86,9 @@ def test_existence_filter(self):
turns = _turns(
'',
)
- 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(
diff --git a/tests/test_change_analysis.py b/tests/test_change_analysis.py
index 6624c50..720b466 100644
--- a/tests/test_change_analysis.py
+++ b/tests/test_change_analysis.py
@@ -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
diff --git a/tests/test_distill_cleanup.py b/tests/test_distill_cleanup.py
index 48dc9e4..bef80a7 100644
--- a/tests/test_distill_cleanup.py
+++ b/tests/test_distill_cleanup.py
@@ -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
diff --git a/tests/test_freshness.py b/tests/test_freshness.py
index 120b38b..6ea37dc 100644
--- a/tests/test_freshness.py
+++ b/tests/test_freshness.py
@@ -15,7 +15,6 @@
from datetime import datetime, timedelta
from pathlib import Path
-import pytest
import yaml
from codewiki.mcp.session import SessionStore
diff --git a/tests/test_friction.py b/tests/test_friction.py
index 1ee1362..08819f4 100644
--- a/tests/test_friction.py
+++ b/tests/test_friction.py
@@ -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
diff --git a/tests/test_index_freshness.py b/tests/test_index_freshness.py
index 89b1a5d..5125a29 100644
--- a/tests/test_index_freshness.py
+++ b/tests/test_index_freshness.py
@@ -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
diff --git a/tests/test_low_adoption.py b/tests/test_low_adoption.py
index 13aed67..d0cca6b 100644
--- a/tests/test_low_adoption.py
+++ b/tests/test_low_adoption.py
@@ -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)
diff --git a/tests/test_openviking_borrowings.py b/tests/test_openviking_borrowings.py
index 834a66a..c03c8a3 100644
--- a/tests/test_openviking_borrowings.py
+++ b/tests/test_openviking_borrowings.py
@@ -11,7 +11,6 @@
import sys
from pathlib import Path
-import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
@@ -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
diff --git a/tests/test_team_telemetry.py b/tests/test_team_telemetry.py
index 83d5bee..41037d4 100644
--- a/tests/test_team_telemetry.py
+++ b/tests/test_team_telemetry.py
@@ -12,18 +12,14 @@
from __future__ import annotations
import json
-import os
from datetime import datetime, timedelta
from pathlib import Path
-import pytest
from codewiki.mcp.session import SessionStore
-from codewiki.mcp.tools import telemetry
from codewiki.mcp.tools.capture_conversation import handle_capture_conversation
from codewiki.mcp.tools.telemetry import (
aggregate_usage,
- record_adopted,
record_hit,
telemetry_enabled,
)
@@ -37,8 +33,8 @@ def _all_events(od: Path) -> list:
out = []
for p in sorted(d.glob("*.jsonl")):
out.extend(
- 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()
)
return out
@@ -111,7 +107,7 @@ def test_adoption_key_dedup(self, tmp_path):
{"t": "adopted", "doc": "notes/a.md", "at": "z", "key": "u/s2"},
]
(p / "u.jsonl").write_text(
- "\n".join(json.dumps(l) for l in lines) + "\n", encoding="utf-8")
+ "\n".join(json.dumps(e) for e in lines) + "\n", encoding="utf-8")
agg = aggregate_usage(tmp_path)
assert agg["notes/a.md"]["adopted"] == 2 # distinct keys only
@@ -198,5 +194,5 @@ def test_adoption_declared_in_conversation_recorded(self, tmp_path):
# event landed in the CURRENT user's stream (user-dependent name)
files = list((od / ".meta" / "telemetry").glob("*.jsonl"))
assert len(files) == 1
- events = [json.loads(l) for l in files[0].read_text(encoding="utf-8").splitlines() if l.strip()]
+ events = [json.loads(line) for line in files[0].read_text(encoding="utf-8").splitlines() if line.strip()]
assert any(e["t"] == "adopted" and e["doc"] == "notes/pit.md" for e in events)
diff --git a/tests/test_usage_ranking.py b/tests/test_usage_ranking.py
index 013bb09..4c8167b 100644
--- a/tests/test_usage_ranking.py
+++ b/tests/test_usage_ranking.py
@@ -16,7 +16,6 @@
import inspect
import math
-import sqlite3
from datetime import date, datetime, timedelta
from pathlib import Path