deepdebug-local-agent 1.3.0 → 1.5.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.
- package/.github/workflows/deploy-gcp.yml +58 -26
- package/CHANGELOG.md +6 -0
- package/docs/la-protocol-v2.md +102 -0
- package/package.json +2 -1
- package/scripts/sync-la-protocol-v2-contract.mjs +58 -0
- package/src/la-protocol-v2/chunks.js +45 -0
- package/src/la-protocol-v2/config.js +27 -0
- package/src/la-protocol-v2/content-handlers/audit.js +36 -0
- package/src/la-protocol-v2/content-handlers/config.js +48 -0
- package/src/la-protocol-v2/content-handlers/egress.js +65 -0
- package/src/la-protocol-v2/content-handlers/git-diff.js +92 -0
- package/src/la-protocol-v2/content-handlers/handler.js +170 -0
- package/src/la-protocol-v2/content-handlers/index.js +6 -0
- package/src/la-protocol-v2/content-handlers/install.js +50 -0
- package/src/la-protocol-v2/content-handlers/path-guard.js +142 -0
- package/src/la-protocol-v2/content-handlers/ref-registry.js +87 -0
- package/src/la-protocol-v2/content.js +63 -0
- package/src/la-protocol-v2/contract/CONTRACT_SOURCE.json +81 -0
- package/src/la-protocol-v2/contract/README.md +103 -0
- package/src/la-protocol-v2/contract/ack.schema.json +51 -0
- package/src/la-protocol-v2/contract/alert.schema.json +62 -0
- package/src/la-protocol-v2/contract/approval.decision.schema.json +57 -0
- package/src/la-protocol-v2/contract/audit.event.schema.json +86 -0
- package/src/la-protocol-v2/contract/config.update.schema.json +73 -0
- package/src/la-protocol-v2/contract/content.request.schema.json +110 -0
- package/src/la-protocol-v2/contract/content.response.schema.json +90 -0
- package/src/la-protocol-v2/contract/defs.schema.json +191 -0
- package/src/la-protocol-v2/contract/envelope.schema.json +89 -0
- package/src/la-protocol-v2/contract/error.schema.json +59 -0
- package/src/la-protocol-v2/contract/heartbeat.schema.json +100 -0
- package/src/la-protocol-v2/contract/hello.schema.json +111 -0
- package/src/la-protocol-v2/contract/incident.candidate.schema.json +102 -0
- package/src/la-protocol-v2/contract/index.json +166 -0
- package/src/la-protocol-v2/contract/job.assign.schema.json +212 -0
- package/src/la-protocol-v2/contract/job.cancel.schema.json +52 -0
- package/src/la-protocol-v2/contract/job.event.schema.json +115 -0
- package/src/la-protocol-v2/contract/job.result.schema.json +100 -0
- package/src/la-protocol-v2/contract/log.stats.schema.json +79 -0
- package/src/la-protocol-v2/contract/ready.schema.json +106 -0
- package/src/la-protocol-v2/contract/register.schema.json +92 -0
- package/src/la-protocol-v2/contract/scan.upload.schema.json +59 -0
- package/src/la-protocol-v2/contract/scan.uploaded.schema.json +51 -0
- package/src/la-protocol-v2/contract/webhook.forward.schema.json +69 -0
- package/src/la-protocol-v2/contract.js +90 -0
- package/src/la-protocol-v2/errors.js +34 -0
- package/src/la-protocol-v2/index.js +8 -0
- package/src/la-protocol-v2/link.js +93 -0
- package/src/la-protocol-v2/runtime.js +81 -0
- package/src/la-protocol-v2/session.js +650 -0
- package/src/la-protocol-v2/strict-json.js +47 -0
- package/src/server.js +25 -4
- package/test/contract/la-protocol-v2/vectors/ack.vectors.json +149 -0
- package/test/contract/la-protocol-v2/vectors/alert.vectors.json +101 -0
- package/test/contract/la-protocol-v2/vectors/approval.decision.vectors.json +165 -0
- package/test/contract/la-protocol-v2/vectors/audit.event.vectors.json +156 -0
- package/test/contract/la-protocol-v2/vectors/config.update.vectors.json +189 -0
- package/test/contract/la-protocol-v2/vectors/content.request.vectors.json +325 -0
- package/test/contract/la-protocol-v2/vectors/content.response.vectors.json +413 -0
- package/test/contract/la-protocol-v2/vectors/cross-type.vectors.json +260 -0
- package/test/contract/la-protocol-v2/vectors/error.vectors.json +179 -0
- package/test/contract/la-protocol-v2/vectors/heartbeat.vectors.json +109 -0
- package/test/contract/la-protocol-v2/vectors/hello.vectors.json +224 -0
- package/test/contract/la-protocol-v2/vectors/incident.candidate.vectors.json +186 -0
- package/test/contract/la-protocol-v2/vectors/job.assign.vectors.json +631 -0
- package/test/contract/la-protocol-v2/vectors/job.cancel.vectors.json +133 -0
- package/test/contract/la-protocol-v2/vectors/job.event.vectors.json +233 -0
- package/test/contract/la-protocol-v2/vectors/job.result.vectors.json +137 -0
- package/test/contract/la-protocol-v2/vectors/log.stats.vectors.json +137 -0
- package/test/contract/la-protocol-v2/vectors/ready.vectors.json +177 -0
- package/test/contract/la-protocol-v2/vectors/register.vectors.json +399 -0
- package/test/contract/la-protocol-v2/vectors/scan.upload.vectors.json +137 -0
- package/test/contract/la-protocol-v2/vectors/scan.uploaded.vectors.json +101 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-agent-mismatch.json +140 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-after-last.json +200 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-byte-length-mismatch.json +174 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-last-flag.json +200 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-out-of-order.json +200 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-total-changes.json +200 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-clock-skew.json +46 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-event-before-ready.json +72 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-first-frame-not-register.json +48 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-frame-over-48kib.json +141 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-la-sends-job-assign.json +173 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-params-raw-differs-from-payload.json +172 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-payload-above-limit.json +174 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-resume-restarts-seq.json +144 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-gap.json +166 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-repeated.json +166 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-sig-tenant-mismatch.json +172 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-signature-warn-with-enforce-required.json +77 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-tenant-mismatch.json +140 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-type-outside-allowlist.json +140 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-content-chunks-in-order.json +224 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-handshake-and-job.json +266 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-resume-continues-seq.json +168 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-retransmit-is-idempotent.json +193 -0
- package/test/contract/la-protocol-v2/vectors/webhook.forward.vectors.json +170 -0
- package/test/la-protocol-v2/content-handlers.test.js +633 -0
- package/test/la-protocol-v2/content-path-guard.test.js +214 -0
- package/test/la-protocol-v2/content-wiring.test.js +225 -0
- package/test/la-protocol-v2/contract.test.js +131 -0
- package/test/la-protocol-v2/helpers/content-rig.js +94 -0
- package/test/la-protocol-v2/helpers/la-rig.js +83 -0
- package/test/la-protocol-v2/helpers/reference-cp.js +349 -0
- package/test/la-protocol-v2/helpers/spawn-agent.js +36 -0
- package/test/la-protocol-v2/link.test.js +141 -0
- package/test/la-protocol-v2/session-inbound.test.js +279 -0
- package/test/la-protocol-v2/session-outbound.test.js +382 -0
- package/test/la-protocol-v2/traces.test.js +270 -0
- package/test/la-protocol-v2/tunnel-e2e.test.js +124 -0
- package/tools/graphify-validation/extras/README.md +19 -0
- package/tools/graphify-validation/extras/bymethod.py +35 -0
- package/tools/graphify-validation/extras/docker/Dockerfile +16 -0
- package/tools/graphify-validation/extras/docker/inner.sh +7 -0
- package/tools/graphify-validation/extras/g10.py +59 -0
- package/tools/graphify-validation/extras/g3aug.py +49 -0
- package/tools/graphify-validation/extras/hybrid.py +34 -0
- package/tools/graphify-validation/extras/truth2.py +44 -0
- package/tools/interop/LaV2NodeContentInteropTest.java +248 -0
- package/tools/interop/LaV2NodeInteropTest.java +266 -0
- package/tools/interop/README.md +38 -0
- package/tools/interop/la-interop-client.mjs +97 -0
- package/tools/interop/la-interop-content-client.mjs +91 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// F3-05b end to end: the real agent process (src/server.js) against the reference CP.
|
|
2
|
+
// - flag unset / false: the v1 client, nothing of v2 on the wire (characterization; the frames are the v1 ones)
|
|
3
|
+
// - flag true: v2 handshake with a signed ready, v1 commands still answered on the same connection
|
|
4
|
+
// - flag true against a gateway without v2: falls back to the v1 register on the same connection
|
|
5
|
+
// - flag true without an agent id: stays v1
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { ReferenceCpServer, TENANT, AGENT, waitFor } from "./helpers/reference-cp.js";
|
|
9
|
+
import { runAgent } from "./helpers/spawn-agent.js";
|
|
10
|
+
|
|
11
|
+
const baseEnv = (srv) => ({
|
|
12
|
+
TENANT_ID: TENANT,
|
|
13
|
+
LOCAL_AGENT_SIGNATURE_MODE: "enforce",
|
|
14
|
+
LOCAL_AGENT_SIGNING_PUBLIC_KEYS: `${srv.keyId}=${srv.publicKeyRaw}`,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
for (const flag of [undefined, "false", "off", "0", "bogus"]) {
|
|
18
|
+
test(`flag ${flag === undefined ? "unset" : JSON.stringify(flag)}: the v1 client, exactly: legacy register, no v2 frame, v1 command answered`, async () => {
|
|
19
|
+
const srv = await new ReferenceCpServer({}).start();
|
|
20
|
+
try {
|
|
21
|
+
await runAgent({
|
|
22
|
+
// everything v2 needs IS configured (UUID tenant, agent id, trusted key): only the flag keeps it off
|
|
23
|
+
srv, cfg: { authorizationId: AGENT, tenantId: TENANT }, env: { ...baseEnv(srv), LOCAL_AGENT_PROTOCOL_V2: flag },
|
|
24
|
+
until: async () => {
|
|
25
|
+
if (srv.ws && srv.legacy.some((f) => f.type === "register") && !srv.sent) {
|
|
26
|
+
srv.sent = true;
|
|
27
|
+
srv.ws.send(srv.cp.v1Command("health", {}));
|
|
28
|
+
}
|
|
29
|
+
return srv.legacy.some((f) => f.command === "health");
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const reg = srv.legacy.find((f) => f.type === "register");
|
|
33
|
+
assert.deepEqual(Object.keys(reg).sort(), ["port", "tenantId", "type", "workspacePath"]);
|
|
34
|
+
assert.equal(reg.tenantId, TENANT);
|
|
35
|
+
const health = srv.legacy.find((f) => f.command === "health");
|
|
36
|
+
assert.equal(health.status, "ok");
|
|
37
|
+
assert.equal(srv.cp.rawIn.length, 0, "no v2 frame was ever sent");
|
|
38
|
+
assert.equal(srv.cp.registers.length, 0);
|
|
39
|
+
assert.equal(srv.textsFromLa.some((t) => t.includes("schemaVersion")), false);
|
|
40
|
+
} finally { await srv.stop(); }
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("flag true: v2 handshake (register, hello, signed ready), no v1 register, and a v1 command is still answered on the same connection", async () => {
|
|
45
|
+
const srv = await new ReferenceCpServer({ requireEnforce: true }).start();
|
|
46
|
+
try {
|
|
47
|
+
await runAgent({
|
|
48
|
+
srv, cfg: { authorizationId: AGENT, tenantId: TENANT },
|
|
49
|
+
env: { ...baseEnv(srv), LOCAL_AGENT_PROTOCOL_V2: "true" },
|
|
50
|
+
until: async () => {
|
|
51
|
+
if (srv.cp.state === "ready" && !srv.sent) { srv.sent = true; srv.ws.send(srv.cp.v1Command("health", {})); }
|
|
52
|
+
return srv.legacy.some((f) => f.command === "health");
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
assert.equal(srv.connections, 1);
|
|
56
|
+
const reg = srv.cp.registers[0];
|
|
57
|
+
assert.equal(reg.protocol, 2);
|
|
58
|
+
assert.equal(reg.signatureMode, "enforce");
|
|
59
|
+
assert.match(reg.laVersion, /^\d+\.\d+\.\d+$/);
|
|
60
|
+
assert.deepEqual(reg.capabilities, ["chunk.base64", "protocol.v2", "v1.commands"]);
|
|
61
|
+
assert.ok(reg.workspaces.every((w) => /^[0-9a-f]{64}$/.test(w.pathSha256)), "only hashes of paths");
|
|
62
|
+
assert.equal(JSON.stringify(srv.cp.rawIn).includes("/tmp") || JSON.stringify(srv.cp.rawIn).includes("v2e2e-ws"), false, "no workspace path on the wire");
|
|
63
|
+
assert.equal(srv.cp.hellos[0].signatureMode, "enforce");
|
|
64
|
+
assert.equal(srv.legacy.some((f) => f.type === "register"), false, "no v1 register: v2 owns the registration");
|
|
65
|
+
assert.deepEqual(srv.cp.errorsSent, []);
|
|
66
|
+
assert.equal(srv.legacy.find((f) => f.command === "health").status, "ok", "v1 command on a v2 connection");
|
|
67
|
+
} finally { await srv.stop(); }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("flag true, the CP sends a signed v2 job.cancel: acked `rejected protocol_unsupported` (no handler yet), connection stays up", async () => {
|
|
71
|
+
const srv = await new ReferenceCpServer({ requireEnforce: true }).start();
|
|
72
|
+
try {
|
|
73
|
+
await runAgent({
|
|
74
|
+
srv, cfg: { authorizationId: AGENT, tenantId: TENANT },
|
|
75
|
+
env: { ...baseEnv(srv), LOCAL_AGENT_PROTOCOL_V2: "true" },
|
|
76
|
+
until: async () => {
|
|
77
|
+
if (srv.cp.state === "ready" && !srv.sent) { srv.sent = true; srv.cp.sendData("job.cancel", { jobId: "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f", reason: "user" }); }
|
|
78
|
+
return srv.cp.acksFromLa.length > 0;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
assert.deepEqual(srv.cp.acksFromLa, [{ ackSeq: 1, status: "rejected", errorCode: "protocol_unsupported" }]);
|
|
82
|
+
assert.deepEqual(srv.cp.errorsSent, []);
|
|
83
|
+
} finally { await srv.stop(); }
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("flag true against a gateway that does not speak v2: after the handshake timeout the v1 register goes out on the same connection", async () => {
|
|
87
|
+
const srv = await new ReferenceCpServer({ v2Disabled: true }).start();
|
|
88
|
+
try {
|
|
89
|
+
await runAgent({
|
|
90
|
+
srv, cfg: { authorizationId: AGENT, tenantId: TENANT },
|
|
91
|
+
env: { ...baseEnv(srv), LOCAL_AGENT_PROTOCOL_V2: "true", LOCAL_AGENT_V2_HANDSHAKE_MS: "300" },
|
|
92
|
+
until: async () => srv.legacy.some((f) => f.type === "register"),
|
|
93
|
+
});
|
|
94
|
+
const reg = srv.legacy.find((f) => f.type === "register");
|
|
95
|
+
assert.deepEqual(Object.keys(reg).sort(), ["port", "tenantId", "type", "workspacePath"]);
|
|
96
|
+
assert.equal(srv.connections, 1, "same connection");
|
|
97
|
+
} finally { await srv.stop(); }
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("flag true without an agent id (no authorizationId): v1 only, nothing of v2 on the wire", async () => {
|
|
101
|
+
const srv = await new ReferenceCpServer({}).start();
|
|
102
|
+
try {
|
|
103
|
+
await runAgent({
|
|
104
|
+
srv, env: { ...baseEnv(srv), LOCAL_AGENT_PROTOCOL_V2: "true" },
|
|
105
|
+
until: async () => srv.legacy.some((f) => f.type === "register"),
|
|
106
|
+
});
|
|
107
|
+
assert.equal(srv.cp.rawIn.length, 0);
|
|
108
|
+
assert.ok(srv.legacy.some((f) => f.type === "register"));
|
|
109
|
+
} finally { await srv.stop(); }
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("flag true with a tenant id that is not a UUID: v1 only", async () => {
|
|
113
|
+
const srv = await new ReferenceCpServer({}).start();
|
|
114
|
+
try {
|
|
115
|
+
await runAgent({
|
|
116
|
+
srv, cfg: { authorizationId: AGENT },
|
|
117
|
+
env: { ...baseEnv(srv), TENANT_ID: "tenant-test", LOCAL_AGENT_SIGNATURE_MODE: "off", LOCAL_AGENT_PROTOCOL_V2: "true" },
|
|
118
|
+
until: async () => srv.legacy.some((f) => f.type === "register"),
|
|
119
|
+
});
|
|
120
|
+
assert.equal(srv.cp.rawIn.length, 0);
|
|
121
|
+
} finally { await srv.stop(); }
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("sanity: waitFor is exported", () => assert.equal(typeof waitFor, "function"));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Graphify validation extras (T4-06, round 2, 2026-10-28)
|
|
2
|
+
|
|
3
|
+
Evidence scripts used for `10 Library/Plans/Fecho Dados 2026-10-28 T4.md`. They sit beside the harness and read its
|
|
4
|
+
output; nothing here is part of the agent runtime. Layout expected under `$GV_T4_ROOT`:
|
|
5
|
+
`harness/` (a `git archive` of this repo), `out/run1/` (a `run.sh --out` directory: `venv/`, `work/`, `results/`),
|
|
6
|
+
`bin/rg` (ripgrep wrapper), `g10/home/` (empty HOME), `pub/` (shallow clones of public Go/Terraform repos).
|
|
7
|
+
|
|
8
|
+
| Script | What it measures |
|
|
9
|
+
|---|---|
|
|
10
|
+
| `g10.py` | cold run, network denied, determinism and per-language file coverage for extra repos (Python, TS, Go, Terraform, SQL) |
|
|
11
|
+
| `truth2.py` | `affected` recall against a code-only ground truth (comment, import and test lines removed), class level and class+methods |
|
|
12
|
+
| `bymethod.py` | `affected` by class versus by method, against `rg -w` as the harness does |
|
|
13
|
+
| `hybrid.py` | recall@8 of graph, lexical, reciprocal-rank fusion and lexical-first interleave |
|
|
14
|
+
| `g3aug.py` | G3 Spring fixture with a wrapper-level "call via interface reaches implementations" post-process |
|
|
15
|
+
| `docker/` | G9: image from `requirements-py311.lock` (build context `tools/graphify-validation`), `inner.sh` measures wall time and cgroup peak |
|
|
16
|
+
|
|
17
|
+
Docker run used for the numbers: `docker run --rm --read-only --network none --user 10001:10001 --cpus=2 --memory=4g
|
|
18
|
+
--tmpfs /tmp -e HOME=/data/home -v DATA:/data -v inner.sh:/inner.sh:ro --entrypoint /bin/sh IMAGE /inner.sh REPO RUN`.
|
|
19
|
+
Public repos are only parsed (tree-sitter), never executed. No customer repository is involved.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys, json, pathlib, subprocess, os
|
|
3
|
+
SP=pathlib.Path(os.environ['GV_T4_ROOT'])
|
|
4
|
+
sys.path.insert(0,str(SP/'harness/tools/graphify-validation/lib'))
|
|
5
|
+
os.environ['RG']=str(SP/'bin/rg')
|
|
6
|
+
import affected_vs_rg as A, gv_common as gc, graph_metrics as gm
|
|
7
|
+
S=json.load(open(SP/'harness/tools/graphify-validation/config/symbols.json'))
|
|
8
|
+
env=gc.clean_env(SP/'g10/home',str(SP/'out/run1/venv/bin')); G=str(SP/'out/run1/venv/bin/graphify')
|
|
9
|
+
out={}
|
|
10
|
+
for repo in ['vh-api-gateway','visionhammer-agents','deepdebug-local-agent']:
|
|
11
|
+
wd=SP/'out/run1/work'/repo; g=gm.load_graph(wd)
|
|
12
|
+
syms=S[repo] if isinstance(S,dict) else S
|
|
13
|
+
rows=[]
|
|
14
|
+
for sym in syms:
|
|
15
|
+
cls=[n for n in g['nodes'] if n.get('label')==sym and n.get('source_file') and not n['label']==n['source_file'].rsplit('/',1)[-1]]
|
|
16
|
+
if not cls: rows.append({'symbol':sym,'status':'nodef'}); continue
|
|
17
|
+
defs={n['source_file'] for n in cls}
|
|
18
|
+
ids=[n['id'] for n in cls]
|
|
19
|
+
for n in g['nodes']:
|
|
20
|
+
if n.get('source_file') in defs and n.get('label','').startswith('.') and n['label'].endswith('()'):
|
|
21
|
+
ids.append(n['id'])
|
|
22
|
+
files=set()
|
|
23
|
+
for i in ids:
|
|
24
|
+
r=subprocess.run([G,'affected',i,'--depth','1'],cwd=str(wd),env=env,capture_output=True,text=True,timeout=300)
|
|
25
|
+
files|=A.parse_affected(r.stdout)
|
|
26
|
+
files-=defs
|
|
27
|
+
rgf=gc.files_with_word(wd,sym,include_tests=True)-defs
|
|
28
|
+
m=lambda s:{f for f in s if not gc.is_test_path(f)}
|
|
29
|
+
gf,rf=m(files),m(rgf)
|
|
30
|
+
rows.append({'symbol':sym,'methods':len(ids)-len(cls),'graph':len(gf),'rg':len(rf),'recall':round(100*len(gf&rf)/len(rf),1) if rf else None,'precision':round(100*len(gf&rf)/len(gf),1) if gf else None})
|
|
31
|
+
rr=[r for r in rows if r.get('recall') is not None]
|
|
32
|
+
tot_rg=sum(r['rg'] for r in rr); tot_both=sum(round(r['recall']*r['rg']/100) for r in rr)
|
|
33
|
+
out[repo]={'rows':rows,'agg_recall_pct':round(100*tot_both/tot_rg,1) if tot_rg else None}
|
|
34
|
+
print(repo,out[repo]['agg_recall_pct'],[(r['symbol'],r.get('methods'),r.get('recall')) for r in rows])
|
|
35
|
+
json.dump(out,open(SP/'g10/bymethod.json','w'),indent=1)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# T4-06 G9 test image: mirrors the python slice of the LA Dockerfile.full (node:20-slim = Debian 12, python3 3.11) + packaging plan 2.2
|
|
2
|
+
FROM node:20-slim AS base
|
|
3
|
+
RUN apt-get update && apt-get install -y --no-install-recommends git python3 python3-pip \
|
|
4
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
5
|
+
|
|
6
|
+
FROM base AS graphify
|
|
7
|
+
RUN apt-get update && apt-get install -y --no-install-recommends python3-venv ripgrep \
|
|
8
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
9
|
+
COPY requirements-py311.lock /tmp/graphify.lock
|
|
10
|
+
RUN python3 -m venv /opt/vh-graphify \
|
|
11
|
+
&& /opt/vh-graphify/bin/pip install --no-cache-dir --disable-pip-version-check \
|
|
12
|
+
--require-hashes --no-deps --only-binary=:all: -r /tmp/graphify.lock \
|
|
13
|
+
&& /opt/vh-graphify/bin/python -m compileall -q /opt/vh-graphify/lib \
|
|
14
|
+
&& rm -f /opt/vh-graphify/bin/graphify-mcp /tmp/graphify.lock
|
|
15
|
+
RUN useradd -u 10001 -m -d /nonexistent-home vhga 2>/dev/null || true
|
|
16
|
+
ENV PATH=/opt/vh-graphify/bin:$PATH PYTHONDONTWRITEBYTECODE=1 PYTHONHASHSEED=0
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# runs inside the container: $1 = repo dir under /data/ws
|
|
3
|
+
S=$(date +%s.%N)
|
|
4
|
+
graphify update /data/ws/$1 > /data/log-$1-$2.txt 2>&1; RC=$?
|
|
5
|
+
E=$(date +%s.%N)
|
|
6
|
+
PEAK=$(cat /sys/fs/cgroup/memory.peak 2>/dev/null)
|
|
7
|
+
echo "repo=$1 run=$2 rc=$RC wall=$(echo "$E $S" | awk '{printf "%.1f",$1-$2}')s cgroup_peak_mb=$(echo $PEAK | awk '{printf "%.0f",$1/1048576}')"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys, os, json, subprocess, shutil, pathlib, collections
|
|
3
|
+
LIB=os.path.join(os.environ['GV_T4_ROOT'],'harness/tools/graphify-validation/lib')
|
|
4
|
+
sys.path.insert(0, LIB)
|
|
5
|
+
import run_graphify as rgf, graph_metrics as gm, gv_common as gc
|
|
6
|
+
SP=pathlib.Path(os.environ['GV_T4_ROOT'])
|
|
7
|
+
VENV=SP/'out/run1/venv'; BIN=str(VENV/'bin'); G=str(VENV/'bin/graphify')
|
|
8
|
+
W=SP/'g10/work'; W.mkdir(exist_ok=True)
|
|
9
|
+
HOME=SP/'g10/home'; HOME.mkdir(exist_ok=True)
|
|
10
|
+
BASE='/Users/eduardooliveira/Documents/visionhammer projects/'
|
|
11
|
+
targets=[
|
|
12
|
+
('vh-litellm', BASE+'vh-litellm','origin/main'),
|
|
13
|
+
('visionhammer-pentest', BASE+'../x','') ,
|
|
14
|
+
('vision-heart-glow','%svision-heart-glow-3478cd8d'%BASE,'origin/main'),
|
|
15
|
+
('go-chi-chi', str(SP/'pub/chi'),'HEAD'),
|
|
16
|
+
('spf13-cobra', str(SP/'pub/cobra'),'HEAD'),
|
|
17
|
+
('tf-aws-vpc', str(SP/'pub/terraform-aws-vpc'),'HEAD'),
|
|
18
|
+
('tf-provider-random', str(SP/'pub/terraform-provider-random'),'HEAD'),
|
|
19
|
+
]
|
|
20
|
+
targets=[t for t in targets if t[2]]
|
|
21
|
+
targets.append(('visionhammer-pentest', str(pathlib.Path(os.environ.get('GV_PENTEST_CLONE','/nonexistent'))),'HEAD'))
|
|
22
|
+
wrap,mech=rgf.network_wrapper()
|
|
23
|
+
ok,why=rgf.verify_block(wrap,sys.executable)
|
|
24
|
+
print('network block',mech,ok,why)
|
|
25
|
+
EXTS={'.java':'java','.ts':'ts','.tsx':'ts','.js':'js','.jsx':'js','.mjs':'js','.py':'python','.go':'go','.tf':'terraform','.sql':'sql'}
|
|
26
|
+
res={}
|
|
27
|
+
for name,repo,ref in targets:
|
|
28
|
+
d=W/name
|
|
29
|
+
if d.exists(): shutil.rmtree(d)
|
|
30
|
+
d.mkdir(parents=True)
|
|
31
|
+
a=subprocess.Popen(['git','-C',repo,'archive',ref],stdout=subprocess.PIPE)
|
|
32
|
+
subprocess.run(['tar','-x','-C',str(d)],stdin=a.stdout,check=True); a.wait()
|
|
33
|
+
shutil.rmtree(d/'graphify-out',ignore_errors=True)
|
|
34
|
+
env=gc.clean_env(HOME,BIN)
|
|
35
|
+
audit=SP/('g10/audit-%s.tsv'%name)
|
|
36
|
+
r1=rgf.measure([G,'update','.'],d,env,wrapper=wrap,audit_file=audit)
|
|
37
|
+
g=gm.load_graph(d)
|
|
38
|
+
sig1=gm.signature(g)
|
|
39
|
+
# second cold
|
|
40
|
+
shutil.rmtree(d/'graphify-out')
|
|
41
|
+
r2=rgf.measure([G,'update','.'],d,env)
|
|
42
|
+
sig2=gm.signature(gm.load_graph(d))
|
|
43
|
+
# per-language coverage (own: includes sql), files with >=1 symbol
|
|
44
|
+
with_sym,_=gm.file_node_labels(g)
|
|
45
|
+
cnt=collections.defaultdict(lambda:[0,0])
|
|
46
|
+
for p in d.rglob('*'):
|
|
47
|
+
if p.is_file() and p.suffix in EXTS and 'node_modules' not in p.parts and 'graphify-out' not in p.parts and '.git' not in p.parts:
|
|
48
|
+
rel=str(p.relative_to(d)); lang=EXTS[p.suffix]
|
|
49
|
+
if gc.is_test_path(rel) and lang!='go': pass
|
|
50
|
+
cnt[lang][0]+=1
|
|
51
|
+
if rel in with_sym: cnt[lang][1]+=1
|
|
52
|
+
cov={k:{'files':v[0],'with_symbols':v[1],'pct':round(100*v[1]/v[0],1) if v[0] else None} for k,v in cnt.items()}
|
|
53
|
+
g2=gm.g2_edges(g)
|
|
54
|
+
rel=collections.Counter(e.get('relation') for e in gm.edges_of(g))
|
|
55
|
+
res[name]={'ref':ref,'cold':{k:r1[k] for k in ('exit_code','wall_s','peak_tree_rss_mb','max_child_rss_mb')},
|
|
56
|
+
'network_events':r1.get('audit'),'deterministic':sig1==sig2,'signature':sig1,'coverage_by_lang':cov,
|
|
57
|
+
'g1_harness':gm.g1_extraction(g,d),'inferred_pct':g2['inferred_pct'],'inferred_by_relation':g2['inferred_by_relation'],'relations_top':dict(rel.most_common(8))}
|
|
58
|
+
print(name, json.dumps({k:res[name][k] for k in ('cold','deterministic','coverage_by_lang','inferred_pct')}))
|
|
59
|
+
json.dump(res,open(SP/'g10/g10.json','w'),indent=1)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys, json, pathlib, os, copy, collections
|
|
3
|
+
SP=pathlib.Path(os.environ['GV_T4_ROOT'])
|
|
4
|
+
sys.path.insert(0,str(SP/'harness/tools/graphify-validation/lib'))
|
|
5
|
+
import spring_fixture as sf, graph_metrics as gm
|
|
6
|
+
wd=SP/'out/run1/work/spring-fixture'
|
|
7
|
+
g=gm.load_graph(wd)
|
|
8
|
+
exp=json.load(open(SP/'harness/tools/graphify-validation/fixtures/spring/expected.json'))
|
|
9
|
+
base=sf.check(g,exp)
|
|
10
|
+
print('baseline',{k:(v['found'],v['expected']) for k,v in base['groups'].items()})
|
|
11
|
+
def augment(g):
|
|
12
|
+
g=copy.deepcopy(g); es=gm.edges_of(g)
|
|
13
|
+
byid={n['id']:n for n in g['nodes']}
|
|
14
|
+
# class of a method node = node in same file with non-method label (first)
|
|
15
|
+
file_cls={}
|
|
16
|
+
for n in g['nodes']:
|
|
17
|
+
sfile=n.get('source_file'); lab=n.get('label','')
|
|
18
|
+
if sfile and not lab.startswith('.') and lab!=os.path.basename(sfile): file_cls.setdefault(sfile,n['id'])
|
|
19
|
+
impls=collections.defaultdict(list) # interface class id -> impl class ids
|
|
20
|
+
for e in es:
|
|
21
|
+
if e.get('relation') in ('implements','inherits','extends'): impls[e['target']].append(e['source'])
|
|
22
|
+
methods=collections.defaultdict(dict) # class id -> {label: id}
|
|
23
|
+
for n in g['nodes']:
|
|
24
|
+
lab=n.get('label','')
|
|
25
|
+
if lab.startswith('.') and n.get('source_file') in file_cls: methods[file_cls[n['source_file']]][lab]=n['id']
|
|
26
|
+
new=[]
|
|
27
|
+
for e in es:
|
|
28
|
+
if e.get('relation')!='calls': continue
|
|
29
|
+
t=byid.get(e['target']);
|
|
30
|
+
if not t or t.get('source_file') not in file_cls: continue
|
|
31
|
+
iface=file_cls[t['source_file']]
|
|
32
|
+
for c in impls.get(iface,[]):
|
|
33
|
+
m=methods[c].get(t['label'])
|
|
34
|
+
if m: new.append({**e,'target':m,'relation':'calls','confidence':'INFERRED','note':'via-interface (wrapper)'})
|
|
35
|
+
# field-type edges (references) + implements: class A references interface I => A reaches impl classes (depth)
|
|
36
|
+
for e in es:
|
|
37
|
+
if e.get('relation')=='references' and e['target'] in impls:
|
|
38
|
+
for c in impls[e['target']]: new.append({**e,'target':c,'relation':'references','confidence':'INFERRED'})
|
|
39
|
+
g['links']=es+new if 'links' in g else g.get('edges',[])+new
|
|
40
|
+
if 'edges' in g and 'links' not in g: g['edges']=es+new
|
|
41
|
+
return g,len(new)
|
|
42
|
+
g2,n=augment(g)
|
|
43
|
+
aug=sf.check(g2,exp)
|
|
44
|
+
print('augmented (+%d synthetic edges)'%n,{k:(v['found'],v['expected']) for k,v in aug['groups'].items()})
|
|
45
|
+
print('interface_impl pct',base['groups']['interface_impl']['pct'],'->',aug['groups']['interface_impl']['pct'])
|
|
46
|
+
for k,v in aug['groups'].items():
|
|
47
|
+
for it in v['items']:
|
|
48
|
+
if not it['found']: print(' still failing',k,it['id'],it['detail'])
|
|
49
|
+
json.dump({'baseline':{k:v['pct'] for k,v in base['groups'].items()},'augmented':{k:v['pct'] for k,v in aug['groups'].items()},'synthetic_edges':n},open(SP/'g10/g3aug.json','w'))
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys, json, pathlib
|
|
3
|
+
SP=pathlib.Path(os.environ['GV_T4_ROOT'])
|
|
4
|
+
sys.path.insert(0,str(SP/'harness/tools/graphify-validation/lib'))
|
|
5
|
+
import recall as R, gv_common as gc
|
|
6
|
+
import os
|
|
7
|
+
os.environ['RG']=str(SP/'bin/rg')
|
|
8
|
+
Q=json.load(open(SP/'harness/tools/graphify-validation/config/questions.json'))
|
|
9
|
+
env=gc.clean_env(SP/'g10/home',str(SP/'out/run1/venv/bin'))
|
|
10
|
+
G=str(SP/'out/run1/venv/bin/graphify')
|
|
11
|
+
def rrf(*ranks,k=60):
|
|
12
|
+
s={}
|
|
13
|
+
for r in ranks:
|
|
14
|
+
for i,f in enumerate(R.clean(r)): s[f]=s.get(f,0)+1/(k+i+1)
|
|
15
|
+
return [f for f,_ in sorted(s.items(),key=lambda kv:-kv[1])]
|
|
16
|
+
def interleave(a,b):
|
|
17
|
+
out=[]
|
|
18
|
+
for x,y in zip(R.clean(a),R.clean(b)):
|
|
19
|
+
for z in (y,x):
|
|
20
|
+
if z not in out: out.append(z)
|
|
21
|
+
return out + [f for f in R.clean(a)+R.clean(b) if f not in out]
|
|
22
|
+
res={}
|
|
23
|
+
for repo,wd in [('vh-api-gateway',SP/'out/run1/work/vh-api-gateway'),('visionhammer-agents',SP/'out/run1/work/visionhammer-agents'),('deepdebug-local-agent',SP/'out/run1/work/deepdebug-local-agent')]:
|
|
24
|
+
rows=[]
|
|
25
|
+
for q in Q[repo]:
|
|
26
|
+
g,_=R.rank_graph(G,wd,q['question'],env)
|
|
27
|
+
l=R.rank_lexical(wd,R.auto_keywords(q['question']),whole_word=False,ignore_case=True)
|
|
28
|
+
rows.append({'kind':q['kind'],'graph':R.recall_at(g,q['truth'])[0],'lex':R.recall_at(l,q['truth'])[0],
|
|
29
|
+
'rrf':R.recall_at(rrf(g,l),q['truth'])[0],'inter':R.recall_at(interleave(l,g),q['truth'])[0]})
|
|
30
|
+
def avg(sel,k): return round(100*sum(r[k] for r in sel)/len(sel),1) if sel else None
|
|
31
|
+
res[repo]={kind:{k:avg([r for r in rows if kind=='all' or r['kind']==kind],k) for k in ('graph','lex','rrf','inter')}|{'n':len([r for r in rows if kind=='all' or r['kind']==kind])} for kind in ('all','symbolic','conceptual')}
|
|
32
|
+
print(repo,json.dumps(res[repo]))
|
|
33
|
+
tot=[];
|
|
34
|
+
json.dump(res,open(SP/'g10/hybrid.json','w'),indent=1)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys, json, pathlib, subprocess, os, re
|
|
3
|
+
SP=pathlib.Path(os.environ['GV_T4_ROOT'])
|
|
4
|
+
sys.path.insert(0,str(SP/'harness/tools/graphify-validation/lib'))
|
|
5
|
+
os.environ['RG']=str(SP/'bin/rg')
|
|
6
|
+
import affected_vs_rg as A, gv_common as gc, graph_metrics as gm
|
|
7
|
+
S=json.load(open(SP/'harness/tools/graphify-validation/config/symbols.json'))
|
|
8
|
+
env=gc.clean_env(SP/'g10/home',str(SP/'out/run1/venv/bin')); G=str(SP/'out/run1/venv/bin/graphify')
|
|
9
|
+
SKIP=re.compile(r'^\s*(\*|//|/\*|#|import\s|from\s|package\s|\*/)')
|
|
10
|
+
def code_files(wd,word):
|
|
11
|
+
r=subprocess.run([str(SP/'bin/rg'),'-w','-n','--no-heading','--glob','*.java','--glob','*.js','--glob','*.mjs','--glob','*.ts','--glob','*.tsx','--glob','*.py','--glob','!**/test/**','--glob','!**/tests/**','--glob','!**/node_modules/**','--glob','!graphify-out/**',word,'.'],cwd=str(wd),capture_output=True,text=True)
|
|
12
|
+
fs=set()
|
|
13
|
+
for ln in r.stdout.splitlines():
|
|
14
|
+
m=re.match(r'\./([^:]+):\d+:(.*)',ln)
|
|
15
|
+
if not m: continue
|
|
16
|
+
f,t=m.groups()
|
|
17
|
+
if SKIP.match(t) or gc.is_test_path(f): continue
|
|
18
|
+
fs.add(f)
|
|
19
|
+
return fs
|
|
20
|
+
out={}
|
|
21
|
+
tot={'class':[0,0,0],'union':[0,0,0]}
|
|
22
|
+
for repo in ['vh-api-gateway','visionhammer-agents','deepdebug-local-agent']:
|
|
23
|
+
wd=SP/'out/run1/work'/repo; g=gm.load_graph(wd); rows=[]
|
|
24
|
+
for sym in S[repo]:
|
|
25
|
+
cls=[n for n in g['nodes'] if n.get('label')==sym and n.get('source_file') and n['label']!=n['source_file'].rsplit('/',1)[-1]]
|
|
26
|
+
if not cls: continue
|
|
27
|
+
defs={n['source_file'] for n in cls}
|
|
28
|
+
ids=[n['id'] for n in cls]
|
|
29
|
+
meth=[n['id'] for n in g['nodes'] if n.get('source_file') in defs and n.get('label','').startswith('.') and n['label'].endswith('()')]
|
|
30
|
+
def aff(idl):
|
|
31
|
+
fs=set()
|
|
32
|
+
for i in idl:
|
|
33
|
+
r=subprocess.run([G,'affected',i,'--depth','1'],cwd=str(wd),env=env,capture_output=True,text=True,timeout=300); fs|=A.parse_affected(r.stdout)
|
|
34
|
+
return {f for f in fs-defs if not gc.is_test_path(f)}
|
|
35
|
+
truth=code_files(wd,sym)-defs
|
|
36
|
+
a_c=aff(ids); a_u=aff(ids+meth)
|
|
37
|
+
row={'symbol':sym,'truth_code_files':len(truth),'class':[len(a_c),len(a_c&truth)],'union':[len(a_u),len(a_u&truth)]}
|
|
38
|
+
rows.append(row)
|
|
39
|
+
for k,idx in (('class','class'),('union','union')):
|
|
40
|
+
tt=sum(r['truth_code_files'] for r in rows); bb=sum(r[idx][1] for r in rows); gg=sum(r[idx][0] for r in rows)
|
|
41
|
+
out.setdefault(repo,{})[k]={'recall_pct':round(100*bb/tt,1) if tt else None,'precision_pct':round(100*bb/gg,1) if gg else None,'truth':tt}
|
|
42
|
+
out[repo]['rows']=rows
|
|
43
|
+
print(repo,json.dumps({k:out[repo][k] for k in ('class','union')}),[(r['symbol'],r['truth_code_files'],r['class'][1],r['union'][1]) for r in rows])
|
|
44
|
+
json.dump(out,open(SP/'g10/truth2.json','w'),indent=1)
|