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,31 @@
1
+ {
2
+ "_comment": "Reference questions with hand-checked ground truth (files that implement the thing asked). kind=symbolic names a symbol; kind=conceptual does not. `keywords` feed only the lexical (ripgrep) baseline. Add or extend per repo; truth paths are validated against the snapshot on every run.",
3
+ "vh-api-gateway": [
4
+ {"id": "gw-1", "kind": "symbolic", "question": "Where is the PolicyGate that decides ALLOW, DENY or REQUIRE_HUMAN for an action?", "keywords": ["PolicyGate", "decide"], "truth": ["src/main/java/io/visionhammer/gateway/governance/domain/PolicyGate.java"]},
5
+ {"id": "gw-2", "kind": "symbolic", "question": "Which class checks that a tenant owns a workspace before allowing access (TenantAccessGuard)?", "keywords": ["TenantAccessGuard", "requireOwnedWorkspace"], "truth": ["src/main/java/io/visionhammer/gateway/security/TenantAccessGuard.java"]},
6
+ {"id": "gw-3", "kind": "conceptual", "question": "Which code erases a tenant's data when a deletion request is made?", "keywords": ["erasure", "erase", "tenant"], "truth": ["src/main/java/io/visionhammer/gateway/tenancy/application/DataErasureService.java"]},
7
+ {"id": "gw-4", "kind": "conceptual", "question": "How are login attempts rate limited on the authentication endpoints?", "keywords": ["ratelimit", "RateLimiter", "login", "attempts"], "truth": ["src/main/java/io/visionhammer/gateway/identity/application/AuthRateLimiter.java", "src/main/java/io/visionhammer/gateway/identity/api/AuthPathRateLimitFilter.java"]},
8
+ {"id": "gw-5", "kind": "conceptual", "question": "Where does the gateway send transactional email through the Resend provider?", "keywords": ["Resend", "email", "send"], "truth": ["src/main/java/io/visionhammer/gateway/shared/mail/ResendEmailSender.java"]},
9
+ {"id": "gw-6", "kind": "conceptual", "question": "Where are incoming Slack requests verified with a signature before being processed?", "keywords": ["Slack", "signature", "verify"], "truth": ["src/main/java/io/visionhammer/gateway/security/SlackRequestVerifier.java"]},
10
+ {"id": "gw-7", "kind": "conceptual", "question": "Which component blocks outbound requests to internal or private addresses?", "keywords": ["egress", "private", "internal", "address"], "truth": ["src/main/java/io/visionhammer/gateway/security/EgressUrlGuard.java"]},
11
+ {"id": "gw-8", "kind": "conceptual", "question": "How are in-band error frames of the agent loop mapped to server-sent events?", "keywords": ["SSE", "frame", "ServerSentEvent", "error"], "truth": ["src/main/java/io/visionhammer/gateway/agenttask/api/SseFrameMapper.java"]}
12
+ ],
13
+ "visionhammer-agents": [
14
+ {"id": "ag-1", "kind": "symbolic", "question": "Which class assembles the RAG context (chunks and files) for an agent2 request (ContextAssembler)?", "keywords": ["ContextAssembler", "chunks"], "truth": ["agents/software-engineer/src/main/java/io/visionhammer/agents/agent2/service/ContextAssembler.java"]},
15
+ {"id": "ag-2", "kind": "conceptual", "question": "Where are text embeddings requested from the Voyage API?", "keywords": ["Voyage", "embedding", "embed"], "truth": ["agents/software-engineer/src/main/java/io/visionhammer/agents/agent2/client/VoyageEmbeddingClient.java"]},
16
+ {"id": "ag-3", "kind": "conceptual", "question": "Which code masks personal data before scan content leaves the service?", "keywords": ["PII", "mask", "egress"], "truth": ["agents/software-engineer/src/main/java/io/visionhammer/agents/agent2/scan/PiiEgressMasker.java"]},
17
+ {"id": "ag-4", "kind": "conceptual", "question": "Where are secrets redacted from the scan report text?", "keywords": ["secret", "mask", "report", "redact"], "truth": ["agents/software-engineer/src/main/java/io/visionhammer/agents/agent2/scan/report/SecretMasker.java"]},
18
+ {"id": "ag-5", "kind": "conceptual", "question": "Which detector checks whether application logs carry a correlation id?", "keywords": ["correlation", "detector"], "truth": ["shared/src/main/java/io/visionhammer/agents/shared/scan/CorrelationIdDetector.java"]},
19
+ {"id": "ag-6", "kind": "conceptual", "question": "Which adapter sends chat completions through the LiteLLM proxy?", "keywords": ["LiteLlm", "chat", "completion"], "truth": ["shared/src/main/java/io/visionhammer/agents/shared/llm/LiteLlmChatAdapter.java"]},
20
+ {"id": "ag-7", "kind": "conceptual", "question": "Which controller exposes the evaluation of retrieval quality for the RAG index?", "keywords": ["RagEval", "eval", "recall"], "truth": ["agents/software-engineer/src/main/java/io/visionhammer/agents/agent2/controller/RagEvalController.java"]},
21
+ {"id": "ag-8", "kind": "conceptual", "question": "Where are stored RAG chunks masked again after they were indexed?", "keywords": ["remask", "chunk", "mask"], "truth": ["agents/software-engineer/src/main/java/io/visionhammer/agents/agent2/rag/remask/application/ChunkRemaskService.java"]}
22
+ ],
23
+ "deepdebug-local-agent": [
24
+ {"id": "la-1", "kind": "conceptual", "question": "Where are commands sent by the platform verified with an Ed25519 signature?", "keywords": ["Ed25519", "signature", "verify"], "truth": ["src/utils/request-signing.js"]},
25
+ {"id": "la-2", "kind": "conceptual", "question": "Which code removes secret environment variables before running a command for the model?", "keywords": ["secret", "env", "child"], "truth": ["src/utils/child-env.js"]},
26
+ {"id": "la-3", "kind": "conceptual", "question": "Where does the agent decide whether a shell command is allowed to execute?", "keywords": ["allowlist", "exec", "policy", "command"], "truth": ["src/utils/exec-policy.js"]},
27
+ {"id": "la-4", "kind": "conceptual", "question": "Which modules confine file paths to the workspace root?", "keywords": ["workspace", "root", "confine", "sandbox"], "truth": ["src/utils/workspace-root.js", "src/utils/sandbox.js"]},
28
+ {"id": "la-5", "kind": "conceptual", "question": "Which module collapses a storm of identical errors into one forward before reporting to the gateway?", "keywords": ["burst", "suppress", "storm"], "truth": ["src/siblings/burst-suppressor.js"]},
29
+ {"id": "la-6", "kind": "conceptual", "question": "Where is the network interface the HTTP server listens on chosen?", "keywords": ["bind", "host", "loopback"], "truth": ["src/utils/bind-host.js"]}
30
+ ]
31
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "_comment": "Only VisionHammer-owned repos. The harness refuses any repo whose origin is not under the VISIONHAMMER GitHub org. Never point it at a customer repository.",
3
+ "allowed_origin_owners": ["VISIONHAMMER"],
4
+ "repos": [
5
+ {"name": "vh-api-gateway", "ref": "origin/release/qa", "lang": "java"},
6
+ {"name": "visionhammer-agents", "ref": "origin/release/qa", "lang": "java"},
7
+ {"name": "deepdebug-local-agent", "ref": "origin/main", "lang": "javascript"}
8
+ ]
9
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "_comment": "Symbols for the `affected` versus ripgrep comparison (classes/types for Java, exported functions or classes for JS).",
3
+ "vh-api-gateway": ["TenantAccessGuard", "PolicyGate", "EgressUrlGuard", "SlackRequestVerifier", "JwtTokenProvider", "AuthRateLimiter", "DataErasureService", "EmailService"],
4
+ "visionhammer-agents": ["ContextAssembler", "VoyageEmbeddingClient", "PiiEgressMasker", "SecretMasker", "CorrelationIdDetector", "LiteLlmChatAdapter", "PolicyGateGuard", "ChunkRemaskService"],
5
+ "deepdebug-local-agent": ["confineToRoot", "sanitizedChildEnv", "verifySignature", "evaluateCommand", "resolveBindHost", "validateHeaderRoot", "BurstSuppressor", "isSecretEnvName"]
6
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "description": "Expected Spring/DI relationships for the synthetic fixture. Selectors are 'File.java::label'. Kinds: edge (direct graph edge, any listed relation), reaches (reverse traversal from `to` reaches `from` within `depth` hops, any relation).",
3
+ "groups": {
4
+ "interface_impl": [
5
+ {"id": "stripe-implements", "kind": "edge", "from": "StripeGateway.java::StripeGateway", "to": "PaymentGateway.java::PaymentGateway", "relations": ["implements", "inherits", "extends"]},
6
+ {"id": "paypal-implements", "kind": "edge", "from": "PaypalGateway.java::PaypalGateway", "to": "PaymentGateway.java::PaymentGateway", "relations": ["implements", "inherits", "extends"]},
7
+ {"id": "inmemory-implements", "kind": "edge", "from": "InMemoryCache.java::InMemoryCache", "to": "Cache.java::Cache", "relations": ["implements", "inherits", "extends"]},
8
+ {"id": "checkout-calls-iface", "kind": "edge", "from": "CheckoutService.java::.checkout()", "to": "PaymentGateway.java::.charge()", "relations": ["calls"]},
9
+ {"id": "refund-calls-iface-lombok", "kind": "edge", "from": "RefundService.java::.refund()", "to": "PaymentGateway.java::.charge()", "relations": ["calls"]},
10
+ {"id": "checkout-reaches-stripe-impl", "kind": "reaches", "from": "CheckoutService.java::.checkout()", "to": "StripeGateway.java::.charge()", "depth": 2},
11
+ {"id": "refund-reaches-paypal-impl", "kind": "reaches", "from": "RefundService.java::.refund()", "to": "PaypalGateway.java::.charge()", "depth": 2},
12
+ {"id": "refund-reaches-stripe-impl", "kind": "reaches", "from": "RefundService.java::.refund()", "to": "StripeGateway.java::.charge()", "depth": 2}
13
+ ],
14
+ "lombok_injection": [
15
+ {"id": "refund-field-type", "kind": "edge", "from": "RefundService.java::RefundService", "to": "PaymentGateway.java::PaymentGateway", "relations": ["references"]},
16
+ {"id": "order-field-checkout", "kind": "edge", "from": "OrderController.java::OrderController", "to": "CheckoutService.java::CheckoutService", "relations": ["references"]},
17
+ {"id": "order-calls-checkout", "kind": "edge", "from": "OrderController.java::.order()", "to": "CheckoutService.java::.checkout()", "relations": ["calls"]},
18
+ {"id": "order-calls-refund", "kind": "edge", "from": "OrderController.java::.refund()", "to": "RefundService.java::.refund()", "relations": ["calls"]}
19
+ ],
20
+ "bean_methods": [
21
+ {"id": "bean-clock-consumed-by-audit-bean", "kind": "reaches", "from": "AppConfig.java::.auditLogger()", "to": "AppConfig.java::.clock()", "depth": 2},
22
+ {"id": "bean-audit-built-in-config", "kind": "edge", "from": "AppConfig.java::.auditLogger()", "to": "AuditLogger.java::AuditLogger", "relations": ["calls", "references"]},
23
+ {"id": "bean-fastcache-impl", "kind": "edge", "from": "AppConfig.java::.fastCache()", "to": "InMemoryCache.java::InMemoryCache", "relations": ["calls", "references"]},
24
+ {"id": "bean-consumer-checkout-audit", "kind": "reaches", "from": "CheckoutService.java::CheckoutService", "to": "AppConfig.java::.auditLogger()", "depth": 2}
25
+ ],
26
+ "qualifier": [
27
+ {"id": "qualifier-stripe-selected", "kind": "reaches", "from": "CheckoutService.java::CheckoutService", "to": "StripeGateway.java::StripeGateway", "depth": 2},
28
+ {"id": "qualifier-stripe-not-paypal", "kind": "not_reaches", "requires": "qualifier-stripe-selected", "from": "CheckoutService.java::CheckoutService", "to": "PaypalGateway.java::PaypalGateway", "depth": 2},
29
+ {"id": "qualifier-fastcache-bean", "kind": "reaches", "from": "CheckoutService.java::CheckoutService", "to": "AppConfig.java::.fastCache()", "depth": 2}
30
+ ]
31
+ },
32
+ "headline_group": "interface_impl",
33
+ "threshold_pct": 90
34
+ }
@@ -0,0 +1,29 @@
1
+ package com.example.fx;
2
+
3
+ import java.time.Clock;
4
+ import org.springframework.context.annotation.Bean;
5
+ import org.springframework.context.annotation.Configuration;
6
+
7
+ @Configuration
8
+ public class AppConfig {
9
+
10
+ @Bean
11
+ public Clock clock() {
12
+ return Clock.systemUTC();
13
+ }
14
+
15
+ @Bean
16
+ public AuditLogger auditLogger(Clock clock) {
17
+ return new AuditLogger(clock);
18
+ }
19
+
20
+ @Bean("fastCache")
21
+ public Cache fastCache() {
22
+ return new InMemoryCache();
23
+ }
24
+
25
+ @Bean("slowCache")
26
+ public Cache slowCache() {
27
+ return new InMemoryCache();
28
+ }
29
+ }
@@ -0,0 +1,15 @@
1
+ package com.example.fx;
2
+
3
+ import java.time.Clock;
4
+
5
+ public class AuditLogger {
6
+ private final Clock clock;
7
+
8
+ public AuditLogger(Clock clock) {
9
+ this.clock = clock;
10
+ }
11
+
12
+ public String stamp(String message) {
13
+ return clock.instant() + " " + message;
14
+ }
15
+ }
@@ -0,0 +1,7 @@
1
+ package com.example.fx;
2
+
3
+ public interface Cache {
4
+ String get(String key);
5
+
6
+ void put(String key, String value);
7
+ }
@@ -0,0 +1,25 @@
1
+ package com.example.fx;
2
+
3
+ import org.springframework.beans.factory.annotation.Qualifier;
4
+ import org.springframework.stereotype.Service;
5
+
6
+ @Service
7
+ public class CheckoutService {
8
+ private final PaymentGateway gateway;
9
+ private final Cache cache;
10
+ private final AuditLogger audit;
11
+
12
+ public CheckoutService(@Qualifier("stripe") PaymentGateway gateway,
13
+ @Qualifier("fastCache") Cache cache,
14
+ AuditLogger audit) {
15
+ this.gateway = gateway;
16
+ this.cache = cache;
17
+ this.audit = audit;
18
+ }
19
+
20
+ public String checkout(String account, long cents) {
21
+ String receipt = gateway.charge(account, cents);
22
+ cache.put(account, receipt);
23
+ return audit.stamp(receipt);
24
+ }
25
+ }
@@ -0,0 +1,18 @@
1
+ package com.example.fx;
2
+
3
+ import java.util.HashMap;
4
+ import java.util.Map;
5
+
6
+ public class InMemoryCache implements Cache {
7
+ private final Map<String, String> data = new HashMap<>();
8
+
9
+ @Override
10
+ public String get(String key) {
11
+ return data.get(key);
12
+ }
13
+
14
+ @Override
15
+ public void put(String key, String value) {
16
+ data.put(key, value);
17
+ }
18
+ }
@@ -0,0 +1,22 @@
1
+ package com.example.fx;
2
+
3
+ import lombok.RequiredArgsConstructor;
4
+ import org.springframework.web.bind.annotation.PostMapping;
5
+ import org.springframework.web.bind.annotation.RestController;
6
+
7
+ @RestController
8
+ @RequiredArgsConstructor
9
+ public class OrderController {
10
+ private final CheckoutService checkout;
11
+ private final RefundService refunds;
12
+
13
+ @PostMapping("/orders")
14
+ public String order(String account, long cents) {
15
+ return checkout.checkout(account, cents);
16
+ }
17
+
18
+ @PostMapping("/refunds")
19
+ public String refund(String account, long cents) {
20
+ return refunds.refund(account, cents);
21
+ }
22
+ }
@@ -0,0 +1,5 @@
1
+ package com.example.fx;
2
+
3
+ public interface PaymentGateway {
4
+ String charge(String account, long cents);
5
+ }
@@ -0,0 +1,11 @@
1
+ package com.example.fx;
2
+
3
+ import org.springframework.stereotype.Component;
4
+
5
+ @Component("paypal")
6
+ public class PaypalGateway implements PaymentGateway {
7
+ @Override
8
+ public String charge(String account, long cents) {
9
+ return "paypal:" + account + ":" + cents;
10
+ }
11
+ }
@@ -0,0 +1,14 @@
1
+ package com.example.fx;
2
+
3
+ import lombok.RequiredArgsConstructor;
4
+ import org.springframework.stereotype.Service;
5
+
6
+ @Service
7
+ @RequiredArgsConstructor
8
+ public class RefundService {
9
+ private final PaymentGateway gateway;
10
+
11
+ public String refund(String account, long cents) {
12
+ return gateway.charge(account, -cents);
13
+ }
14
+ }
@@ -0,0 +1,11 @@
1
+ package com.example.fx;
2
+
3
+ import org.springframework.stereotype.Component;
4
+
5
+ @Component("stripe")
6
+ public class StripeGateway implements PaymentGateway {
7
+ @Override
8
+ public String charge(String account, long cents) {
9
+ return "stripe:" + account + ":" + cents;
10
+ }
11
+ }
@@ -0,0 +1,81 @@
1
+ """`graphify affected` (depth 1) versus a ripgrep whole-word search (G5, "who calls / who is affected")."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ import subprocess
6
+
7
+ from gv_common import CODE_EXTS, files_with_word, is_test_path
8
+
9
+ LINE = re.compile(r"^- (?P<label>.+?) \[(?P<rel>[^\]]+)\] (?P<path>[^:\s]+):L\d+")
10
+
11
+
12
+ def parse_affected(text):
13
+ """Code files only: markdown/doc nodes that mention a symbol are not callers."""
14
+ files = set()
15
+ for ln in text.splitlines():
16
+ m = LINE.match(ln.strip())
17
+ if m and any(m.group("path").endswith(e) for e in CODE_EXTS):
18
+ files.add(m.group("path"))
19
+ return files
20
+
21
+
22
+ def run_affected(graphify, workdir, symbol, env, depth=1):
23
+ last = None
24
+ for label in (symbol, symbol + "()"):
25
+ res = subprocess.run([graphify, "affected", label, "--depth", str(depth)], cwd=str(workdir), env=env,
26
+ capture_output=True, text=True, timeout=300)
27
+ last = res
28
+ if res.returncode == 0 and "- " in res.stdout:
29
+ return "ok", label, parse_affected(res.stdout)
30
+ if res.returncode == 3:
31
+ return "ambiguous", label, set()
32
+ return "no_match(exit=%s)" % (last.returncode if last else "?"), symbol, set()
33
+
34
+
35
+ def definition_files(g, symbol):
36
+ cands = {symbol, symbol + "()", "." + symbol + "()"}
37
+ return {n["source_file"] for n in g["nodes"] if n.get("label") in cands and n.get("source_file")
38
+ and n["label"] != n["source_file"].rsplit("/", 1)[-1]}
39
+
40
+
41
+ def compare(graphify, workdir, g, symbols, env):
42
+ rows = []
43
+ for sym in symbols:
44
+ status, used_label, graph_files = run_affected(graphify, workdir, sym, env)
45
+ _, _, graph_files_d2 = run_affected(graphify, workdir, sym, env, depth=2)
46
+ defs = definition_files(g, sym)
47
+ rg_files = files_with_word(workdir, sym, include_tests=True) - defs
48
+ graph_files = graph_files - defs
49
+ graph_files_d2 = graph_files_d2 - defs
50
+ row = {"symbol": sym, "graph_status": status, "definition_files": sorted(defs)}
51
+ for scope, keep in (("all", lambda f: True), ("main", lambda f: not is_test_path(f))):
52
+ gf = {f for f in graph_files if keep(f)}
53
+ rf = {f for f in rg_files if keep(f)}
54
+ inter = gf & rf
55
+ g2f = {f for f in graph_files_d2 if keep(f)}
56
+ row[scope + "_depth2"] = {"graph": len(g2f), "both": len(g2f & rf),
57
+ "recall_vs_rg_pct": round(100.0 * len(g2f & rf) / len(rf), 1) if rf else None}
58
+ row[scope] = {
59
+ "graph": len(gf), "rg": len(rf), "both": len(inter),
60
+ "graph_only": len(gf - rf), "rg_only": len(rf - gf),
61
+ "recall_vs_rg_pct": round(100.0 * len(inter) / len(rf), 1) if rf else None,
62
+ "precision_vs_rg_pct": round(100.0 * len(inter) / len(gf), 1) if gf else None,
63
+ "rg_only_examples": sorted(rf - gf)[:3], "graph_only_examples": sorted(gf - rf)[:3],
64
+ }
65
+ rows.append(row)
66
+ ok = [r for r in rows if r["graph_status"] == "ok" and r["main"]["rg"]]
67
+ tot_both = sum(r["main"]["both"] for r in ok)
68
+ tot_rg = sum(r["main"]["rg"] for r in ok)
69
+ tot_graph = sum(r["main"]["graph"] for r in ok)
70
+ tot_both2 = sum(r["main_depth2"]["both"] for r in ok)
71
+ return {
72
+ "symbols": rows,
73
+ "aggregate_main": {
74
+ "symbols_compared": len(ok),
75
+ "recall_vs_rg_pct": round(100.0 * tot_both / tot_rg, 1) if tot_rg else None,
76
+ "precision_vs_rg_pct": round(100.0 * tot_both / tot_graph, 1) if tot_graph else None,
77
+ "depth2_recall_vs_rg_pct": round(100.0 * tot_both2 / tot_rg, 1) if tot_rg else None,
78
+ },
79
+ "note": "rg whole-word match is the reference set (includes comments/strings/imports of same-named types); "
80
+ "graph_only files are references the textual search missed or aliases.",
81
+ }
@@ -0,0 +1,22 @@
1
+ """Audit hook loaded via PYTHONPATH (G7). Appends one line per socket/exec event.
2
+
3
+ Enabled only when GV_AUDIT_FILE is set. The hook is inherited by worker
4
+ processes because PYTHONPATH travels in the environment. It never sends data
5
+ anywhere: it only appends to the local file named by GV_AUDIT_FILE.
6
+ """
7
+ import os
8
+ import sys
9
+
10
+ _path = os.environ.get("GV_AUDIT_FILE")
11
+ if _path:
12
+ _prefixes = ("socket.", "subprocess.Popen", "os.system", "os.exec", "os.posix_spawn", "os.spawn", "ssl.")
13
+
14
+ def _hook(event, args):
15
+ if event.startswith(_prefixes):
16
+ try:
17
+ with open(_path, "a", encoding="utf-8") as fh:
18
+ fh.write("%d\t%s\t%s\n" % (os.getpid(), event, repr(args)[:300].replace("\n", " ")))
19
+ except Exception:
20
+ pass
21
+
22
+ sys.addaudithook(_hook)
@@ -0,0 +1,96 @@
1
+ """Graph-level metrics from graphify-out/graph.json (G1, G2, G8)."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ import os
6
+ from collections import Counter
7
+ from pathlib import Path
8
+
9
+ from gv_common import iter_code_files, is_test_path, load_json
10
+
11
+
12
+ def load_graph(workdir):
13
+ return load_json(Path(workdir) / "graphify-out" / "graph.json")
14
+
15
+
16
+ def edges_of(g):
17
+ return g.get("links") or g.get("edges") or []
18
+
19
+
20
+ def file_node_labels(g):
21
+ """Set of (source_file) that have at least one real symbol (non file-level node)."""
22
+ with_symbols = set()
23
+ all_files = set()
24
+ for n in g["nodes"]:
25
+ sf = n.get("source_file")
26
+ if not sf:
27
+ continue
28
+ all_files.add(sf)
29
+ if n.get("label") != os.path.basename(sf):
30
+ with_symbols.add(sf)
31
+ return with_symbols, all_files
32
+
33
+
34
+ def g1_extraction(g, workdir):
35
+ code_files = iter_code_files(workdir)
36
+ with_symbols, _ = file_node_labels(g)
37
+ missing = [f for f in code_files if f not in with_symbols]
38
+ n = len(code_files)
39
+ return {
40
+ "code_files": n,
41
+ "files_with_symbols": n - len(missing),
42
+ "coverage_pct": round(100.0 * (n - len(missing)) / n, 2) if n else None,
43
+ "files_without_symbols": missing[:200],
44
+ "files_without_symbols_total": len(missing),
45
+ "of_which_in_tests": sum(1 for f in missing if is_test_path(f)),
46
+ }
47
+
48
+
49
+ def g2_edges(g):
50
+ es = edges_of(g)
51
+ conf = Counter(e.get("confidence") for e in es)
52
+ total = len(es)
53
+ main = [e for e in es if not is_test_path(e.get("source_file", ""))]
54
+ main_inf = sum(1 for e in main if e.get("confidence") == "INFERRED")
55
+ inferred_calls = Counter()
56
+ for e in es:
57
+ if e.get("confidence") == "INFERRED":
58
+ inferred_calls[e.get("relation")] += 1
59
+ return {
60
+ "nodes": len(g["nodes"]),
61
+ "edges": total,
62
+ "extracted": conf.get("EXTRACTED", 0),
63
+ "inferred": conf.get("INFERRED", 0),
64
+ "inferred_pct": round(100.0 * conf.get("INFERRED", 0) / total, 2) if total else None,
65
+ "edges_from_main_sources": len(main),
66
+ "inferred_pct_main_sources": round(100.0 * main_inf / len(main), 2) if main else None,
67
+ "inferred_by_relation": dict(inferred_calls.most_common(6)),
68
+ "communities": len({n.get("community") for n in g["nodes"] if n.get("community") is not None}),
69
+ "cross_repo_edges": 0,
70
+ "cross_repo_note": "one graph per repo by construction; a mixed corpus is not built by this harness",
71
+ }
72
+
73
+
74
+ def signature(g):
75
+ """Order-independent fingerprint of nodes and edges (communities excluded)."""
76
+ node_ids = sorted(n["id"] for n in g["nodes"])
77
+ edge_keys = sorted("%s|%s|%s|%s" % (e["source"], e["target"], e.get("relation"), e.get("confidence")) for e in edges_of(g))
78
+ h = hashlib.sha256()
79
+ h.update("\n".join(node_ids).encode())
80
+ h.update(b"\x00")
81
+ h.update("\n".join(edge_keys).encode())
82
+ return {"nodes": len(node_ids), "edges": len(edge_keys), "sha256": h.hexdigest()[:16]}
83
+
84
+
85
+ def class_index(g):
86
+ """Helpers shared by the edge oracle and the Spring fixture check."""
87
+ ns = {n["id"]: n for n in g["nodes"]}
88
+ owner = {}
89
+ for e in edges_of(g):
90
+ if e.get("relation") == "method":
91
+ owner[e["target"]] = e["source"]
92
+ supers = {}
93
+ for e in edges_of(g):
94
+ if e.get("relation") in ("inherits", "implements", "extends"):
95
+ supers.setdefault(ns[e["source"]]["label"], set()).add(ns[e["target"]]["label"])
96
+ return ns, owner, supers
@@ -0,0 +1,130 @@
1
+ """Shared helpers for the Graphify validation harness (stdlib only, Python >= 3.9)."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ HARNESS_DIR = Path(__file__).resolve().parent.parent
13
+
14
+ # Extensions that count as "code" for G1 (pom/SQL/markdown are excluded on purpose).
15
+ CODE_EXTS = {
16
+ ".java", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".tf",
17
+ }
18
+ SKIP_DIRS = {
19
+ ".git", "node_modules", "target", "graphify-out", "dist", "build", ".idea",
20
+ ".gradle", "__pycache__", ".venv", "venv",
21
+ }
22
+ TEST_MARKERS = ("/src/test/", "/test/", "/tests/", "/__tests__/", "/fixtures/")
23
+ TEST_SUFFIXES = (".test.js", ".test.ts", ".test.tsx", ".spec.ts", ".spec.js", "Test.java", "Tests.java", "IT.java")
24
+
25
+
26
+ def load_json(path):
27
+ with open(path, encoding="utf-8") as fh:
28
+ return json.load(fh)
29
+
30
+
31
+ def dump_json(path, obj):
32
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
33
+ with open(path, "w", encoding="utf-8") as fh:
34
+ json.dump(obj, fh, indent=2, sort_keys=True, ensure_ascii=False)
35
+ fh.write("\n")
36
+
37
+
38
+ def is_test_path(rel):
39
+ p = "/" + rel.replace("\\", "/")
40
+ return any(m in p for m in TEST_MARKERS) or p.endswith(TEST_SUFFIXES)
41
+
42
+
43
+ def iter_code_files(root):
44
+ """Yield repo-relative paths of code files under root (sorted, deterministic)."""
45
+ root = Path(root)
46
+ out = []
47
+ for dirpath, dirnames, filenames in os.walk(root):
48
+ dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS)
49
+ for fn in sorted(filenames):
50
+ if Path(fn).suffix in CODE_EXTS:
51
+ out.append(str((Path(dirpath) / fn).relative_to(root)).replace(os.sep, "/"))
52
+ return out
53
+
54
+
55
+ def rg_binary():
56
+ """Return the ripgrep command (list) or None. $RG overrides PATH lookup."""
57
+ env_rg = os.environ.get("RG")
58
+ if env_rg:
59
+ return [env_rg]
60
+ found = shutil.which("rg")
61
+ return [found] if found else None
62
+
63
+
64
+ def rg_version():
65
+ cmd = rg_binary()
66
+ if not cmd:
67
+ return "python-fallback (ripgrep not found; regex word search over the same files)"
68
+ try:
69
+ return subprocess.run(cmd + ["--version"], capture_output=True, text=True, timeout=20).stdout.splitlines()[0]
70
+ except Exception as exc: # pragma: no cover - environment dependent
71
+ return "rg unavailable: %s" % exc
72
+
73
+
74
+ def files_with_word(root, word, include_tests=True, whole_word=True, ignore_case=False):
75
+ """Code files (repo-relative) that contain `word` as a whole word.
76
+
77
+ Uses ripgrep when available, otherwise a regex scan with the same semantics
78
+ (fixed string, whole word by default, code extensions only, skip dirs ignored).
79
+ """
80
+ root = Path(root)
81
+ cmd = rg_binary()
82
+ hits = set()
83
+ if cmd:
84
+ args = cmd + ["-l", "-F", "--no-messages", "--no-ignore", "--hidden"]
85
+ if whole_word:
86
+ args.append("-w")
87
+ if ignore_case:
88
+ args.append("-i")
89
+ for ext in sorted(CODE_EXTS):
90
+ args += ["-g", "*" + ext]
91
+ for d in sorted(SKIP_DIRS):
92
+ args += ["-g", "!%s/" % d]
93
+ args += ["--", word, "."]
94
+ res = subprocess.run(args, cwd=str(root), capture_output=True, text=True, timeout=300)
95
+ for line in res.stdout.splitlines():
96
+ hits.add(line[2:] if line.startswith("./") else line)
97
+ else:
98
+ body = re.escape(word)
99
+ if whole_word:
100
+ body = r"(?<![A-Za-z0-9_])" + body + r"(?![A-Za-z0-9_])"
101
+ pat = re.compile(body, re.I if ignore_case else 0)
102
+ for rel in iter_code_files(root):
103
+ try:
104
+ text = (root / rel).read_text(encoding="utf-8", errors="replace")
105
+ except OSError:
106
+ continue
107
+ if pat.search(text):
108
+ hits.add(rel)
109
+ if not include_tests:
110
+ hits = {h for h in hits if not is_test_path(h)}
111
+ return hits
112
+
113
+
114
+ def clean_env(home, venv_bin, extra=None):
115
+ """Minimal environment: no LLM keys, no cloud credentials, no proxies."""
116
+ env = {
117
+ "HOME": str(home),
118
+ "PATH": "%s:/usr/bin:/bin:/usr/sbin:/sbin" % venv_bin,
119
+ "LANG": "C.UTF-8",
120
+ "PYTHONHASHSEED": "0",
121
+ "NO_COLOR": "1",
122
+ }
123
+ if extra:
124
+ env.update(extra)
125
+ return env
126
+
127
+
128
+ def log(msg):
129
+ sys.stderr.write("[gv] %s\n" % msg)
130
+ sys.stderr.flush()
@@ -0,0 +1,47 @@
1
+ """License inventory of the installed distributions (run with the venv python). Stdlib only."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ import sys
7
+ from importlib import metadata
8
+
9
+ COPYLEFT = re.compile(r"\b(AGPL|GPL|LGPL|SSPL|EUPL|MPL|CC-BY-SA|OSL)\b", re.I)
10
+ PERMISSIVE = re.compile(r"\b(MIT|BSD|Apache|ISC|PSF|Python Software Foundation|Unlicense|CC0|HPND|Zlib)\b", re.I)
11
+
12
+
13
+ def classify(text):
14
+ if not text:
15
+ return "UNKNOWN"
16
+ if COPYLEFT.search(text):
17
+ return "REVIEW (copyleft-family)"
18
+ if PERMISSIVE.search(text):
19
+ return "permissive"
20
+ return "UNKNOWN"
21
+
22
+
23
+ def main():
24
+ rows = []
25
+ for dist in sorted(metadata.distributions(), key=lambda d: (d.metadata["Name"] or "").lower()):
26
+ name = dist.metadata["Name"]
27
+ if name.lower() in ("pip", "setuptools", "wheel"):
28
+ continue
29
+ meta = dist.metadata
30
+ expr = meta.get("License-Expression") or ""
31
+ lic = (meta.get("License") or "").strip().splitlines()[0] if meta.get("License") else ""
32
+ classifiers = [c.split("::")[-1].strip() for c in meta.get_all("Classifier") or [] if c.startswith("License ::")]
33
+ text = " ".join([expr, lic] + classifiers)
34
+ files = [str(f) for f in (dist.files or []) if re.search(r"(LICEN[CS]E|NOTICE|COPYING)", str(f), re.I)]
35
+ rows.append({"name": name, "version": dist.version, "license_expression": expr, "license_field": lic[:80],
36
+ "classifiers": classifiers, "license_files": files[:4], "class": classify(text),
37
+ "has_license_file": bool(files)})
38
+ summary = {"total": len(rows),
39
+ "permissive": sum(1 for r in rows if r["class"] == "permissive"),
40
+ "review": [r["name"] for r in rows if r["class"].startswith("REVIEW")],
41
+ "unknown": [r["name"] for r in rows if r["class"] == "UNKNOWN"],
42
+ "without_license_file": [r["name"] for r in rows if not r["has_license_file"]]}
43
+ json.dump({"distributions": rows, "summary": summary}, sys.stdout, indent=2)
44
+
45
+
46
+ if __name__ == "__main__":
47
+ main()