diffgenome 0.1.0__py3-none-any.whl

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.
Files changed (53) hide show
  1. diffgenome/__init__.py +7 -0
  2. diffgenome/__main__.py +240 -0
  3. diffgenome/_collectors/go/dg/dg.go +623 -0
  4. diffgenome/_collectors/go/go.mod +3 -0
  5. diffgenome/_collectors/go/instrument/facts.go +346 -0
  6. diffgenome/_collectors/go/instrument/main.go +484 -0
  7. diffgenome/_collectors/node/instrument.js +289 -0
  8. diffgenome/_collectors/node/jest-setup.js +40 -0
  9. diffgenome/_collectors/node/package-lock.json +35 -0
  10. diffgenome/_collectors/node/package.json +11 -0
  11. diffgenome/_collectors/node/runtime.js +426 -0
  12. diffgenome/ambiguity.py +122 -0
  13. diffgenome/api.py +67 -0
  14. diffgenome/change.py +86 -0
  15. diffgenome/change_artifact.py +310 -0
  16. diffgenome/collect/__init__.py +2 -0
  17. diffgenome/collect/go_test.py +271 -0
  18. diffgenome/collect/node_jest.py +319 -0
  19. diffgenome/collect/py_monitoring.py +985 -0
  20. diffgenome/collect/py_runtime.py +116 -0
  21. diffgenome/collect/py_symbols.py +238 -0
  22. diffgenome/collect/pytest_plugin.py +130 -0
  23. diffgenome/compose.py +469 -0
  24. diffgenome/dependence.py +264 -0
  25. diffgenome/evaluate.py +669 -0
  26. diffgenome/frontends/__init__.py +0 -0
  27. diffgenome/frontends/python_ir.py +335 -0
  28. diffgenome/genome.py +1016 -0
  29. diffgenome/genome_pipeline.py +674 -0
  30. diffgenome/genome_prompt.py +33 -0
  31. diffgenome/genome_state.py +2118 -0
  32. diffgenome/graph.py +426 -0
  33. diffgenome/llm.py +189 -0
  34. diffgenome/model.py +364 -0
  35. diffgenome/mvp.py +398 -0
  36. diffgenome/probe.py +509 -0
  37. diffgenome/projection.py +308 -0
  38. diffgenome/py.typed +0 -0
  39. diffgenome/render.py +118 -0
  40. diffgenome/report.py +363 -0
  41. diffgenome/resolve.py +37 -0
  42. diffgenome/runtime.py +74 -0
  43. diffgenome/runtime_evidence.py +261 -0
  44. diffgenome/sandbox.py +166 -0
  45. diffgenome/serialize.py +96 -0
  46. diffgenome/sites.py +19 -0
  47. diffgenome/static_types.py +69 -0
  48. diffgenome/structure.py +462 -0
  49. diffgenome-0.1.0.dist-info/METADATA +139 -0
  50. diffgenome-0.1.0.dist-info/RECORD +53 -0
  51. diffgenome-0.1.0.dist-info/WHEEL +4 -0
  52. diffgenome-0.1.0.dist-info/entry_points.txt +2 -0
  53. diffgenome-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,116 @@
