deepdebug-local-agent 1.0.23 → 1.1.0

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 (168) hide show
  1. package/.claude/context.md +76 -0
  2. package/.claude/skills/README.md +25 -0
  3. package/.claude/skills/tdd-debugging/SKILL.md +53 -0
  4. package/.claude/skills/tdd-debugging/references/anti-patterns.md +43 -0
  5. package/.claude/skills/tdd-debugging/references/command-reference.md +46 -0
  6. package/.claude/standards/README.md +51 -0
  7. package/.claude/standards/clean-code.md +75 -0
  8. package/.claude/standards/code-review-checklist.md +61 -0
  9. package/.claude/standards/layered-architecture.md +163 -0
  10. package/.claude/standards/node.md +45 -0
  11. package/.claude/standards/security-owasp.md +114 -0
  12. package/.claude/standards/simplicity.md +42 -0
  13. package/.claude/standards/solid.md +64 -0
  14. package/.claude/standards/testing.md +59 -0
  15. package/.cursor/rules/standards.mdc +17 -0
  16. package/.github/workflows/build-byoc-image.yml +315 -0
  17. package/.github/workflows/deploy-gcp.yml +102 -0
  18. package/.github/workflows/npm-publish.yml +69 -8
  19. package/AGENTS.md +21 -0
  20. package/CHANGELOG.md +69 -0
  21. package/CLAUDE.md +13 -0
  22. package/CLAUDE_CODE_AGENT_INFINITE.md +5 -3
  23. package/Dockerfile +4 -0
  24. package/Dockerfile.full +55 -3
  25. package/bin/install.js +16 -5
  26. package/cloudbuild-full.yaml +57 -0
  27. package/docker-compose.yml +5 -3
  28. package/docs/OBSERVABILITY.md +164 -0
  29. package/docs/byoc-agent-image.md +70 -0
  30. package/docs/graphify-packaging-plan.md +133 -0
  31. package/docs/graphify-validation-run-2026-10-28.md +243 -0
  32. package/docs/releasing.md +43 -0
  33. package/docs/signed-commands.md +163 -0
  34. package/env.example +30 -0
  35. package/helm/Chart.yaml +2 -2
  36. package/helm/templates/deployment.yaml +13 -3
  37. package/helm/templates/networkpolicy.yaml +22 -0
  38. package/helm/templates/secret.yaml +1 -0
  39. package/helm/values.yaml +13 -14
  40. package/mcp-server.js +4 -1
  41. package/package.json +9 -4
  42. package/scripts/upgrade-observability-push.sh +227 -0
  43. package/src/ai-engine.js +22 -22
  44. package/src/config/trusted-signing-keys.json +3 -0
  45. package/src/detectors/build-tool-detector.js +50 -0
  46. package/src/detectors/multi-project-detector.js +22 -22
  47. package/src/detectors/service-detector.js +24 -24
  48. package/src/exec-utils.js +26 -4
  49. package/src/git/base-git-provider.js +117 -117
  50. package/src/git/bitbucket-provider.js +2 -2
  51. package/src/git/git-provider-registry.js +18 -18
  52. package/src/git/github-provider.js +13 -13
  53. package/src/git/multi-repo-pr.js +161 -0
  54. package/src/mcp-http-server.js +32 -16
  55. package/src/observability/bundle-files.js +418 -0
  56. package/src/observability/detect.js +151 -0
  57. package/src/observability/provision.js +212 -0
  58. package/src/observability/render-cli.js +59 -0
  59. package/src/observability/routes.js +88 -0
  60. package/src/patch/patch-engine.js +28 -28
  61. package/src/patch-manager.js +85 -85
  62. package/src/patch_bkp.js +14 -14
  63. package/src/ports.js +4 -4
  64. package/src/routes/workspace.route.js +47 -16
  65. package/src/runtimes/base-runtime.js +75 -75
  66. package/src/runtimes/java/gradle-runtime.js +17 -17
  67. package/src/runtimes/java/java-integrations.js +25 -25
  68. package/src/runtimes/java/maven-runtime.js +34 -34
  69. package/src/runtimes/node/node-integrations.js +1 -1
  70. package/src/runtimes/node/npm-runtime.js +14 -14
  71. package/src/runtimes/node/yarn-runtime.js +4 -4
  72. package/src/runtimes/runtime-registry.js +25 -25
  73. package/src/server-local.js +4 -4
  74. package/src/server.js +1366 -333
  75. package/src/siblings/burst-suppressor.js +89 -0
  76. package/src/siblings/capabilities.js +273 -0
  77. package/src/siblings/env-compat.cjs +58 -0
  78. package/src/siblings/incident-forwarder.js +320 -0
  79. package/src/siblings/logger.js +15 -0
  80. package/src/siblings/metrics.js +94 -0
  81. package/src/utils/agent-auth.js +184 -0
  82. package/src/utils/approval-token.js +56 -0
  83. package/src/utils/bind-host.js +29 -0
  84. package/src/utils/branch-refspec.js +98 -0
  85. package/src/utils/child-env.js +34 -0
  86. package/src/utils/environment-diagnostics.js +55 -55
  87. package/src/utils/exec-policy.js +962 -0
  88. package/src/utils/exec-utils.js +15 -15
  89. package/src/utils/file-count.js +46 -0
  90. package/src/utils/fs-utils.js +10 -10
  91. package/src/utils/multi-repo-open.js +132 -0
  92. package/src/utils/open-gate.js +183 -0
  93. package/src/utils/ranged-read.js +65 -0
  94. package/src/utils/request-guards.js +77 -0
  95. package/src/utils/request-signing.js +342 -0
  96. package/src/utils/root-recovery.js +244 -0
  97. package/src/utils/safe-exec-args.js +58 -0
  98. package/src/utils/sandbox.js +182 -0
  99. package/src/utils/search-outcome.js +56 -0
  100. package/src/utils/workspace-root.js +80 -0
  101. package/src/validators/syntax-validator.js +149 -0
  102. package/src/workspace/detect-port.js +16 -16
  103. package/src/workspace/file-reader.js +1 -1
  104. package/src/workspace/process-manager.js +46 -46
  105. package/src/workspace-manager.js +86 -24
  106. package/test/adapters/registry.test.js +9 -2
  107. package/test/agent-auth.test.js +194 -0
  108. package/test/bind-host.test.js +26 -0
  109. package/test/branch-refspec.test.js +224 -0
  110. package/test/burst-suppressor.test.js +60 -0
  111. package/test/capabilities-access.test.js +47 -0
  112. package/test/capabilities.test.js +87 -0
  113. package/test/child-env.test.js +23 -0
  114. package/test/env-compat.test.js +71 -0
  115. package/test/exec-policy.test.js +463 -0
  116. package/test/file-count.test.js +64 -0
  117. package/test/incident-forwarder.test.js +231 -0
  118. package/test/multi-repo-open.test.js +180 -0
  119. package/test/multi-repo-pr.test.js +129 -0
  120. package/test/observability-detect.test.js +199 -0
  121. package/test/open-gate.test.js +341 -0
  122. package/test/ranged-read.test.js +60 -0
  123. package/test/request-guards.test.js +91 -0
  124. package/test/request-signing.test.js +284 -0
  125. package/test/root-recovery-sidecar.test.js +171 -0
  126. package/test/root-recovery.test.js +274 -0
  127. package/test/safe-exec-args.test.js +58 -0
  128. package/test/sandbox.test.js +98 -0
  129. package/test/search-outcome.test.js +50 -0
  130. package/test/workspace-root.test.js +64 -0
  131. package/tools/graphify-validation/README.md +92 -0
  132. package/tools/graphify-validation/config/questions.json +31 -0
  133. package/tools/graphify-validation/config/repos.json +9 -0
  134. package/tools/graphify-validation/config/symbols.json +6 -0
  135. package/tools/graphify-validation/fixtures/spring/expected.json +34 -0
  136. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AppConfig.java +29 -0
  137. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AuditLogger.java +15 -0
  138. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/Cache.java +7 -0
  139. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/CheckoutService.java +25 -0
  140. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/InMemoryCache.java +18 -0
  141. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/OrderController.java +22 -0
  142. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaymentGateway.java +5 -0
  143. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaypalGateway.java +11 -0
  144. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/RefundService.java +14 -0
  145. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/StripeGateway.java +11 -0
  146. package/tools/graphify-validation/lib/affected_vs_rg.py +81 -0
  147. package/tools/graphify-validation/lib/audit_hook/sitecustomize.py +22 -0
  148. package/tools/graphify-validation/lib/graph_metrics.py +96 -0
  149. package/tools/graphify-validation/lib/gv_common.py +130 -0
  150. package/tools/graphify-validation/lib/licenses.py +47 -0
  151. package/tools/graphify-validation/lib/pip_audit_summary.py +18 -0
  152. package/tools/graphify-validation/lib/recall.py +115 -0
  153. package/tools/graphify-validation/lib/report.py +226 -0
  154. package/tools/graphify-validation/lib/run_all.py +237 -0
  155. package/tools/graphify-validation/lib/run_graphify.py +144 -0
  156. package/tools/graphify-validation/lib/rusage_helper.py +17 -0
  157. package/tools/graphify-validation/lib/sample_edges.py +246 -0
  158. package/tools/graphify-validation/lib/spring_fixture.py +81 -0
  159. package/tools/graphify-validation/requirements-py311.lock +508 -0
  160. package/tools/graphify-validation/requirements-tools.in +1 -0
  161. package/tools/graphify-validation/requirements-tools.lock +371 -0
  162. package/tools/graphify-validation/requirements.in +1 -0
  163. package/tools/graphify-validation/requirements.lock +502 -0
  164. package/tools/graphify-validation/run.sh +43 -0
  165. package/tools/graphify-validation/tests/test_harness.py +151 -0
  166. package/cloudbuild-agent-qa.yaml +0 -43
  167. package/cloudbuild-agent-staging.yaml +0 -40
  168. package/cloudbuild.yaml.deprecated +0 -42
