deepdebug-local-agent 1.2.0 → 1.4.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 (123) hide show
  1. package/.github/workflows/deploy-gcp.yml +58 -26
  2. package/CHANGELOG.md +13 -0
  3. package/docs/la-protocol-v2.md +68 -0
  4. package/docs/ws-metadata-only.md +44 -0
  5. package/env.example +7 -0
  6. package/package.json +2 -1
  7. package/scripts/sync-la-protocol-v2-contract.mjs +58 -0
  8. package/src/la-protocol-v2/chunks.js +45 -0
  9. package/src/la-protocol-v2/config.js +27 -0
  10. package/src/la-protocol-v2/content.js +57 -0
  11. package/src/la-protocol-v2/contract/CONTRACT_SOURCE.json +81 -0
  12. package/src/la-protocol-v2/contract/README.md +103 -0
  13. package/src/la-protocol-v2/contract/ack.schema.json +51 -0
  14. package/src/la-protocol-v2/contract/alert.schema.json +62 -0
  15. package/src/la-protocol-v2/contract/approval.decision.schema.json +57 -0
  16. package/src/la-protocol-v2/contract/audit.event.schema.json +86 -0
  17. package/src/la-protocol-v2/contract/config.update.schema.json +73 -0
  18. package/src/la-protocol-v2/contract/content.request.schema.json +110 -0
  19. package/src/la-protocol-v2/contract/content.response.schema.json +90 -0
  20. package/src/la-protocol-v2/contract/defs.schema.json +191 -0
  21. package/src/la-protocol-v2/contract/envelope.schema.json +89 -0
  22. package/src/la-protocol-v2/contract/error.schema.json +59 -0
  23. package/src/la-protocol-v2/contract/heartbeat.schema.json +100 -0
  24. package/src/la-protocol-v2/contract/hello.schema.json +111 -0
  25. package/src/la-protocol-v2/contract/incident.candidate.schema.json +102 -0
  26. package/src/la-protocol-v2/contract/index.json +166 -0
  27. package/src/la-protocol-v2/contract/job.assign.schema.json +212 -0
  28. package/src/la-protocol-v2/contract/job.cancel.schema.json +52 -0
  29. package/src/la-protocol-v2/contract/job.event.schema.json +115 -0
  30. package/src/la-protocol-v2/contract/job.result.schema.json +100 -0
  31. package/src/la-protocol-v2/contract/log.stats.schema.json +79 -0
  32. package/src/la-protocol-v2/contract/ready.schema.json +106 -0
  33. package/src/la-protocol-v2/contract/register.schema.json +92 -0
  34. package/src/la-protocol-v2/contract/scan.upload.schema.json +59 -0
  35. package/src/la-protocol-v2/contract/scan.uploaded.schema.json +51 -0
  36. package/src/la-protocol-v2/contract/webhook.forward.schema.json +69 -0
  37. package/src/la-protocol-v2/contract.js +90 -0
  38. package/src/la-protocol-v2/errors.js +34 -0
  39. package/src/la-protocol-v2/index.js +8 -0
  40. package/src/la-protocol-v2/link.js +93 -0
  41. package/src/la-protocol-v2/runtime.js +72 -0
  42. package/src/la-protocol-v2/session.js +650 -0
  43. package/src/la-protocol-v2/strict-json.js +47 -0
  44. package/src/mcp-http-server.js +5 -3
  45. package/src/server.js +70 -9
  46. package/src/utils/exec-policy.js +8 -1
  47. package/src/ws-channel/allowlist.js +143 -0
  48. package/src/ws-channel/config.js +22 -0
  49. package/src/ws-channel/connection-state.js +18 -0
  50. package/src/ws-channel/index.js +6 -0
  51. package/src/ws-channel/metadata-guard.js +78 -0
  52. package/src/ws-channel/policy-offline.js +40 -0
  53. package/src/ws-channel/transit-guard.js +55 -0
  54. package/test/contract/la-protocol-v2/vectors/ack.vectors.json +149 -0
  55. package/test/contract/la-protocol-v2/vectors/alert.vectors.json +101 -0
  56. package/test/contract/la-protocol-v2/vectors/approval.decision.vectors.json +165 -0
  57. package/test/contract/la-protocol-v2/vectors/audit.event.vectors.json +156 -0
  58. package/test/contract/la-protocol-v2/vectors/config.update.vectors.json +189 -0
  59. package/test/contract/la-protocol-v2/vectors/content.request.vectors.json +325 -0
  60. package/test/contract/la-protocol-v2/vectors/content.response.vectors.json +413 -0
  61. package/test/contract/la-protocol-v2/vectors/cross-type.vectors.json +260 -0
  62. package/test/contract/la-protocol-v2/vectors/error.vectors.json +179 -0
  63. package/test/contract/la-protocol-v2/vectors/heartbeat.vectors.json +109 -0
  64. package/test/contract/la-protocol-v2/vectors/hello.vectors.json +224 -0
  65. package/test/contract/la-protocol-v2/vectors/incident.candidate.vectors.json +186 -0
  66. package/test/contract/la-protocol-v2/vectors/job.assign.vectors.json +631 -0
  67. package/test/contract/la-protocol-v2/vectors/job.cancel.vectors.json +133 -0
  68. package/test/contract/la-protocol-v2/vectors/job.event.vectors.json +233 -0
  69. package/test/contract/la-protocol-v2/vectors/job.result.vectors.json +137 -0
  70. package/test/contract/la-protocol-v2/vectors/log.stats.vectors.json +137 -0
  71. package/test/contract/la-protocol-v2/vectors/ready.vectors.json +177 -0
  72. package/test/contract/la-protocol-v2/vectors/register.vectors.json +399 -0
  73. package/test/contract/la-protocol-v2/vectors/scan.upload.vectors.json +137 -0
  74. package/test/contract/la-protocol-v2/vectors/scan.uploaded.vectors.json +101 -0
  75. package/test/contract/la-protocol-v2/vectors/traces/invalid-agent-mismatch.json +140 -0
  76. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-after-last.json +200 -0
  77. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-byte-length-mismatch.json +174 -0
  78. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-last-flag.json +200 -0
  79. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-out-of-order.json +200 -0
  80. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-total-changes.json +200 -0
  81. package/test/contract/la-protocol-v2/vectors/traces/invalid-clock-skew.json +46 -0
  82. package/test/contract/la-protocol-v2/vectors/traces/invalid-event-before-ready.json +72 -0
  83. package/test/contract/la-protocol-v2/vectors/traces/invalid-first-frame-not-register.json +48 -0
  84. package/test/contract/la-protocol-v2/vectors/traces/invalid-frame-over-48kib.json +141 -0
  85. package/test/contract/la-protocol-v2/vectors/traces/invalid-la-sends-job-assign.json +173 -0
  86. package/test/contract/la-protocol-v2/vectors/traces/invalid-params-raw-differs-from-payload.json +172 -0
  87. package/test/contract/la-protocol-v2/vectors/traces/invalid-payload-above-limit.json +174 -0
  88. package/test/contract/la-protocol-v2/vectors/traces/invalid-resume-restarts-seq.json +144 -0
  89. package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-gap.json +166 -0
  90. package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-repeated.json +166 -0
  91. package/test/contract/la-protocol-v2/vectors/traces/invalid-sig-tenant-mismatch.json +172 -0
  92. package/test/contract/la-protocol-v2/vectors/traces/invalid-signature-warn-with-enforce-required.json +77 -0
  93. package/test/contract/la-protocol-v2/vectors/traces/invalid-tenant-mismatch.json +140 -0
  94. package/test/contract/la-protocol-v2/vectors/traces/invalid-type-outside-allowlist.json +140 -0
  95. package/test/contract/la-protocol-v2/vectors/traces/valid-content-chunks-in-order.json +224 -0
  96. package/test/contract/la-protocol-v2/vectors/traces/valid-handshake-and-job.json +266 -0
  97. package/test/contract/la-protocol-v2/vectors/traces/valid-resume-continues-seq.json +168 -0
  98. package/test/contract/la-protocol-v2/vectors/traces/valid-retransmit-is-idempotent.json +193 -0
  99. package/test/contract/la-protocol-v2/vectors/webhook.forward.vectors.json +170 -0
  100. package/test/la-protocol-v2/contract.test.js +131 -0
  101. package/test/la-protocol-v2/helpers/la-rig.js +83 -0
  102. package/test/la-protocol-v2/helpers/reference-cp.js +349 -0
  103. package/test/la-protocol-v2/helpers/spawn-agent.js +35 -0
  104. package/test/la-protocol-v2/link.test.js +141 -0
  105. package/test/la-protocol-v2/session-inbound.test.js +279 -0
  106. package/test/la-protocol-v2/session-outbound.test.js +382 -0
  107. package/test/la-protocol-v2/traces.test.js +270 -0
  108. package/test/la-protocol-v2/tunnel-e2e.test.js +124 -0
  109. package/test/ws-channel/allowlist.test.js +67 -0
  110. package/test/ws-channel/metadata-guard.test.js +104 -0
  111. package/test/ws-channel/policy-offline.test.js +99 -0
  112. package/test/ws-channel/tunnel-e2e.test.js +88 -0
  113. package/tools/graphify-validation/extras/README.md +19 -0
  114. package/tools/graphify-validation/extras/bymethod.py +35 -0
  115. package/tools/graphify-validation/extras/docker/Dockerfile +16 -0
  116. package/tools/graphify-validation/extras/docker/inner.sh +7 -0
  117. package/tools/graphify-validation/extras/g10.py +59 -0
  118. package/tools/graphify-validation/extras/g3aug.py +49 -0
  119. package/tools/graphify-validation/extras/hybrid.py +34 -0
  120. package/tools/graphify-validation/extras/truth2.py +44 -0
  121. package/tools/interop/LaV2NodeInteropTest.java +266 -0
  122. package/tools/interop/README.md +21 -0
  123. package/tools/interop/la-interop-client.mjs +97 -0