1
+ """Python/pytest runtime adapter: the CPython collector as a `RuntimeAdapter`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from pathlib import Path
7
+
8
+ from diffgenome.collect.py_symbols import PythonSymbolIndex
9
+ from diffgenome.model import Execution, Stimulus
10
+ from diffgenome.probe import copy_probe_traces
11
+ from diffgenome.runtime import SymbolIndex
12
+ from diffgenome.sandbox import Workspace
13
+ from diffgenome.serialize import execution_from_json
14
+
15
+ DIFFGENOME_SRC = Path(__file__).resolve().parents[2]
16
+
17
+
18
+ class PytestRuntime:
19
+ name = "python/pytest"
20
+
21
+ def __init__(
22
+ self,
23
+ repo: Path,
24
+ python: Path,
25
+ source_root: str,
26
+ test_root: str,
27
+ tests: str,
28
+ pytest_args: list[str],
29
+ pythonpath: list[str] | None = None,
30
+ test_env: dict[str, str] | None = None,
31
+ allow_loopback: bool = False,
32
+ ) -> None:
33
+ self.repo = repo
34
+ self.python = python
35
+ self.source_root = source_root
36
+ self.test_root = test_root
37
+ self.tests = tests
38
+ self.pytest_args = pytest_args
39
+ # Generic target-environment options (experiment 10, Baserow): repo-relative import
40
+ # roots resolved inside the disposable workspace; allow-listed, non-secret test
41
+ # variables (e.g. a local disposable database); loopback-only networking for local
42
+ # services. Target code still runs under the OS sandbox with no other network.
43
+ self.pythonpath = list(pythonpath or [])
44
+ self.test_env = dict(test_env or {})
45
+ self.allow_loopback = allow_loopback
46
+
47
+ def prepare(self, ws: Workspace) -> SymbolIndex:
48
+ # Index the workspace copy, i.e. the analyzed revision (`--rev` archives it there), not
49
+ # the checkout: the checkout may sit at another revision, and the index maps the
50
+ # change's diff lines and the executed code's locations onto symbols.
51
+ return PythonSymbolIndex(ws.repo, [ws.repo / self.source_root], [ws.repo / self.test_root])
52
+
53
+ def trace(
54
+ self, ws: Workspace, out_dir: Path, stimulus: Stimulus, only: list[str] | None
55
+ ) -> tuple[list[Execution], str, str]:
56
+ tag = "existing" if stimulus is Stimulus.EXISTING_TEST else "probe"
57
+ traces_ws = ws.root / f"traces-{tag}-{len(list(ws.root.glob('traces-*')))}"
58
+ traces_ws.mkdir(parents=True, exist_ok=True)
59
+ argv = [
60
+ str(self.python), "-m", "pytest", "-q", "-p", "no:cacheprovider",
61
+ "-p", "diffgenome.collect.pytest_plugin",
62
+ "--diffgenome-out", str(traces_ws),
63
+ "--diffgenome-repo-root", ".",
64
+ "--diffgenome-source-root", self.source_root,
65
+ "--diffgenome-test-root", self.test_root,
66
+ "--diffgenome-stimulus", stimulus.value,
67
+ "--diffgenome-egress-guard",
68
+ *self.pytest_args,
69
+ *(only if only else [self.tests]),
70
+ ] # fmt: skip
71
+ roots = [str(ws.repo / p) for p in self.pythonpath]
72
+ env = {**self.test_env, "PYTHONPATH": ":".join([*roots, str(DIFFGENOME_SRC)])}
73
+ result = ws.run(argv, env=env, timeout=1800, allow_loopback=self.allow_loopback)
74
+ out_dir.mkdir(parents=True, exist_ok=True)
75
+ copy_probe_traces(traces_ws, out_dir)
76
+ executions = [execution_from_json(f.read_text()) for f in sorted(out_dir.glob("*.json"))]
77
+ return executions, result.stdout, result.stderr
78
+
79
+ def probe_relpath(self, tag: str) -> str:
80
+ return f"{self.test_root}/test_diffgenome_probe_{tag}.py"
81
+
82
+ def conventions(self, index: SymbolIndex) -> str:
83
+ parts = ["Runtime: Python, pytest. The probe is a pytest test file."]
84
+ for root in index.test_roots:
85
+ f = root / "conftest.py"
86
+ if f.is_file():
87
+ lines: list[str] = []
88
+ try:
89
+ tree = ast.parse(f.read_text(encoding="utf-8"))
90
+ except SyntaxError:
91
+ tree = None
92
+ for node in tree.body if tree else []:
93
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and any(
94
+ "fixture" in ast.unparse(d) for d in node.decorator_list
95
+ ):
96
+ doc = ast.get_docstring(node) or ""
97
+ params = ", ".join(a.arg for a in node.args.args)
98
+ lines.append(
99
+ f"- {node.name}({params}): {doc.splitlines()[0] if doc else ''}"
100
+ )
101
+ if lines:
102
+ parts.append("conftest fixtures available:\n" + "\n".join(lines[:30]))
103
+ ini = index.repo_root / "pytest.ini"
104
+ if ini.is_file():
105
+ parts.append(
106
+ "pytest.ini:\n" + "\n".join(ini.read_text(encoding="utf-8").splitlines()[:20])
107
+ )
108
+ pyproject = index.repo_root / "pyproject.toml"
109
+ if pyproject.is_file():
110
+ text = pyproject.read_text(encoding="utf-8")
111
+ if "[tool.pytest" in text:
112
+ parts.append(
113
+ "pyproject pytest section:\n"
114
+ + "\n".join(text[text.index("[tool.pytest") :].splitlines()[:15])
115
+ )
116
+ return "\n\n".join(parts)
@@ -0,0 +1,238 @@
1
+ """Python source adapter: static symbol index over a repository's files.
2
+
3
+ Maps source lines to `SymbolId`s with the *same naming scheme the tracer uses* (module from
4
+ path under the roots, dotted qualname), extracts a symbol's source text for probe context,
5
+ and lists test files that import a module. Language-specific by nature; it lives beside
6
+ the collector and is used through `diffgenome.change.SymbolIndex`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ast
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+
15
+ from diffgenome.model import SymbolId
16
+
17
+ _EXCLUDED = {"site-packages", "dist-packages", "node_modules", "__pycache__"}
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Definition:
22
+ symbol: SymbolId
23
+ path: str # repository-relative
24
+ start: int
25
+ end: int
26
+ kind: str # "function" | "class"
27
+ is_async: bool = False
28
+
29
+
30
+ class PythonSymbolIndex:
31
+ def __init__(self, repo_root: Path, source_roots: list[Path], test_roots: list[Path]) -> None:
32
+ self.repo_root = repo_root.resolve()
33
+ self.source_roots = [p.resolve() for p in source_roots]
34
+ self.test_roots = [p.resolve() for p in test_roots]
35
+ self._defs: dict[str, list[Definition]] = {}
36
+
37
+ # ------------------------------------------------------------------ naming (mirrors tracer)
38
+
39
+ def module_for(self, path: Path) -> str | None:
40
+ path = path.resolve()
41
+ if any(p in _EXCLUDED or p.startswith(".") for p in path.parts[1:]):
42
+ return None
43
+ for roots, is_test in ((self.test_roots, True), (self.source_roots, False)):
44
+ for root in roots:
45
+ try:
46
+ rel = path.relative_to(root)
47
+ except ValueError:
48
+ continue
49
+ parts = list(rel.with_suffix("").parts)
50
+ if parts and parts[-1] == "__init__":
51
+ parts.pop()
52
+ prefix = root.relative_to(self.repo_root).parts if is_test else ()
53
+ return ".".join((*prefix, *parts))
54
+ return None
55
+
56
+ # ------------------------------------------------------------------ definitions
57
+
58
+ def definitions(self, rel_path: str) -> list[Definition]:
59
+ if rel_path in self._defs:
60
+ return self._defs[rel_path]
61
+ file = self.repo_root / rel_path
62
+ module = self.module_for(file)
63
+ out: list[Definition] = []
64
+ if module is not None and file.suffix == ".py":
65
+ try:
66
+ tree = ast.parse(file.read_text(encoding="utf-8"))
67
+ except (SyntaxError, OSError, UnicodeDecodeError):
68
+ tree = None
69
+ if tree is not None:
70
+ self._collect(tree.body, module, [], rel_path, out)
71
+ self._defs[rel_path] = out
72
+ return out
73
+
74
+ def _collect(
75
+ self, body: list[ast.stmt], module: str, scope: list[str], rel: str, out: list[Definition]
76
+ ) -> None:
77
+ for node in body:
78
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
79
+ qual = ".".join([*scope, node.name])
80
+ out.append(
81
+ Definition(
82
+ f"py:{module}.{qual}",
83
+ rel,
84
+ node.lineno,
85
+ node.end_lineno or node.lineno,
86
+ "function",
87
+ isinstance(node, ast.AsyncFunctionDef),
88
+ )
89
+ )
90
+ self._collect(node.body, module, [*scope, node.name, "<locals>"], rel, out)
91
+ elif isinstance(node, ast.ClassDef):
92
+ qual = ".".join([*scope, node.name])
93
+ out.append(
94
+ Definition(
95
+ f"py:{module}.{qual}",
96
+ rel,
97
+ node.lineno,
98
+ node.end_lineno or node.lineno,
99
+ "class",
100
+ )
101
+ )
102
+ self._collect(node.body, module, [*scope, node.name], rel, out)
103
+
104
+ def symbols_at(self, rel_path: str, lines: set[int]) -> list[Definition]:
105
+ """Innermost function definitions covering any of the lines (classes only when a
106
+ changed line belongs to no function, e.g. a class attribute)."""
107
+ defs = self.definitions(rel_path)
108
+ hits: dict[SymbolId, Definition] = {}
109
+ for line in lines:
110
+ covering = [d for d in defs if d.start <= line <= d.end]
111
+ funcs = [d for d in covering if d.kind == "function"]
112
+ pick = (
113
+ max(funcs, key=lambda d: d.start)
114
+ if funcs
115
+ else (max(covering, key=lambda d: d.start) if covering else None)
116
+ )
117
+ if pick:
118
+ hits[pick.symbol] = pick
119
+ return sorted(hits.values(), key=lambda d: (d.path, d.start))
120
+
121
+ def find(self, symbol: SymbolId) -> Definition | None:
122
+ rel = self.path_for(symbol)
123
+ if rel is None:
124
+ return None
125
+ for d in self.definitions(rel):
126
+ if d.symbol == symbol:
127
+ return d
128
+ return None
129
+
130
+ def path_for(self, symbol: SymbolId) -> str | None:
131
+ """Best-effort inverse of the naming scheme: longest module prefix that is a file."""
132
+ name = symbol.split(":", 1)[1]
133
+ parts = name.split(".")
134
+ for i in range(len(parts), 0, -1):
135
+ candidate = "/".join(parts[:i])
136
+ for roots, is_test in ((self.test_roots, True), (self.source_roots, False)):
137
+ for root in roots:
138
+ base = root
139
+ rel_prefix = root.relative_to(self.repo_root).parts if is_test else ()
140
+ cand_parts = parts[:i]
141
+ if is_test:
142
+ if tuple(cand_parts[: len(rel_prefix)]) != tuple(rel_prefix):
143
+ continue
144
+ cand_parts = cand_parts[len(rel_prefix) :]
145
+ candidate = "/".join(cand_parts)
146
+ for suffix in (".py", "/__init__.py"):
147
+ file = base / (candidate + suffix)
148
+ if file.is_file():
149
+ return str(file.resolve().relative_to(self.repo_root))
150
+ return None
151
+
152
+ def source(self, symbol: SymbolId, context: int = 0) -> str | None:
153
+ d = self.find(symbol)
154
+ if d is None:
155
+ return None
156
+ lines = (self.repo_root / d.path).read_text(encoding="utf-8").splitlines()
157
+ lo, hi = max(0, d.start - 1 - context), min(len(lines), d.end + context)
158
+ return "\n".join(f"{i + 1:5d} {lines[i]}" for i in range(lo, hi))
159
+
160
+ def enclosing_class(self, symbol: SymbolId) -> Definition | None:
161
+ d = self.find(symbol)
162
+ if d is None:
163
+ return None
164
+ classes = [
165
+ c for c in self.definitions(d.path)
166
+ if c.kind == "class" and c.start <= d.start and c.end >= d.end and c.symbol != d.symbol
167
+ ] # fmt: skip
168
+ return max(classes, key=lambda c: c.start) if classes else None
169
+
170
+ def is_test(self, symbol: SymbolId) -> bool:
171
+ d = self.find(symbol)
172
+ if d is None:
173
+ return False
174
+ path = self.repo_root / d.path
175
+ return any(path.is_relative_to(r) for r in self.test_roots)
176
+
177
+ def return_type_of(self, symbol: SymbolId) -> SymbolId | None:
178
+ d = self.find(symbol)
179
+ if d is None or d.kind != "function":
180
+ return None
181
+ try:
182
+ tree = ast.parse((self.repo_root / d.path).read_text(encoding="utf-8"))
183
+ except (SyntaxError, OSError):
184
+ return None
185
+ for node in ast.walk(tree):
186
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef) and node.lineno == d.start:
187
+ ann = node.returns
188
+ if ann is None:
189
+ return None
190
+ name = ast.unparse(ann)
191
+ if name.startswith("Optional[") and name.endswith("]"):
192
+ name = name[len("Optional[") : -1]
193
+ if name.endswith(" | None"):
194
+ name = name[: -len(" | None")]
195
+ cls = name.split(".")[-1]
196
+ if not cls.isidentifier():
197
+ return None
198
+ candidates = [
199
+ x
200
+ for x in self.all_definitions()
201
+ if x.kind == "class" and x.symbol.endswith("." + cls)
202
+ ]
203
+ return candidates[0].symbol if len(candidates) == 1 else None
204
+ return None
205
+
206
+ def all_definitions(self) -> list[Definition]:
207
+ if not getattr(self, "_all", None):
208
+ out: list[Definition] = []
209
+ for root in self.source_roots:
210
+ for f in sorted(root.rglob("*.py")):
211
+ try:
212
+ rel = str(f.resolve().relative_to(self.repo_root))
213
+ except ValueError:
214
+ continue
215
+ out.extend(self.definitions(rel))
216
+ self._all = out
217
+ return self._all
218
+
219
+ def module_of(self, symbol: SymbolId) -> str | None:
220
+ """Dotted module of a symbol: the longest prefix of its qualified name that is a file."""
221
+ rel = self.path_for(symbol)
222
+ return self.module_for(self.repo_root / rel) if rel else None
223
+
224
+ def tests_importing(self, module: str, limit: int = 3) -> list[str]:
225
+ """Test files that import the module (or its package), by name. Shallow and cheap."""
226
+ hits: list[str] = []
227
+ needles = {module, module.rsplit(".", 1)[0]}
228
+ for root in self.test_roots:
229
+ for file in sorted(root.rglob("test_*.py")):
230
+ try:
231
+ text = file.read_text(encoding="utf-8")
232
+ except (OSError, UnicodeDecodeError):
233
+ continue
234
+ if any(f"from {n} import" in text or f"import {n}" in text for n in needles):
235
+ hits.append(str(file.resolve().relative_to(self.repo_root)))
236
+ if len(hits) >= limit:
237
+ return hits
238
+ return hits
@@ -0,0 +1,130 @@
1
+ """pytest adapter for the CPython collector: traces the call phase of every test and writes
2
+ one `Execution` JSON per test.
3
+
4
+ python -m pytest -p diffgenome.collect.pytest_plugin \\
5
+ --diffgenome-out OUT --diffgenome-source-root . --diffgenome-test-root tests
6
+
7
+ The target repository is never written to: pass `-p no:cacheprovider` and set
8
+ PYTHONDONTWRITEBYTECODE=1 when the target is mounted read-only.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import re
15
+ import subprocess
16
+ from collections.abc import Generator
17
+ from pathlib import Path
18
+
19
+ import pytest
20
+
21
+ from diffgenome.collect.py_monitoring import COLLECTOR, EGRESS_GUARD, Tracer
22
+ from diffgenome.model import Collector, Execution, Stimulus
23
+ from diffgenome.serialize import execution_to_json
24
+
25
+
26
+ def pytest_addoption(parser: pytest.Parser) -> None:
27
+ group = parser.getgroup("diffgenome")
28
+ group.addoption("--diffgenome-out", required=True, help="directory for Execution JSON")
29
+ group.addoption(
30
+ "--diffgenome-source-root",
31
+ action="append",
32
+ default=[],
33
+ help="repository source root (repeatable); the first is the repository root",
34
+ )
35
+ group.addoption(
36
+ "--diffgenome-test-root", action="append", default=[], help="test root (repeatable)"
37
+ )
38
+ group.addoption(
39
+ "--diffgenome-repo-root",
40
+ default=None,
41
+ help="repository root that site ids and paths are relative to (default: first source root)",
42
+ )
43
+ group.addoption(
44
+ "--diffgenome-stimulus",
45
+ default="existing_test",
46
+ choices=[s.value for s in Stimulus],
47
+ help="how these executions were caused (generated probes must say so)",
48
+ )
49
+ group.addoption(
50
+ "--diffgenome-egress-guard",
51
+ action="store_true",
52
+ help="refuse socket connects and record attempts as OS-plane evidence",
53
+ )
54
+
55
+
56
+ def _revision(cwd: Path) -> str | None:
57
+ try:
58
+ out = subprocess.run(
59
+ ["git", "rev-parse", "HEAD"], cwd=cwd, capture_output=True, text=True, timeout=5
60
+ )
61
+ except (OSError, subprocess.TimeoutExpired):
62
+ return None
63
+ return out.stdout.strip() or None
64
+
65
+
66
+ class _Plugin:
67
+ def __init__(self, config: pytest.Config) -> None:
68
+ self.out = Path(config.getoption("--diffgenome-out")).resolve()
69
+ self.out.mkdir(parents=True, exist_ok=True)
70
+ roots = [Path(p).resolve() for p in config.getoption("--diffgenome-source-root")]
71
+ if not roots:
72
+ roots = [Path(config.rootpath).resolve()]
73
+ tests = [Path(p).resolve() for p in config.getoption("--diffgenome-test-root")]
74
+ # Site ids hash the repository-relative path (sites.py); a source root below the
75
+ # repository root (Baserow: backend/src) must not become the base. Experiment 10, F2.
76
+ repo_opt = config.getoption("--diffgenome-repo-root")
77
+ repo_root = Path(repo_opt).resolve() if repo_opt else roots[0]
78
+ self.tracer = Tracer(
79
+ repo_root=repo_root, source_roots=tuple(roots), test_roots=tuple(tests)
80
+ )
81
+ self.tracer.install_patch_hook()
82
+ self.stimulus = Stimulus(config.getoption("--diffgenome-stimulus"))
83
+ self.collectors: tuple[Collector, ...] = (COLLECTOR,)
84
+ if config.getoption("--diffgenome-egress-guard"):
85
+ self.tracer.install_egress_guard()
86
+ self.collectors = (COLLECTOR, EGRESS_GUARD)
87
+ self.revision = _revision(roots[0])
88
+
89
+ @pytest.hookimpl(wrapper=True)
90
+ def pytest_runtest_call(self, item: pytest.Item) -> Generator[None, None, None]:
91
+ function = getattr(item, "function", None)
92
+ code = getattr(function, "__code__", None)
93
+ root_symbol = (code and self.tracer.symbol_for_code(code)) or f"py:{item.nodeid}"
94
+ self.tracer.start(root_symbol, code)
95
+ outcome = "passed"
96
+ try:
97
+ return (yield)
98
+ except BaseException:
99
+ outcome = "failed"
100
+ raise
101
+ finally:
102
+ result = self.tracer.stop()
103
+ execution = Execution(
104
+ id=f"{item.nodeid}@{self.revision or 'unknown'}",
105
+ stimulus=self.stimulus,
106
+ stimulus_ref=item.nodeid,
107
+ outcome=outcome,
108
+ revision=self.revision,
109
+ collectors=self.collectors,
110
+ symbols=result.symbols,
111
+ nodes=result.nodes,
112
+ branches=result.branches,
113
+ diagnostics=(
114
+ ("stack_repairs", str(result.stack_repairs)),
115
+ ("attribution_disagreements", str(result.attribution_disagreements)),
116
+ ),
117
+ )
118
+ # Sanitized ids can collide (parametrized ids differing only in punctuation)
119
+ # and can exceed filesystem limits; the digest keeps every execution distinct.
120
+ digest = hashlib.sha256(item.nodeid.encode()).hexdigest()[:12]
121
+ name = re.sub(r"[^A-Za-z0-9_.-]+", "_", item.nodeid)[:100]
122
+ (self.out / f"{name}-{digest}.json").write_text(execution_to_json(execution))
123
+ if result.stack_repairs:
124
+ item.add_report_section(
125
+ "call", "diffgenome", f"stack repairs: {result.stack_repairs}"
126
+ )
127
+
128
+
129
+ def pytest_configure(config: pytest.Config) -> None:
130
+ config.pluginmanager.register(_Plugin(config), "diffgenome-collector")