mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-13 06:31:47 +00:00
* docs: turn README into an index and move reference detail to docs/guide The README had grown to 393 KB because every change appended its notes there. The text moves verbatim into 18 topic guides under docs/guide/; only headings are re-leveled and relative links re-pointed. The README keeps the introduction, a quick start and a guide index. Contributor test notes move to CONTRIBUTING.md. * docs: send user docs to topic guides and budget the root README The rule 'update README.md for user-facing changes' is what grew the README. Guidance now points to docs/guide/, and check_agent_guidance.py fails when the root README exceeds 16 KiB (AG003). * chore: remove unreferenced screenshots and finished plan notes pr-build/ holds two PR screenshots (3 MB) from #1986. plans/ holds the plan for the merged #4049. docs/SKILL_NAME_CONFLICT_FIX.md is a February change note. Nothing references them. * docs: route always-loaded AGENTS.md files to scoped guides * docs(agents): cut scoped guides to scope, verify, traps, decisions Each AGENTS.md keeps four kinds of content: the scope of the directory, the verification commands, traps a local test run does not show, and decisions the code cannot state. Values, field lists, function inventories, test names, issue numbers and history are deleted, because the code, the tests and git state them. Search and fetch providers share one community guide. Four guides with no remaining content are removed. The size check now allows 6 KiB per file (hard limit 8 KiB) and 24 KiB per chain (hard limit 32 KiB). * style: format frontend guide table and guidance test imports --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
217 lines
9.0 KiB
Python
217 lines
9.0 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
from pathlib import Path, PurePosixPath
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
CHECKER_PATH = REPO_ROOT / "scripts" / "check_agent_guidance.py"
|
|
EXPECTED_GUIDANCE_PATHS = {
|
|
"AGENTS.md",
|
|
"docker/AGENTS.md",
|
|
"skills/AGENTS.md",
|
|
"skills/public/data-analysis/AGENTS.md",
|
|
"skills/public/video-generation/AGENTS.md",
|
|
"backend/AGENTS.md",
|
|
"backend/tests/AGENTS.md",
|
|
"backend/scripts/benchmark/AGENTS.md",
|
|
"frontend/AGENTS.md",
|
|
"backend/app/gateway/AGENTS.md",
|
|
"backend/app/gateway/routers/AGENTS.md",
|
|
"backend/app/channels/AGENTS.md",
|
|
"backend/packages/harness/deerflow/AGENTS.md",
|
|
"backend/packages/harness/deerflow/agents/AGENTS.md",
|
|
"backend/packages/harness/deerflow/agents/middlewares/AGENTS.md",
|
|
"backend/packages/harness/deerflow/agents/memory/AGENTS.md",
|
|
"backend/packages/harness/deerflow/agents/task_continuity/AGENTS.md",
|
|
"backend/packages/harness/deerflow/community/AGENTS.md",
|
|
"backend/packages/harness/deerflow/community/groundroute/AGENTS.md",
|
|
"backend/packages/harness/deerflow/community/jina_ai/AGENTS.md",
|
|
"backend/packages/harness/deerflow/community/ragflow/AGENTS.md",
|
|
"backend/packages/harness/deerflow/community/serper/AGENTS.md",
|
|
"backend/packages/harness/deerflow/community/tavily/AGENTS.md",
|
|
"backend/packages/harness/deerflow/community/ddg_search/AGENTS.md",
|
|
"backend/packages/harness/deerflow/community/e2b_sandbox/AGENTS.md",
|
|
"backend/packages/harness/deerflow/community/aio_sandbox/AGENTS.md",
|
|
"backend/packages/harness/deerflow/community/boxlite/AGENTS.md",
|
|
"backend/packages/harness/deerflow/community/browser_automation/AGENTS.md",
|
|
"backend/packages/harness/deerflow/community/opensandbox/AGENTS.md",
|
|
"backend/packages/harness/deerflow/config/AGENTS.md",
|
|
"backend/packages/harness/deerflow/extensions/AGENTS.md",
|
|
"backend/packages/harness/deerflow/runtime/AGENTS.md",
|
|
"backend/packages/harness/deerflow/sandbox/AGENTS.md",
|
|
"backend/packages/harness/deerflow/mcp/AGENTS.md",
|
|
"backend/packages/harness/deerflow/models/AGENTS.md",
|
|
"backend/packages/harness/deerflow/persistence/AGENTS.md",
|
|
"backend/packages/harness/deerflow/persistence/migrations/AGENTS.md",
|
|
"backend/packages/harness/deerflow/persistence/user/AGENTS.md",
|
|
"backend/packages/harness/deerflow/skills/AGENTS.md",
|
|
"backend/packages/harness/deerflow/storage/AGENTS.md",
|
|
"backend/packages/harness/deerflow/subagents/AGENTS.md",
|
|
"backend/packages/harness/deerflow/tools/AGENTS.md",
|
|
"backend/packages/harness/deerflow/tracing/AGENTS.md",
|
|
"backend/packages/harness/deerflow/tui/AGENTS.md",
|
|
"backend/packages/harness/deerflow/typesafe/AGENTS.md",
|
|
"backend/packages/harness/deerflow/uploads/AGENTS.md",
|
|
"backend/packages/harness/deerflow/utils/AGENTS.md",
|
|
"frontend/src/AGENTS.md",
|
|
"frontend/src/components/workspace/AGENTS.md",
|
|
"frontend/src/components/workspace/browser-view/AGENTS.md",
|
|
"frontend/src/core/auth/AGENTS.md",
|
|
"frontend/src/core/background-tasks/AGENTS.md",
|
|
"frontend/src/core/memory/AGENTS.md",
|
|
"frontend/src/core/models/AGENTS.md",
|
|
"frontend/tests/e2e/AGENTS.md",
|
|
"scripts/AGENTS.md",
|
|
"examples/deerflow-extension-agent-teams/AGENTS.md",
|
|
"examples/deerflow-extension-workflows/AGENTS.md",
|
|
"examples/deerflow-extension-weknora/AGENTS.md",
|
|
"examples/deerflow-extension-knowledge-management/AGENTS.md",
|
|
"examples/deerflow-extension-knowledge-platform/AGENTS.md",
|
|
"examples/deerflow-extension-knowledge-center/AGENTS.md",
|
|
"examples/deerflow-extension-lightrag/AGENTS.md",
|
|
}
|
|
|
|
|
|
def _load_checker():
|
|
assert CHECKER_PATH.exists(), f"{CHECKER_PATH} must exist"
|
|
spec = importlib.util.spec_from_file_location("deerflow_agent_guidance_check", CHECKER_PATH)
|
|
assert spec is not None
|
|
assert spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
checker = _load_checker()
|
|
|
|
|
|
def _files(**overrides: str) -> dict[PurePosixPath, str]:
|
|
files = {PurePosixPath("AGENTS.md"): "# Repository instructions\n"}
|
|
files.update({PurePosixPath(path): text for path, text in overrides.items()})
|
|
return files
|
|
|
|
|
|
def _codes(findings, severity: str | None = None) -> list[str]:
|
|
return [finding.code for finding in findings if severity is None or finding.severity == severity]
|
|
|
|
|
|
def test_normalized_utf8_size_uses_lf_and_counts_non_ascii_bytes() -> None:
|
|
assert checker.normalized_utf8_size("a\r\n中\r") == len("a\n中\n".encode())
|
|
|
|
|
|
def test_single_file_over_hard_limit_is_an_error(tmp_path: Path) -> None:
|
|
files = _files(**{"backend/AGENTS.md": "x" * (checker.AGENT_HARD + 1)})
|
|
|
|
assert _codes(checker.analyze(tmp_path, files), "error") == ["AG001"]
|
|
|
|
|
|
def test_single_file_between_soft_and_hard_limit_is_a_warning(tmp_path: Path) -> None:
|
|
files = _files(**{"backend/AGENTS.md": "x" * (checker.AGENT_SOFT + 1)})
|
|
|
|
findings = checker.analyze(tmp_path, files)
|
|
|
|
assert _codes(findings, "warning") == ["AG001"]
|
|
assert _codes(findings, "error") == []
|
|
|
|
|
|
def test_file_at_soft_limit_has_no_finding(tmp_path: Path) -> None:
|
|
files = _files(**{"backend/AGENTS.md": "x" * checker.AGENT_SOFT})
|
|
|
|
assert checker.analyze(tmp_path, files) == []
|
|
|
|
|
|
def test_effective_ancestor_chain_can_fail_when_each_file_is_valid(tmp_path: Path) -> None:
|
|
size = checker.AGENT_HARD - 1
|
|
files = _files(
|
|
**{
|
|
"AGENTS.md": "r" * size,
|
|
"backend/AGENTS.md": "m" * size,
|
|
"backend/app/AGENTS.md": "l" * size,
|
|
"backend/app/gateway/AGENTS.md": "g" * size,
|
|
"backend/app/gateway/routers/AGENTS.md": "o" * size,
|
|
}
|
|
)
|
|
|
|
findings = checker.analyze(tmp_path, files)
|
|
|
|
assert "AG001" not in _codes(findings, "error")
|
|
assert [finding.path.as_posix() for finding in findings if finding.code == "AG002" and finding.severity == "error"] == ["backend/app/gateway/routers/AGENTS.md"]
|
|
|
|
|
|
def test_legacy_hard_violation_may_shrink_but_may_not_grow(tmp_path: Path) -> None:
|
|
base = _files(**{"AGENTS.md": "x" * (checker.AGENT_HARD + 2)})
|
|
smaller = _files(**{"AGENTS.md": "x" * (checker.AGENT_HARD + 1)})
|
|
grown = _files(**{"AGENTS.md": "x" * (checker.AGENT_HARD + 3)})
|
|
|
|
smaller_findings = checker.analyze(tmp_path, smaller, base_files=base)
|
|
grown_findings = checker.analyze(tmp_path, grown, base_files=base)
|
|
|
|
assert "AG001" not in _codes(smaller_findings, "error")
|
|
assert "AG001" in _codes(grown_findings, "error")
|
|
|
|
|
|
def test_root_readme_over_hard_limit_is_an_error(tmp_path: Path) -> None:
|
|
findings = checker.analyze(tmp_path, _files(**{"README.md": "x" * (16 * 1024 + 1)}))
|
|
|
|
assert _codes(findings, "error") == ["AG003"]
|
|
|
|
|
|
def test_only_the_root_readme_is_budgeted(tmp_path: Path) -> None:
|
|
files = _files(**{"README.md": "x" * (12 * 1024), "backend/README.md": "x" * (64 * 1024)})
|
|
|
|
assert checker.analyze(tmp_path, files) == []
|
|
assert checker.budgeted_paths(files) == {PurePosixPath("AGENTS.md"), PurePosixPath("README.md")}
|
|
|
|
|
|
def test_oversized_readme_may_shrink_but_may_not_grow(tmp_path: Path) -> None:
|
|
base = _files(**{"README.md": "x" * (64 * 1024)})
|
|
smaller = _files(**{"README.md": "x" * (64 * 1024 - 1)})
|
|
grown = _files(**{"README.md": "x" * (64 * 1024 + 1)})
|
|
|
|
assert "AG003" not in _codes(checker.analyze(tmp_path, smaller, base_files=base), "error")
|
|
assert "AG003" in _codes(checker.analyze(tmp_path, grown, base_files=base), "error")
|
|
|
|
|
|
def test_repository_readme_fits_hard_budget() -> None:
|
|
files = {checker.README_PATH: (REPO_ROOT / "README.md").read_text(encoding="utf-8")}
|
|
|
|
assert not [finding for finding in checker.analyze(REPO_ROOT, files) if finding.severity == "error"]
|
|
|
|
|
|
def test_discovery_uses_exact_agents_basename() -> None:
|
|
paths = [
|
|
PurePosixPath("AGENTS.md"),
|
|
PurePosixPath("backend/AGENTS.md"),
|
|
PurePosixPath("backend/CLAUDE.md"),
|
|
PurePosixPath("backend/docs/GITHUB_AGENTS.md"),
|
|
]
|
|
|
|
assert checker.guidance_paths(paths) == {
|
|
PurePosixPath("AGENTS.md"),
|
|
PurePosixPath("backend/AGENTS.md"),
|
|
}
|
|
|
|
|
|
def test_repository_has_the_approved_scoped_guidance_shape() -> None:
|
|
actual = {path.as_posix() for path in checker.guidance_paths(checker._worktree_paths(REPO_ROOT))}
|
|
|
|
assert actual == EXPECTED_GUIDANCE_PATHS
|
|
|
|
|
|
def test_repository_exposes_one_local_and_one_ci_entrypoint() -> None:
|
|
makefile = (REPO_ROOT / "Makefile").read_text(encoding="utf-8")
|
|
workflow = (REPO_ROOT / ".github" / "workflows" / "lint-check.yml").read_text(encoding="utf-8")
|
|
|
|
assert "check-agent-guidance:" in makefile
|
|
assert "scripts/check_agent_guidance.py" in makefile
|
|
assert workflow.count("agent-guidance:") == 1
|
|
assert "fetch-depth: 0" in workflow
|
|
assert "--base-ref" in workflow
|
|
assert "--before" in workflow
|
|
|
|
|
|
def test_repository_guidance_has_no_budget_finding() -> None:
|
|
files = {PurePosixPath(relative): (REPO_ROOT / relative).read_text(encoding="utf-8") for relative in EXPECTED_GUIDANCE_PATHS}
|
|
|
|
assert checker.analyze(REPO_ROOT, files) == []
|