@@ -0,0 +1,99 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createConnectionState } from '../../src/ws-channel/connection-state.js';
4
+ import { createOfflinePolicyGuard, requiresPolicyGate, decideOffline } from '../../src/ws-channel/policy-offline.js';
5
+ import { decideExec } from '../../src/utils/exec-policy.js';
6
+
7
+ test('connection state: online only while open and fresh', () => {
8
+ let t = 0;
9
+ const c = createConnectionState({ now: () => t, staleAfterMs: 90_000 });
10
+ assert.equal(c.isOnline(), false);
11
+ c.markOpen();
12
+ assert.equal(c.isOnline(), true);
13
+ t = 60_000; c.markSeen();
14
+ t = 120_000;
15
+ assert.equal(c.isOnline(), true);
16
+ t = 200_000;
17
+ assert.equal(c.isOnline(), false, 'stale = offline');
18
+ c.markSeen(); // ignored while stale? still open, refreshes
19
+ assert.equal(c.isOnline(), true);
20
+ c.markClosed();
21
+ assert.equal(c.isOnline(), false);
22
+ c.markSeen();
23
+ assert.equal(c.isOnline(), false, 'a message on a closed socket does not revive it');
24
+ });
25
+
26
+ test('D19 classification: only PolicyGate-backed decisions need the gate', () => {
27
+ assert.equal(requiresPolicyGate('platform-tool:git_push'), true);
28
+ assert.equal(requiresPolicyGate('human-approval:ana'), true);
29
+ assert.equal(requiresPolicyGate('allowlist:git'), false);
30
+ assert.equal(requiresPolicyGate('policy-off'), false);
31
+ assert.deepEqual(decideOffline({ gated: true, online: false }).decision, 'DENY');
32
+ assert.deepEqual(decideOffline({ gated: true, online: true }).decision, 'ALLOW');
33
+ assert.deepEqual(decideOffline({ gated: false, online: false }).decision, 'ALLOW');
34
+ });
35
+
36
+ test('D19 guard: offline DENY for gated allows, allowlisted local commands unaffected', () => {
37
+ const state = { online: false };
38
+ const denied = [];
39
+ const guard = createOfflinePolicyGuard({ connection: { isOnline: () => state.online }, onDeny: (d) => denied.push(d) });
40
+ const gated = { allow: true, via: 'platform-tool:git_push' };
41
+ const local = { allow: true, via: 'allowlist:git' };
42
+
43
+ const d = guard(gated);
44
+ assert.equal(d.allow, false);
45
+ assert.equal(d.status, 403);
46
+ assert.equal(d.via, 'policy-gate-offline');
47
+ assert.deepEqual(denied, [{ via: 'platform-tool' }]);
48
+ assert.equal(guard(local), local);
49
+ assert.deepEqual(guard({ allow: false, via: 'rejected' }), { allow: false, via: 'rejected' });
50
+
51
+ state.online = true;
52
+ assert.equal(guard(gated), gated, 'online: PolicyGate path unchanged');
53
+ });
54
+
55
+ test('D19 via decideExec: a signed platform tool is denied offline, allowed online; flag off = unchanged', () => {
56
+ const input = {
57
+ command: 'git push origin feat/x', root: process.cwd(), headers: {},
58
+ body: { commandOrigin: 'platform', tool: 'git_push' }, vhSignature: { verified: true },
59
+ };
60
+ const base = { mode: 'enforce' };
61
+ assert.equal(decideExec(input, base).allow, true, 'flag off (no guard): unchanged');
62
+ let online = false;
63
+ const cfg = { mode: 'enforce', offlineGuard: createOfflinePolicyGuard({ connection: { isOnline: () => online } }) };
64
+ const off = decideExec(input, cfg);
65
+ assert.equal(off.allow, false);
66
+ assert.equal(off.via, 'policy-gate-offline');
67
+ online = true;
68
+ assert.equal(decideExec(input, cfg).allow, true);
69
+ });
70
+
71
+ test('D19 on the MCP bridge: /mcp/execute-command denies a platform tool offline, allows it online', async () => {
72
+ const net = await import('node:net');
73
+ const { startMCPHttpServer } = await import('../../src/mcp-http-server.js');
74
+ const { internalSignatureHeaders } = await import('../../src/utils/request-signing.js');
75
+ const port = await new Promise((resolve) => {
76
+ const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); });
77
+ });
78
+ let online = false;
79
+ const execPolicy = { mode: 'enforce', offlineGuard: createOfflinePolicyGuard({ connection: { isOnline: () => online } }) };
80
+ const app = startMCPHttpServer({ resolveRoot: () => process.cwd(), count: 0, list: () => [] }, port, execPolicy);
81
+ await new Promise((r) => (app.locals.server.listening ? r() : app.locals.server.once('listening', r)));
82
+ try {
83
+ const call = () => fetch(`http://127.0.0.1:${port}/mcp/execute-command`, {
84
+ method: 'POST',
85
+ headers: { 'content-type': 'application/json', ...internalSignatureHeaders() },
86
+ body: JSON.stringify({ workspaceId: 'w', command: 'git --version', commandOrigin: 'platform', tool: 'current_branch' }),
87
+ });
88
+ const off = await call();
89
+ const offBody = await off.json();
90
+ assert.equal(off.status, 403);
91
+ assert.equal(offBody.ok, false);
92
+ assert.match(String(offBody.reason), /control plane|offline|policy/i);
93
+ online = true;
94
+ const on = await call();
95
+ assert.notEqual(on.status, 403, 'online: PolicyGate path unchanged');
96
+ } finally {
97
+ app.locals.server.close();
98
+ }
99
+ });
@@ -0,0 +1,88 @@
1
+ // End-to-end: boots the real agent against a fake control-plane WS server and
2
+ // inspects what actually leaves the socket (T4-10).
3
+ import { test } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import net from 'node:net';
6
+ import os from 'node:os';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { spawn } from 'node:child_process';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { WebSocketServer } from 'ws';
12
+
13
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
14
+ const SECRET = 'TOP_SECRET_SOURCE_MARKER_8841';
15
+
16
+ const freePort = () => new Promise((res) => { const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => res(p)); }); });
17
+
18
+ async function run({ flag, disableTunnel = false }) {
19
+ const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'wsmeta-ws-'));
20
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'wsmeta-home-'));
21
+ fs.writeFileSync(path.join(ws, 'secret.txt'), SECRET);
22
+ const wss = new WebSocketServer({ port: 0, host: '127.0.0.1' });
23
+ await new Promise((r) => wss.on('listening', r));
24
+ const frames = [];
25
+ let connections = 0;
26
+ wss.on('connection', (sock) => {
27
+ connections++;
28
+ sock.on('message', (d) => {
29
+ const t = d.toString();
30
+ if (t === 'ping') return;
31
+ frames.push(JSON.parse(t));
32
+ if (frames[frames.length - 1].type === 'register') {
33
+ sock.send(JSON.stringify({ requestId: 'req-health', command: 'health', params: {} }));
34
+ sock.send(JSON.stringify({ requestId: 'req-file', command: 'workspace.file-content', params: { path: 'secret.txt', workspacePath: ws } }));
35
+ }
36
+ });
37
+ });
38
+ const env = { ...process.env, PORT: String(await freePort()), GATEWAY_URL: `http://127.0.0.1:${wss.address().port}`,
39
+ TENANT_ID: 'tenant-test', AUTH_TOKEN: 'test-token', WORKSPACE_ROOT: ws, HOME: home,
40
+ LOCAL_AGENT_SIGNATURE_MODE: 'off', ...(flag ? { LOCAL_AGENT_WS_METADATA_ONLY: flag } : {}),
41
+ ...(disableTunnel ? { DISABLE_WS_TUNNEL: 'true' } : {}) };
42
+ delete env.LOCAL_AGENT_TOKEN;
43
+ const child = spawn(process.execPath, ['src/server.js'], { cwd: ROOT, env, stdio: 'ignore' });
44
+ try {
45
+ const deadline = Date.now() + (disableTunnel ? 4000 : 20000);
46
+ while (Date.now() < deadline) {
47
+ if (frames.some((f) => f.requestId === 'req-file') && frames.some((f) => f.requestId === 'req-health')) break;
48
+ await new Promise((r) => setTimeout(r, 100));
49
+ }
50
+ } finally {
51
+ child.kill('SIGKILL');
52
+ for (const c of wss.clients) c.terminate();
53
+ await new Promise((r) => wss.close(r));
54
+ fs.rmSync(ws, { recursive: true, force: true });
55
+ fs.rmSync(home, { recursive: true, force: true });
56
+ }
57
+ return { frames, connections };
58
+ }
59
+
60
+ test('flag off (default): legacy channel is unchanged', async () => {
61
+ const { frames } = await run({});
62
+ const reg = frames.find((f) => f.type === 'register');
63
+ assert.ok(reg && 'workspacePath' in reg, 'legacy register still carries workspacePath');
64
+ const file = frames.find((f) => f.requestId === 'req-file');
65
+ assert.equal(file.content, SECRET, 'legacy content command still works with the flag off');
66
+ });
67
+
68
+ test('enforce: nothing but allowlisted metadata leaves the socket, content never does', async () => {
69
+ const { frames } = await run({ flag: 'enforce' });
70
+ assert.ok(frames.length >= 3, 'register + 2 replies expected');
71
+ assert.equal(JSON.stringify(frames).includes(SECRET), false);
72
+ assert.equal(JSON.stringify(frames).includes('secret.txt'), false);
73
+ const reg = frames.find((f) => f.type === 'register');
74
+ assert.equal(reg.workspacePath, undefined);
75
+ assert.match(reg.workspaceHash, /^[a-f0-9]{64}$/);
76
+ assert.deepEqual(frames.find((f) => f.requestId === 'req-file'),
77
+ { requestId: 'req-file', command: 'workspace.file-content', ok: false, errorCode: 'blocked_metadata_only' });
78
+ const health = frames.find((f) => f.requestId === 'req-health');
79
+ assert.equal(health.status, 'ok');
80
+ assert.equal('workspace' in health, false);
81
+ });
82
+
83
+ test('DISABLE_WS_TUNNEL=true: no socket is opened, whatever the flag', async () => {
84
+ for (const flag of [undefined, 'enforce']) {
85
+ const { connections } = await run({ flag, disableTunnel: true });
86
+ assert.equal(connections, 0, `flag=${flag}`);
87
+ }
88
+ });
@@ -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)