@@ -0,0 +1,18 @@
1
+ """Condense pip-audit JSON into results/pip_audit.json (exit code kept: 0 clean, 1 vulnerabilities found)."""
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ src, rc, dest = sys.argv[1], int(sys.argv[2]), sys.argv[3]
7
+ vulns = []
8
+ try:
9
+ data = json.loads(Path(src).read_text())
10
+ for dep in data.get("dependencies", []):
11
+ for v in dep.get("vulns", []):
12
+ vulns.append({"package": dep["name"], "version": dep.get("version"), "id": v["id"], "fix": v.get("fix_versions"), "aliases": v.get("aliases", [])})
13
+ summary = "%d vulnerabilidades conhecidas em %d pacotes (exit %d)" % (len(vulns), len(data.get("dependencies", [])), rc)
14
+ except Exception as exc: # no JSON means pip-audit itself failed (network, resolver)
15
+ summary = "pip-audit nao produziu resultado (exit %d): %s" % (rc, exc)
16
+ Path(dest).parent.mkdir(parents=True, exist_ok=True)
17
+ Path(dest).write_text(json.dumps({"exit_code": rc, "vulnerabilities": vulns, "summary": summary}, indent=2))
18
+ print(summary)
@@ -0,0 +1,115 @@
1
+ """recall@8 of retrieval backends against a hand-written reference question set (G5).
2
+
3
+ Backends:
4
+ graph `graphify query "<question>" --budget N` -> distinct files in output order
5
+ lexical_auto ripgrep keyword ranking (the D14 fallback) with keywords taken from the question text
6
+ lexical_curated same ranking with human-picked keywords (optimistic ceiling: the author knew the answer)
7
+ vector optional: results exported from pgvector, passed with --vector-results
8
+ Test files and non-code files are removed from every ranking (loop context rule).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import re
14
+ import subprocess
15
+ from collections import Counter
16
+ from pathlib import Path
17
+
18
+ from gv_common import files_with_word, is_test_path, CODE_EXTS
19
+
20
+ SRC = re.compile(r"\[src=([^\s\]]*)")
21
+
22
+
23
+ def rank_graph(graphify, workdir, question, env, budget=8000, context=None):
24
+ args = [graphify, "query", question, "--budget", str(budget)]
25
+ if context:
26
+ args += ["--context", context]
27
+ res = subprocess.run(args, cwd=str(workdir), env=env, capture_output=True, text=True, timeout=300)
28
+ seen, order = set(), []
29
+ for m in SRC.finditer(res.stdout):
30
+ f = m.group(1)
31
+ if f and f not in seen:
32
+ seen.add(f)
33
+ order.append(f)
34
+ return order, res.returncode
35
+
36
+
37
+ STOP = set("where which what does that this with from into when then than have been being their there about "
38
+ "after before using used uses over under also each only some such code class classes file files "
39
+ "module modules component components function functions method methods before".split())
40
+
41
+
42
+ def auto_keywords(question):
43
+ """Fair keyword extraction from the question text only: no knowledge of the answer."""
44
+ toks = [t.lower() for t in re.findall(r"[A-Za-z][A-Za-z0-9]+", question)]
45
+ out = []
46
+ for t in toks:
47
+ if len(t) < 4 or t in STOP:
48
+ continue
49
+ stem = t[:5] if len(t) > 5 else t
50
+ if stem not in out:
51
+ out.append(stem)
52
+ return out
53
+
54
+
55
+ def rank_lexical(workdir, keywords, whole_word=True, ignore_case=False):
56
+ distinct = Counter()
57
+ for kw in keywords:
58
+ for f in files_with_word(workdir, kw, include_tests=False, whole_word=whole_word, ignore_case=ignore_case):
59
+ distinct[f] += 1
60
+ # also reward keyword appearance in the file name (cheap and typical for rg + fd)
61
+ for f in list(distinct):
62
+ base = Path(f).stem.lower()
63
+ for kw in keywords:
64
+ if kw.lower() in base:
65
+ distinct[f] += 1
66
+ return [f for f, _ in sorted(distinct.items(), key=lambda kv: (-kv[1], kv[0]))]
67
+
68
+
69
+ def clean(ranking):
70
+ return [f for f in ranking if Path(f).suffix in CODE_EXTS and not is_test_path(f)]
71
+
72
+
73
+ def recall_at(ranking, truth, k=8):
74
+ top = clean(ranking)[:k]
75
+ hit = [t for t in truth if t in top]
76
+ return len(hit) / len(truth), hit
77
+
78
+
79
+ def evaluate(graphify, workdir, repo, questions, env, vector=None, k=8):
80
+ rows = []
81
+ for q in questions:
82
+ truth = q["truth"]
83
+ missing = [t for t in truth if not (Path(workdir) / t).exists()]
84
+ if missing:
85
+ raise SystemExit("question %s: truth files not in snapshot: %s" % (q["id"], missing))
86
+ entry = {"id": q["id"], "kind": q["kind"], "question": q["question"], "truth": truth}
87
+ rank, rc = rank_graph(graphify, workdir, q["question"], env)
88
+ entry["graph"] = _score(rank, truth, k)
89
+ entry["graph"]["exit_code"] = rc
90
+ rank_c, _ = rank_graph(graphify, workdir, q["question"], env, context="call")
91
+ entry["graph_call_only"] = _score(rank_c, truth, k)
92
+ # lexical_curated: keywords picked by a human who knew the answer (optimistic ceiling, NOT a fair baseline)
93
+ entry["lexical_curated"] = _score(rank_lexical(workdir, q["keywords"], whole_word=False, ignore_case=True), truth, k)
94
+ # lexical_auto: keywords extracted from the question text only (fair baseline for the D14 ripgrep fallback)
95
+ entry["lexical_auto"] = _score(rank_lexical(workdir, auto_keywords(q["question"]), whole_word=False, ignore_case=True), truth, k)
96
+ if vector is not None:
97
+ entry["vector"] = _score(vector.get(q["id"], []), truth, k)
98
+ rows.append(entry)
99
+ backends = ["graph", "graph_call_only", "lexical_auto", "lexical_curated"] + (["vector"] if vector is not None else [])
100
+ agg = {}
101
+ for b in backends:
102
+ for kind in ("all", "symbolic", "conceptual"):
103
+ sel = [r for r in rows if kind == "all" or r["kind"] == kind]
104
+ if sel:
105
+ agg.setdefault(b, {})[kind] = {
106
+ "n": len(sel),
107
+ "recall_at_%d_pct" % k: round(100.0 * sum(r[b]["recall"] for r in sel) / len(sel), 1),
108
+ "hit_at_%d_pct" % k: round(100.0 * sum(1 for r in sel if r[b]["recall"] > 0) / len(sel), 1),
109
+ }
110
+ return {"k": k, "questions": rows, "aggregate": agg, "vector_measured": vector is not None}
111
+
112
+
113
+ def _score(ranking, truth, k):
114
+ r, hit = recall_at(ranking, truth, k)
115
+ return {"recall": round(r, 3), "hits": hit, "top": clean(ranking)[:k]}
@@ -0,0 +1,226 @@
1
+ """Render out/REPORT.md from out/results/*.json. Thresholds are the proposed G1-G10 limits (Local Agent GA plan)."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ from pathlib import Path
7
+
8
+ PASS, FAIL, PART, NM = "PASSA", "FALHA", "PARCIAL", "NAO MEDIDO"
9
+ T = {"g1_cov": 95.0, "g2_inf": 15.0, "g2_prec": 85.0, "g4_cold_s": 60.0, "g4_rss_mb": 1024.0, "g4_inc_s": 30.0,
10
+ "g5_calls": 90.0, "max_files_perf": 2000}
11
+
12
+
13
+ def load(out):
14
+ res = {}
15
+ for p in sorted((out / "results").glob("*.json")):
16
+ res[p.stem] = json.loads(p.read_text())
17
+ return res
18
+
19
+
20
+ def fmt(v, suffix=""):
21
+ return "n/d" if v is None else ("%s%s" % (v, suffix))
22
+
23
+
24
+ def repos(res):
25
+ return [k for k in res if k not in ("environment", "spring", "licenses", "pip_audit")]
26
+
27
+
28
+ def section_env(res):
29
+ e = res["environment"]
30
+ lines = ["## 0. Ambiente e versoes", "",
31
+ "| Item | Valor |", "|---|---|",
32
+ "| Data (UTC) | %s |" % e["date_utc"], "| Graphify | graphifyy==%s (fixado por hash) |" % e["graphify_version"],
33
+ "| Python | %s |" % e["python"], "| SO / CPU / RAM | %s / %s CPUs / %s GB |" % (e["os"], e["cpu_count"], e["ram_gb"]),
34
+ "| ripgrep | %s |" % e["ripgrep"],
35
+ "| Bloqueio de rede | %s; eficaz: %s |" % (e["network_block"], e.get("network_block_effective")),
36
+ "| Ambiente do processo | so %s (sem chaves LLM nem credenciais) |" % ", ".join(e["env_keys_passed"]), ""]
37
+ lines += ["| Repo | Ref | SHA | Ficheiros de codigo |", "|---|---|---|---|"]
38
+ for r in repos(res):
39
+ s = res[r].get("snapshot", {})
40
+ lines.append("| %s | %s | %s | %s |" % (r, s.get("ref"), (s.get("sha") or "")[:10], s.get("code_files")))
41
+ lines.append("")
42
+ return lines
43
+
44
+
45
+ def verdicts(res):
46
+ v = {}
47
+ rs = repos(res)
48
+ # G1
49
+ covs = {r: res[r]["cold"]["g1"]["coverage_pct"] for r in rs if "cold" in res[r]}
50
+ v["G1"] = (PASS if covs and all(c >= T["g1_cov"] for c in covs.values()) else (FAIL if covs else NM),
51
+ "; ".join("%s %s%% (%d sem simbolos)" % (r, covs[r], res[r]["cold"]["g1"]["files_without_symbols_total"]) for r in covs))
52
+ # G2
53
+ parts, ok_inf, ok_prec, have = [], True, True, False
54
+ for r in rs:
55
+ if "cold" not in res[r]:
56
+ continue
57
+ have = True
58
+ inf = res[r]["cold"]["g2"]["inferred_pct"]
59
+ ed = res[r].get("edges", {})
60
+ prec = ed.get("oracle_precision_pct")
61
+ ok_inf &= inf <= T["g2_inf"]
62
+ if prec is not None:
63
+ ok_prec &= prec >= T["g2_prec"]
64
+ parts.append("%s INFERRED %s%%, precisao oraculo %s%% (n=%s, decididas %s)" % (r, inf, fmt(prec), ed.get("sample_size", "n/d"), ed.get("oracle_decided", "n/d")))
65
+ if not have:
66
+ v["G2"] = (NM, "")
67
+ elif not (ok_inf and ok_prec):
68
+ v["G2"] = (FAIL, "; ".join(parts))
69
+ else:
70
+ v["G2"] = (PART, "; ".join(parts) + "; falta revisao manual por 2 pessoas")
71
+ # G3
72
+ sp = res.get("spring")
73
+ v["G3"] = ((PASS if sp["pass"] else FAIL), "interface->impl %s%% (limiar %s%%)" % (sp["headline_pct"], sp["threshold_pct"])) if sp else (NM, "")
74
+ # G4: FALHA only on wall time or on the largest single process; a tree sum above the limit is
75
+ # reported as PARCIAL because summed RSS double-counts pages shared between workers.
76
+ parts, fail, soft = [], False, False
77
+ for r in rs:
78
+ c = res[r].get("cold")
79
+ i = res[r].get("incremental")
80
+ if not c:
81
+ continue
82
+ run = c["run"]
83
+ small = res[r]["snapshot"]["code_files"] <= T["max_files_perf"]
84
+ s = "%s frio %ss, arvore %s MB, maior filho %s MB" % (r, run["wall_s"], run["peak_tree_rss_mb"], run["max_child_rss_mb"])
85
+ if small:
86
+ fail |= run["wall_s"] > T["g4_cold_s"] or run["max_child_rss_mb"] > T["g4_rss_mb"]
87
+ soft |= run["peak_tree_rss_mb"] > T["g4_rss_mb"]
88
+ if i:
89
+ ch = i["changed"]
90
+ s += "; incremental %ss, arvore %s MB, maior filho %s MB" % (ch["wall_s"], ch["peak_tree_rss_mb"], ch["max_child_rss_mb"])
91
+ fail |= ch["wall_s"] > T["g4_inc_s"] or ch["max_child_rss_mb"] > T["g4_rss_mb"]
92
+ soft |= ch["peak_tree_rss_mb"] > T["g4_rss_mb"]
93
+ parts.append(s)
94
+ note = "; maquina de desenvolvimento (nao 2 vCPU/4 GB)" + ("; soma de RSS da arvore acima de 1 GB em pelo menos uma corrida (pode contar paginas partilhadas)" if soft else "")
95
+ v["G4"] = (FAIL if fail else (PART if parts else NM), "; ".join(parts) + note)
96
+ # G5
97
+ parts, calls_ok = [], True
98
+ for r in rs:
99
+ a = res[r].get("affected")
100
+ rc = res[r].get("recall")
101
+ if a:
102
+ x = a["aggregate_main"]
103
+ calls_ok &= (x["recall_vs_rg_pct"] or 0) >= T["g5_calls"]
104
+ parts.append("%s affected recall vs rg %s%% (precisao %s%%)" % (r, fmt(x["recall_vs_rg_pct"]), fmt(x["precision_vs_rg_pct"])))
105
+ if rc:
106
+ g = rc["aggregate"].get("graph", {}).get("all", {})
107
+ lx = rc["aggregate"].get("lexical_auto", {}).get("all", {})
108
+ parts.append("%s recall@8 grafo %s%% vs lexical (palavras da pergunta) %s%%" % (r, fmt(g.get("recall_at_8_pct")), fmt(lx.get("recall_at_8_pct"))))
109
+ vec = any(res[r].get("recall", {}).get("vector_measured") for r in rs)
110
+ v["G5"] = ((PASS if (calls_ok and vec) else (FAIL if not calls_ok else PART)) if parts else NM,
111
+ "; ".join(parts) + ("" if vec else "; pgvector NAO medido (sem BD de QA)"))
112
+ v["G6"] = (NM, "exige agent2/LA com LLM e incidentes reais; fora do harness")
113
+ # G7
114
+ socks, pa = [], res.get("pip_audit")
115
+ for r in rs:
116
+ n = res[r].get("network")
117
+ if n and n.get("status") == "measured":
118
+ socks.append((r, n["run"]["audit"]["socket_events"], n["block_effective"], n["run"]["exit_code"]))
119
+ if socks:
120
+ zero = all(s[1] == 0 and s[3] == 0 for s in socks)
121
+ detail = "; ".join("%s: %d eventos socket, exit %s" % (s[0], s[1], s[3]) for s in socks)
122
+ pa_txt = ("pip-audit: %s" % pa["summary"]) if pa else "pip-audit nao corrido"
123
+ v["G7"] = ((PART if zero else FAIL), detail + "; " + pa_txt + "; comandos de rede desativados = requisito de empacotamento, nao testado aqui")
124
+ else:
125
+ v["G7"] = (NM, "")
126
+ # G8
127
+ det = [res[r]["determinism"]["same_as_first"] for r in rs if "determinism" in res[r]]
128
+ tr = {r: res[r]["snapshot"]["tracked_graphify_out_files"] for r in rs if "snapshot" in res[r]}
129
+ parts = ["determinismo %s" % ("igual em %d/%d repos" % (sum(det), len(det)) if det else "n/d")] + ["%s: %d ficheiros graphify-out versionados" % (r, n) for r, n in tr.items()]
130
+ v["G8"] = ((PASS if det and all(det) and not any(tr.values()) else FAIL) if det else NM, "; ".join(parts))
131
+ v["G9"] = (NM, "sem Docker local; ver docs/graphify-packaging-plan.md")
132
+ v["G10"] = (NM, "Java e JS cobertos por repos VH; Python/SQL/Go/Terraform e repo do piloto fora deste conjunto")
133
+ return v
134
+
135
+
136
+ def render(res):
137
+ L = ["# Relatorio de validacao do Graphify (harness reprodutivel)", "",
138
+ "Gerado por `tools/graphify-validation/run.sh`. Marcacao: PASSA/FALHA contra o limiar proposto; PARCIAL = medido mas falta um passo humano ou de ambiente; NAO MEDIDO = nao deu para medir aqui.", ""]
139
+ L += section_env(res)
140
+ L += ["## 1. Resumo por criterio", "", "| ID | Veredito | Medido |", "|---|---|---|"]
141
+ vs = verdicts(res)
142
+ for k in ["G%d" % i for i in range(1, 11)]:
143
+ L.append("| %s | %s | %s |" % (k, vs[k][0], vs[k][1].replace("|", "/")))
144
+ L.append("")
145
+ L += ["## 2. Detalhe por repo", ""]
146
+ for r in repos(res):
147
+ d = res[r]
148
+ L += ["### %s" % r, ""]
149
+ if "cold" in d:
150
+ c = d["cold"]
151
+ L += ["- Corrida a frio: %ss, pico de RSS da arvore %s MB (estimativa; maior filho %s MB), nos %s, arestas %s, graphify-out %s MB." % (
152
+ c["run"]["wall_s"], c["run"]["peak_tree_rss_mb"], c["run"]["max_child_rss_mb"], c["g2"]["nodes"], c["g2"]["edges"], c["graphify_out_mb"]),
153
+ "- G1: %s/%s ficheiros de codigo com simbolos (%s%%); %s sem simbolos, dos quais %s em testes. Lista: %s" % (c["g1"]["files_with_symbols"], c["g1"]["code_files"], c["g1"]["coverage_pct"],
154
+ c["g1"]["files_without_symbols_total"], c["g1"]["of_which_in_tests"], ", ".join(c["g1"]["files_without_symbols"][:8]) or "nenhum"),
155
+ "- G2: INFERRED %s%% (%s%% so em fontes main); INFERRED por relacao %s." % (c["g2"]["inferred_pct"], c["g2"]["inferred_pct_main_sources"], c["g2"]["inferred_by_relation"])]
156
+ if "determinism" in d:
157
+ L.append("- G8 determinismo: assinatura %s vs %s, igual: %s." % (d["cold"]["signature"]["sha256"], d["determinism"]["signature"]["sha256"], d["determinism"]["same_as_first"]))
158
+ if "incremental" in d:
159
+ i = d["incremental"]
160
+ L.append("- Incremental (1 ficheiro alterado: %s): %ss, %s MB; simbolo novo no grafo: %s; sem alteracoes: %ss." % (
161
+ i["probe_file"], i["changed"]["wall_s"], i["changed"]["peak_tree_rss_mb"], i["new_symbol_in_graph"], i["unchanged"]["wall_s"]))
162
+ if "network" in d and d["network"].get("status") == "measured":
163
+ n = d["network"]
164
+ a = n["run"]["audit"]
165
+ L.append("- G7 rede bloqueada (%s): exit %s, %d eventos socket, %d subprocess.Popen (%s)." % (n["mechanism"], n["run"]["exit_code"], a["socket_events"], a["subprocess_popen"], "; ".join(s[:70] for s in a["subprocess_samples"][:1])))
166
+ if "edges" in d:
167
+ e = d["edges"]
168
+ L.append("- G2 amostra (seed %s, n=%s): oraculo ok/wrong/unknown %s; precisao nas decididas %s%%; estrato ambiguo %s; estrato outros %s. Revisao manual: %s linhas." % (
169
+ e["seed"], e["sample_size"], e["oracle_counts"], e["oracle_precision_pct"], e["by_stratum"]["ambiguous"], e["by_stratum"]["other"], e["manual_reviewed"]))
170
+ if "affected" in d:
171
+ x = d["affected"]
172
+ L += ["", "`affected` (profundidade 1) contra `rg -w`, so fontes main:", "", "| Simbolo | grafo | rg | ambos | so grafo | so rg | recall vs rg | precisao vs rg |", "|---|---|---|---|---|---|---|---|"]
173
+ for s in x["symbols"]:
174
+ m = s["main"]
175
+ L.append("| %s (%s) | %d | %d | %d | %d | %d | %s | %s |" % (s["symbol"], s["graph_status"], m["graph"], m["rg"], m["both"], m["graph_only"], m["rg_only"], fmt(m["recall_vs_rg_pct"], "%"), fmt(m["precision_vs_rg_pct"], "%")))
176
+ L.append("")
177
+ L.append("Agregado (profundidade 1): recall %s%%, precisao %s%% (%s simbolos); profundidade 2: recall %s%%." % (fmt(x["aggregate_main"]["recall_vs_rg_pct"]), fmt(x["aggregate_main"]["precision_vs_rg_pct"]), x["aggregate_main"]["symbols_compared"], fmt(x["aggregate_main"]["depth2_recall_vs_rg_pct"])))
178
+ if "recall" in d:
179
+ rc = d["recall"]
180
+ L += ["", "recall@%d no conjunto de referencia (%d perguntas):" % (rc["k"], len(rc["questions"])), "", "| Backend | Tipo | n | recall@8 | acerto@8 |", "|---|---|---|---|---|"]
181
+ for b, kinds in rc["aggregate"].items():
182
+ for kind, m in kinds.items():
183
+ L.append("| %s | %s | %d | %s%% | %s%% |" % (b, kind, m["n"], m["recall_at_8_pct"], m["hit_at_8_pct"]))
184
+ L.append("")
185
+ L.append("`lexical_auto` usa so o texto da pergunta (baseline justo para o fallback ripgrep); `lexical_curated` usa palavras escolhidas por quem conhecia a resposta (teto otimista, nao comparar).")
186
+ if not rc["vector_measured"]:
187
+ L.append("| vector (pgvector) | - | - | NAO MEDIDO | NAO MEDIDO |")
188
+ L.append("")
189
+ sp = res.get("spring")
190
+ if sp:
191
+ L += ["## 3. Spring/DI (fixture sintetica)", "", "| Grupo | Encontrado | Esperado | % |", "|---|---|---|---|"]
192
+ for g, m in sp["groups"].items():
193
+ L.append("| %s | %d | %d | %s%% |" % (g, m["found"], m["expected"], m["pct"]))
194
+ L += ["", "Itens em falta:", ""]
195
+ for g, m in sp["groups"].items():
196
+ for it in m["items"]:
197
+ if not it["found"]:
198
+ L.append("- %s / %s: %s" % (g, it["id"], it["detail"]))
199
+ L.append("")
200
+ lic = res.get("licenses")
201
+ if lic:
202
+ s = lic["summary"]
203
+ L += ["## 4. Licencas das dependencias instaladas", "", "%d distribuicoes; permissivas %d; a rever %s; desconhecidas %s; sem ficheiro de licenca %s." % (s["total"], s["permissive"], s["review"] or "nenhuma", s["unknown"] or "nenhuma", s["without_license_file"] or "nenhuma"), ""]
204
+ pa = res.get("pip_audit")
205
+ if pa:
206
+ L += ["## 5. pip-audit", "", "%s" % pa["summary"], ""]
207
+ L += ["## 6. O que este relatorio nao mediu", "",
208
+ "- recall@8 contra pgvector: precisa do dataset do RagEvalController e da BD de QA (passar `--vector-results`).",
209
+ "- G6 (incidentes ponta a ponta sem Voyage), G9 (imagem local-agent-full) e G10 (repo do piloto, Python/SQL/Go/Terraform reais).",
210
+ "- Revisao manual por 2 pessoas das arestas INFERRED (o CSV em `review/` esta pronto para isso).",
211
+ "- Desempenho em 2 vCPU/4 GB e no hardware do piloto.", ""]
212
+ return "\n".join(L)
213
+
214
+
215
+ def main():
216
+ ap = argparse.ArgumentParser()
217
+ ap.add_argument("--out", required=True)
218
+ a = ap.parse_args()
219
+ out = Path(a.out)
220
+ res = load(out)
221
+ (out / "REPORT.md").write_text(render(res), encoding="utf-8")
222
+ print(out / "REPORT.md")
223
+
224
+
225
+ if __name__ == "__main__":
226
+ main()
@@ -0,0 +1,237 @@
1
+ """Orchestrator: snapshot -> graphify runs -> metrics -> JSON results (then report.py renders REPORT.md)."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import datetime as dt
6
+ import json
7
+ import os
8
+ import platform
9
+ import re
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
16
+
17
+ import affected_vs_rg # noqa: E402
18
+ import graph_metrics as gm # noqa: E402
19
+ import recall as recall_mod # noqa: E402
20
+ import run_graphify as rgf # noqa: E402
21
+ import sample_edges as se # noqa: E402
22
+ import spring_fixture as sf # noqa: E402
23
+ from gv_common import HARNESS_DIR, clean_env, dump_json, is_test_path, iter_code_files, load_json, log, rg_version # noqa: E402
24
+
25
+ ALL_STEPS = ["snapshot", "cold", "determinism", "network", "incremental", "edges", "affected", "recall", "spring", "licenses"]
26
+
27
+
28
+ def sh(args, cwd=None, check=True):
29
+ res = subprocess.run(args, cwd=cwd, capture_output=True, text=True)
30
+ if check and res.returncode != 0:
31
+ raise SystemExit("command failed: %s\n%s" % (" ".join(map(str, args)), res.stderr.strip()))
32
+ return res.stdout.strip()
33
+
34
+
35
+ def check_origin(clone, allowed):
36
+ url = sh(["git", "-C", str(clone), "remote", "get-url", "origin"])
37
+ owner = re.search(r"[:/]([^/:]+)/[^/]+?(?:\.git)?$", url)
38
+ if not owner or owner.group(1) not in allowed:
39
+ raise SystemExit("refusing %s: origin owner is not in allowed_origin_owners %s" % (clone, allowed))
40
+ return url
41
+
42
+
43
+ def snapshot(clone, ref, dest):
44
+ if dest.exists():
45
+ shutil.rmtree(dest)
46
+ dest.mkdir(parents=True)
47
+ archive = subprocess.Popen(["git", "-C", str(clone), "archive", ref], stdout=subprocess.PIPE)
48
+ subprocess.run(["tar", "-x", "-C", str(dest)], stdin=archive.stdout, check=True)
49
+ archive.wait()
50
+ if archive.returncode != 0:
51
+ raise SystemExit("git archive failed for %s %s" % (clone, ref))
52
+ shutil.rmtree(dest / "graphify-out", ignore_errors=True)
53
+
54
+
55
+ def venv_paths(venv):
56
+ return str(Path(venv) / "bin"), str(Path(venv) / "bin" / "graphify"), str(Path(venv) / "bin" / "python")
57
+
58
+
59
+ def graphify_version(venv_python):
60
+ out = sh([venv_python, "-c", "import importlib.metadata as m;print(m.version('graphifyy'))"])
61
+ return out
62
+
63
+
64
+ def main():
65
+ ap = argparse.ArgumentParser(description=__doc__)
66
+ ap.add_argument("--repos-dir", required=True, help="directory holding the git clones named as in config/repos.json")
67
+ ap.add_argument("--out", required=True, help="output directory (work copies, results, review sheets)")
68
+ ap.add_argument("--venv", required=True, help="venv created from requirements.lock (run.sh does this)")
69
+ ap.add_argument("--only", default=",".join(ALL_STEPS), help="comma separated subset of: " + ",".join(ALL_STEPS))
70
+ ap.add_argument("--repo", action="append", help="limit to these repo names")
71
+ ap.add_argument("--fetch", action="store_true", help="run `git fetch origin` in each clone first (only network use against repos)")
72
+ ap.add_argument("--vector-results", help="optional JSON {question_id: [files...]} exported from pgvector")
73
+ ap.add_argument("--manual-dir", help="directory with filled review CSVs named <repo>.csv")
74
+ a = ap.parse_args()
75
+
76
+ steps = [s for s in a.only.split(",") if s]
77
+ out = Path(a.out).resolve()
78
+ (out / "results").mkdir(parents=True, exist_ok=True)
79
+ cfg = load_json(HARNESS_DIR / "config" / "repos.json")
80
+ symbols = load_json(HARNESS_DIR / "config" / "symbols.json")
81
+ questions = load_json(HARNESS_DIR / "config" / "questions.json")
82
+ vbin, graphify, vpy = venv_paths(a.venv)
83
+ home = out / "home"
84
+ home.mkdir(exist_ok=True)
85
+ env = clean_env(home, vbin)
86
+ vector = load_json(a.vector_results) if a.vector_results else None
87
+ wrapper, mech = rgf.network_wrapper()
88
+
89
+ meta = {
90
+ "date_utc": dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
91
+ "graphify_version": graphify_version(vpy),
92
+ "python": sh([vpy, "--version"]),
93
+ "os": "%s %s %s" % (platform.system(), platform.release(), platform.machine()),
94
+ "cpu_count": os.cpu_count(),
95
+ "ram_gb": _ram_gb(),
96
+ "ripgrep": rg_version(),
97
+ "network_block": mech,
98
+ "env_keys_passed": sorted(env.keys()),
99
+ }
100
+ if wrapper:
101
+ ok, evidence = rgf.verify_block(wrapper, vpy)
102
+ meta["network_block_effective"] = ok
103
+ meta["network_block_probe"] = evidence
104
+ dump_json(out / "results" / "environment.json", meta)
105
+ log("graphify %s on %s" % (meta["graphify_version"], meta["os"]))
106
+
107
+ for repo in cfg["repos"]:
108
+ name = repo["name"]
109
+ if a.repo and name not in a.repo:
110
+ continue
111
+ clone = Path(a.repos_dir) / name
112
+ res_path = out / "results" / ("%s.json" % name)
113
+ res = load_json(res_path) if res_path.exists() else {"repo": name}
114
+ origin = check_origin(clone, cfg["allowed_origin_owners"])
115
+ if a.fetch:
116
+ sh(["git", "-C", str(clone), "fetch", "origin", "--quiet"])
117
+ ref = repo["ref"]
118
+ sha = sh(["git", "-C", str(clone), "rev-parse", ref])
119
+ work = out / "work" / name
120
+
121
+ if "snapshot" in steps:
122
+ log("%s: snapshot %s@%s" % (name, ref, sha[:8]))
123
+ snapshot(clone, ref, work)
124
+ tracked = [p for p in sh(["git", "-C", str(clone), "ls-tree", "-r", "--name-only", ref]).splitlines()]
125
+ symlinks = sh(["find", str(work), "-type", "l"], check=False).splitlines()
126
+ res["snapshot"] = {
127
+ "ref": ref, "sha": sha, "origin_owner_ok": True,
128
+ "code_files": len(iter_code_files(work)),
129
+ "tracked_graphify_out_files": sum(1 for p in tracked if p.startswith("graphify-out/")),
130
+ "symlinks_in_snapshot": len(symlinks),
131
+ }
132
+ if not work.exists():
133
+ raise SystemExit("no snapshot for %s; run the snapshot step first" % name)
134
+
135
+ if "cold" in steps:
136
+ log("%s: cold run" % name)
137
+ shutil.rmtree(work / "graphify-out", ignore_errors=True)
138
+ run = rgf.measure([graphify, "update", "."], work, env)
139
+ g = gm.load_graph(work)
140
+ res["cold"] = {"run": run, "g1": gm.g1_extraction(g, work), "g2": gm.g2_edges(g), "signature": gm.signature(g),
141
+ "graphify_out_mb": round(sum(f.stat().st_size for f in (work / "graphify-out").rglob("*") if f.is_file()) / 1e6, 1)}
142
+ if "determinism" in steps:
143
+ log("%s: second cold run (determinism)" % name)
144
+ work_b = out / "work" / (name + "-b")
145
+ snapshot(clone, ref, work_b)
146
+ run = rgf.measure([graphify, "update", "."], work_b, env)
147
+ sig = gm.signature(gm.load_graph(work_b))
148
+ res["determinism"] = {"run": run, "signature": sig, "same_as_first": sig == res.get("cold", {}).get("signature")}
149
+ shutil.rmtree(work_b, ignore_errors=True)
150
+ if "network" in steps:
151
+ log("%s: cold run with network blocked + audit hook" % name)
152
+ work_n = out / "work" / (name + "-net")
153
+ snapshot(clone, ref, work_n)
154
+ if wrapper is None:
155
+ res["network"] = {"status": "not_measured", "reason": mech}
156
+ else:
157
+ run = rgf.measure([graphify, "update", "."], work_n, env, wrapper=wrapper, audit_file=out / ("audit-%s.tsv" % name))
158
+ res["network"] = {"status": "measured", "mechanism": mech, "block_effective": meta.get("network_block_effective"), "run": run}
159
+ shutil.rmtree(work_n, ignore_errors=True)
160
+ if "incremental" in steps:
161
+ log("%s: incremental update" % name)
162
+ probe = _append_probe(work)
163
+ changed = rgf.measure([graphify, "update", "."], work, env)
164
+ found = any(n["label"] in ("GvProbe", ".gvProbeMethod()", "gvProbeFunction()") for n in gm.load_graph(work)["nodes"])
165
+ unchanged = rgf.measure([graphify, "update", "."], work, env)
166
+ res["incremental"] = {"probe_file": probe, "changed": changed, "new_symbol_in_graph": found, "unchanged": unchanged}
167
+ # restore the probe so later steps see the original sources
168
+ (work / probe).write_text(_strip_probe((work / probe).read_text()), encoding="utf-8")
169
+ if "edges" in steps:
170
+ log("%s: edge sample" % name)
171
+ g = gm.load_graph(work)
172
+ rows, smeta = se.build_sample(g, work)
173
+ rows = se.merge_manual(rows, (Path(a.manual_dir) / ("%s.csv" % name)) if a.manual_dir else None)
174
+ se.write_csv(rows, out / "review" / ("%s.csv" % name))
175
+ res["edges"] = se.summarize(rows, smeta)
176
+ if "affected" in steps or "recall" in steps:
177
+ # incremental step modified the graph; rebuild a pristine graph for query-based steps
178
+ shutil.rmtree(work / "graphify-out", ignore_errors=True)
179
+ rgf.measure([graphify, "update", "."], work, env)
180
+ g = gm.load_graph(work)
181
+ if "affected" in steps:
182
+ log("%s: affected vs ripgrep" % name)
183
+ res["affected"] = affected_vs_rg.compare(graphify, work, g, symbols[name], env)
184
+ if "recall" in steps:
185
+ log("%s: recall@8" % name)
186
+ res["recall"] = recall_mod.evaluate(graphify, work, name, questions[name], env, vector=vector)
187
+ dump_json(res_path, res)
188
+
189
+ if "spring" in steps:
190
+ log("spring fixture")
191
+ fx = out / "work" / "spring-fixture"
192
+ if fx.exists():
193
+ shutil.rmtree(fx)
194
+ shutil.copytree(HARNESS_DIR / "fixtures" / "spring", fx, ignore=shutil.ignore_patterns("expected.json"))
195
+ run = rgf.measure([graphify, "update", "."], fx, env)
196
+ g = gm.load_graph(fx)
197
+ result = sf.check(g, sf.load_expected(HARNESS_DIR / "fixtures" / "spring" / "expected.json"))
198
+ result["run"] = run
199
+ dump_json(out / "results" / "spring.json", result)
200
+ if "licenses" in steps:
201
+ log("licenses")
202
+ raw = sh([vpy, "-I", str(HARNESS_DIR / "lib" / "licenses.py")])
203
+ (out / "results" / "licenses.json").write_text(raw, encoding="utf-8")
204
+ log("done; now run: python3 lib/report.py --out %s" % out)
205
+
206
+
207
+ def _ram_gb():
208
+ try:
209
+ if platform.system() == "Darwin":
210
+ return round(int(sh(["sysctl", "-n", "hw.memsize"])) / 2**30, 1)
211
+ with open("/proc/meminfo") as fh:
212
+ return round(int(fh.readline().split()[1]) / 2**20, 1)
213
+ except Exception:
214
+ return None
215
+
216
+
217
+ def _append_probe(work):
218
+ """Append a new symbol to a deterministic main source file; return its repo-relative path."""
219
+ exts = (".java", ".js", ".mjs", ".ts")
220
+ cands = [f for f in iter_code_files(work) if f.endswith(exts) and not is_test_path(f) and (work / f).stat().st_size > 1500]
221
+ probe = sorted(cands)[0]
222
+ path = work / probe
223
+ text = path.read_text(encoding="utf-8")
224
+ if probe.endswith(".java"):
225
+ text += "\n// GV-PROBE-BEGIN\nclass GvProbe {\n int gvProbeMethod() { return 42; }\n}\n// GV-PROBE-END\n"
226
+ else:
227
+ text += "\n// GV-PROBE-BEGIN\nexport function gvProbeFunction() { return 42; }\n// GV-PROBE-END\n"
228
+ path.write_text(text, encoding="utf-8")
229
+ return probe
230
+
231
+
232
+ def _strip_probe(text):
233
+ return re.sub(r"\n// GV-PROBE-BEGIN.*?// GV-PROBE-END\n", "", text, flags=re.S)
234
+
235
+
236
+ if __name__ == "__main__":
237
+ main()