graphcheck 0.2.0__tar.gz → 0.3.0__tar.gz

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 (270) hide show
  1. graphcheck-0.3.0/.gitattributes +2 -0
  2. graphcheck-0.3.0/.github/CODEOWNERS +2 -0
  3. {graphcheck-0.2.0 → graphcheck-0.3.0}/.github/pull_request_template.md +1 -1
  4. graphcheck-0.3.0/.github/scripts/first-run.sh +107 -0
  5. graphcheck-0.3.0/.github/scripts/first_run_evidence.py +403 -0
  6. graphcheck-0.3.0/.github/workflows/ci.yml +144 -0
  7. graphcheck-0.3.0/.github/workflows/extended-ci.yml +232 -0
  8. {graphcheck-0.2.0 → graphcheck-0.3.0}/.github/workflows/graphcheck.yml +4 -2
  9. graphcheck-0.3.0/.github/workflows/hostile-scale.yml +26 -0
  10. {graphcheck-0.2.0 → graphcheck-0.3.0}/.github/workflows/release.yml +1 -1
  11. {graphcheck-0.2.0 → graphcheck-0.3.0}/.gitignore +1 -1
  12. graphcheck-0.3.0/.gitmodules +3 -0
  13. {graphcheck-0.2.0 → graphcheck-0.3.0}/CHANGELOG.md +65 -4
  14. {graphcheck-0.2.0 → graphcheck-0.3.0}/CONTRIBUTING.md +25 -5
  15. {graphcheck-0.2.0 → graphcheck-0.3.0}/PKG-INFO +27 -11
  16. {graphcheck-0.2.0 → graphcheck-0.3.0}/README.md +25 -10
  17. graphcheck-0.3.0/docs/README.md +9 -0
  18. {graphcheck-0.2.0/docs → graphcheck-0.3.0/docs/getting-started}/quickstart.md +7 -4
  19. {graphcheck-0.2.0/docs → graphcheck-0.3.0/docs/guides}/agents.md +14 -8
  20. graphcheck-0.3.0/docs/guides/ci-cd.md +400 -0
  21. graphcheck-0.2.0/docs/ci-setup.md → graphcheck-0.3.0/docs/guides/github-actions.md +26 -28
  22. {graphcheck-0.2.0/docs → graphcheck-0.3.0/docs/guides}/troubleshooting.md +7 -5
  23. {graphcheck-0.2.0/docs → graphcheck-0.3.0/docs/guides}/user-guide.md +19 -16
  24. {graphcheck-0.2.0 → graphcheck-0.3.0}/docs/llms.txt +2 -1
  25. graphcheck-0.3.0/docs/maintainers/ci.md +97 -0
  26. graphcheck-0.2.0/docs/clean-machine-test-protocol.md → graphcheck-0.3.0/docs/maintainers/clean-machine-test.md +1 -1
  27. {graphcheck-0.2.0/docs → graphcheck-0.3.0/docs/maintainers}/releasing.md +5 -2
  28. graphcheck-0.2.0/docs/check-reference.md → graphcheck-0.3.0/docs/reference/checks.md +2 -2
  29. {graphcheck-0.2.0/docs → graphcheck-0.3.0/docs/reference}/compatibility.md +15 -10
  30. {graphcheck-0.2.0/docs → graphcheck-0.3.0/docs/reference}/telemetry.md +1 -1
  31. graphcheck-0.3.0/docs/samples/report-clean.html +12 -0
  32. graphcheck-0.3.0/docs/samples/report-findings.html +12 -0
  33. {graphcheck-0.2.0/docs/samples → graphcheck-0.3.0/docs/samples/reports}/report-clean.html +6 -4
  34. {graphcheck-0.2.0/docs/samples → graphcheck-0.3.0/docs/samples/reports}/report-findings.html +6 -4
  35. {graphcheck-0.2.0/docs/specs → graphcheck-0.3.0/docs/schemas}/results.schema.json +5 -5
  36. graphcheck-0.2.0/docs/specs/SPEC-01-results-json.md → graphcheck-0.3.0/docs/specifications/spec-01-results-json.md +23 -16
  37. graphcheck-0.2.0/docs/specs/SPEC-02-check-yaml.md → graphcheck-0.3.0/docs/specifications/spec-02-check-yaml.md +4 -4
  38. graphcheck-0.2.0/docs/specs/SPEC-03-connector.md → graphcheck-0.3.0/docs/specifications/spec-03-connector.md +3 -2
  39. graphcheck-0.2.0/docs/specs/SPEC-04 Engine.md → graphcheck-0.3.0/docs/specifications/spec-04-engine.md +5 -4
  40. graphcheck-0.2.0/docs/specs/SPEC-05-profiler-baseline.md → graphcheck-0.3.0/docs/specifications/spec-05-profiler-baseline.md +5 -5
  41. graphcheck-0.2.0/docs/specs/SPEC-06-scorer-html-report.md → graphcheck-0.3.0/docs/specifications/spec-06-scorer-html-report.md +24 -16
  42. graphcheck-0.2.0/docs/specs/SPEC-09-packs.md → graphcheck-0.3.0/docs/specifications/spec-09-packs.md +8 -8
  43. graphcheck-0.2.0/docs/specs/SPEC-11-generate.md → graphcheck-0.3.0/docs/specifications/spec-11-generate.md +11 -11
  44. graphcheck-0.2.0/docs/specs/SPEC-12-observability-support → graphcheck-0.3.0/docs/specifications/spec-12-observability-support.md +3 -3
  45. graphcheck-0.3.0/examples/README.md +8 -0
  46. graphcheck-0.2.0/docs/docker-quickstart.md → graphcheck-0.3.0/examples/fraud-ring/README.md +7 -2
  47. graphcheck-0.3.0/examples/fraud-ring/checks/fraud-ring-conformance.yml +19 -0
  48. graphcheck-0.3.0/examples/fraud-ring/checks/smoke.yml +7 -0
  49. graphcheck-0.3.0/examples/fraud-ring/graphcheck.yml +4 -0
  50. graphcheck-0.2.0/profiles.yml → graphcheck-0.3.0/examples/fraud-ring/profiles.example.yml +2 -1
  51. graphcheck-0.3.0/examples/minimal/README.md +17 -0
  52. {graphcheck-0.2.0/examples → graphcheck-0.3.0/examples/minimal}/checks/example.yml +0 -7
  53. graphcheck-0.3.0/examples/minimal/graphcheck.yml +4 -0
  54. graphcheck-0.3.0/examples/minimal/optional-checks/customer-count-drift.yml +8 -0
  55. graphcheck-0.3.0/examples/minimal/profiles.example.yml +8 -0
  56. {graphcheck-0.2.0 → graphcheck-0.3.0/examples}/monitoring/README.md +1 -1
  57. {graphcheck-0.2.0 → graphcheck-0.3.0/examples}/monitoring/prometheus/README.md +3 -3
  58. {graphcheck-0.2.0 → graphcheck-0.3.0}/pyproject.toml +4 -1
  59. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/__init__.py +1 -1
  60. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/cli.py +28 -9
  61. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/contracts/results.py +14 -8
  62. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/contracts/schemas.py +10 -10
  63. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/engine/runner.py +5 -5
  64. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/neo4j_adapter.py +1 -1
  65. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/project.py +2 -2
  66. graphcheck-0.3.0/src/graphcheck/reporting/coverage.py +18 -0
  67. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/reporting/explorer.py +1 -1
  68. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/reporting/history.py +15 -22
  69. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/reporting/html.py +24 -15
  70. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/reporting/presentation.py +7 -7
  71. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/reporting/redaction.py +1 -1
  72. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/reporting/writer.py +17 -11
  73. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/conftest.py +2 -0
  74. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/integration/conftest.py +1 -1
  75. graphcheck-0.3.0/tests/integration/hostile/README.md +41 -0
  76. graphcheck-0.3.0/tests/integration/hostile/apoc-less.yml +10 -0
  77. graphcheck-0.3.0/tests/integration/hostile/cases.yml +42 -0
  78. graphcheck-0.3.0/tests/integration/hostile/empty.yml +6 -0
  79. graphcheck-0.3.0/tests/integration/hostile/llm-kg-builder.cypher +71 -0
  80. graphcheck-0.3.0/tests/integration/hostile/llm-kg-builder.yml +14 -0
  81. graphcheck-0.3.0/tests/integration/hostile/neo4j-44-cluster.yml +62 -0
  82. graphcheck-0.3.0/tests/integration/hostile/public-scale.yml +6 -0
  83. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/integration/test_generate_integration.py +1 -1
  84. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/integration/test_generate_live.py +1 -1
  85. graphcheck-0.3.0/tests/integration/test_hostile_graphs.py +376 -0
  86. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/integration/test_integration_neo4j_adapter.py +1 -1
  87. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/integration/test_mcp_agent_surface.py +3 -1
  88. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/integration/test_mcp_failure_isolation.py +1 -1
  89. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/integration/test_mcp_neo4j_workflow.py +1 -1
  90. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/performance/test_engine_budget.py +1 -1
  91. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/property/test_evaluator_properties.py +2 -1
  92. graphcheck-0.3.0/tests/repository/test_ci_action_boundary.py +19 -0
  93. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/repository}/test_docker_quickstart.py +11 -8
  94. graphcheck-0.3.0/tests/repository/test_first_run_evidence.py +174 -0
  95. graphcheck-0.3.0/tests/repository/test_hostile_manifest.py +66 -0
  96. graphcheck-0.3.0/tests/repository/test_minimal_example.py +31 -0
  97. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/repository}/test_packaging.py +1 -1
  98. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/repository}/test_sample_reports.py +15 -3
  99. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/application}/test_baselines.py +1 -1
  100. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/application}/test_diff.py +1 -1
  101. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/application}/test_project_profiles.py +7 -1
  102. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/cli}/test_cli.py +75 -13
  103. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/cli}/test_cli_lazy_imports.py +2 -2
  104. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/cli}/test_generate_cli.py +1 -1
  105. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/cli}/test_run_cli.py +27 -7
  106. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/connectors}/test_neo4j_adapter.py +6 -3
  107. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/results.clean.json +2 -2
  108. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/results.complete.json +2 -2
  109. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/results.complete.rendered.json +2 -2
  110. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/results.failed.json +2 -2
  111. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/results.generated-only.json +2 -2
  112. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/results.partial.json +2 -2
  113. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/test_agents_example.py +2 -2
  114. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/test_check_validation.py +3 -7
  115. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/test_profile.py +2 -2
  116. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/test_results.py +8 -20
  117. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/engine}/test_packs.py +3 -3
  118. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/engine/test_pii_pack_runtime.py +1 -1
  119. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/engine/test_runner.py +20 -16
  120. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/mcp}/test_mcp_adapter.py +1 -1
  121. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/observability/test_monitoring_config.py +4 -4
  122. graphcheck-0.3.0/tests/unit/observability/test_runner.py +30 -0
  123. graphcheck-0.3.0/tests/unit/observability/test_server.py +23 -0
  124. graphcheck-0.3.0/tests/unit/reporting/test_coverage.py +25 -0
  125. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/reporting}/test_redaction.py +1 -1
  126. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/reporting}/test_report_explorer.py +32 -1
  127. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/reporting}/test_report_history.py +100 -8
  128. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/reporting}/test_reporting.py +31 -15
  129. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/reporting}/test_result_presentation.py +63 -4
  130. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/telemetry/test_cli_boundary_integration.py +8 -4
  131. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/telemetry/test_transparency_doc.py +4 -4
  132. {graphcheck-0.2.0/scripts → graphcheck-0.3.0/tools}/generate_sample_reports.py +6 -6
  133. graphcheck-0.3.0/tools/run_hostile_graphs.py +54 -0
  134. graphcheck-0.2.0/.gitattributes +0 -2
  135. graphcheck-0.2.0/.github/CODEOWNERS +0 -5
  136. graphcheck-0.2.0/.github/actions/graphcheck-action/README.md +0 -94
  137. graphcheck-0.2.0/.github/actions/graphcheck-action/action.yml +0 -127
  138. graphcheck-0.2.0/.github/actions/graphcheck-action/generate_profile.py +0 -53
  139. graphcheck-0.2.0/.github/actions/graphcheck-action/write_summary.py +0 -102
  140. graphcheck-0.2.0/.github/scripts/first-run.sh +0 -43
  141. graphcheck-0.2.0/.github/workflows/ci.yml +0 -269
  142. graphcheck-0.2.0/.gitmodules +0 -3
  143. graphcheck-0.2.0/graphcheck.yml +0 -4
  144. graphcheck-0.2.0/tests/observability/test_runner.py +0 -109
  145. graphcheck-0.2.0/tests/observability/test_server.py +0 -63
  146. {graphcheck-0.2.0 → graphcheck-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  147. {graphcheck-0.2.0 → graphcheck-0.3.0}/.github/ISSUE_TEMPLATE/deliverable.md +0 -0
  148. {graphcheck-0.2.0 → graphcheck-0.3.0}/.github/ISSUE_TEMPLATE/docs.md +0 -0
  149. {graphcheck-0.2.0 → graphcheck-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  150. {graphcheck-0.2.0 → graphcheck-0.3.0}/.pre-commit-config.yaml +0 -0
  151. {graphcheck-0.2.0 → graphcheck-0.3.0}/.python-version +0 -0
  152. {graphcheck-0.2.0 → graphcheck-0.3.0}/LICENSE +0 -0
  153. {graphcheck-0.2.0/docs → graphcheck-0.3.0/docs/assets}/graphcheck-demo.gif +0 -0
  154. {graphcheck-0.2.0/docs/specs → graphcheck-0.3.0/docs/schemas}/check.envelope.schema.json +0 -0
  155. {graphcheck-0.2.0/docs/specs → graphcheck-0.3.0/docs/schemas}/check.schema.json +0 -0
  156. {graphcheck-0.2.0/docs/specs → graphcheck-0.3.0/docs/schemas}/pack.schema.json +0 -0
  157. {graphcheck-0.2.0/docs/specs → graphcheck-0.3.0/docs/schemas}/profile.schema.json +0 -0
  158. /graphcheck-0.2.0/docs/specs/SPEC-10-telemetry-events.md → /graphcheck-0.3.0/docs/specifications/spec-10-telemetry-events.md +0 -0
  159. /graphcheck-0.2.0/docker-compose.yml → /graphcheck-0.3.0/examples/fraud-ring/compose.yml +0 -0
  160. /graphcheck-0.2.0/monitoring/docker-compose.yml → /graphcheck-0.3.0/examples/monitoring/compose.yml +0 -0
  161. {graphcheck-0.2.0 → graphcheck-0.3.0/examples}/monitoring/grafana/dashboards/graphcheck-database-health.json +0 -0
  162. {graphcheck-0.2.0 → graphcheck-0.3.0/examples}/monitoring/grafana/provisioning/dashboards/dashboards.yml +0 -0
  163. {graphcheck-0.2.0 → graphcheck-0.3.0/examples}/monitoring/grafana/provisioning/datasources/prometheus-datasource.yml +0 -0
  164. {graphcheck-0.2.0 → graphcheck-0.3.0/examples}/monitoring/prometheus/prometheus.yml +0 -0
  165. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/application/__init__.py +0 -0
  166. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/application/artifacts.py +0 -0
  167. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/application/paths.py +0 -0
  168. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/application/run.py +0 -0
  169. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/application/suites.py +0 -0
  170. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/baselines.py +0 -0
  171. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/bootstrap.py +0 -0
  172. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/connection_profiles.py +0 -0
  173. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/contracts/__init__.py +0 -0
  174. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/contracts/check.py +0 -0
  175. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/contracts/profile.py +0 -0
  176. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/contracts/scalars.py +0 -0
  177. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/debug_diagnostics.py +0 -0
  178. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/diff.py +0 -0
  179. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/engine/__init__.py +0 -0
  180. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/engine/baseline.py +0 -0
  181. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/engine/compiler.py +0 -0
  182. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/engine/core_pack.py +0 -0
  183. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/engine/evaluator.py +0 -0
  184. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/engine/executor.py +0 -0
  185. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/engine/identifiers.py +0 -0
  186. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/engine/parameters.py +0 -0
  187. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/engine/pii_pack.py +0 -0
  188. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/engine/sampling.py +0 -0
  189. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/errors.py +0 -0
  190. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/generation/__init__.py +0 -0
  191. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/generation/client.py +0 -0
  192. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/generation/config.py +0 -0
  193. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/generation/disclosure.py +0 -0
  194. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/generation/prompts.py +0 -0
  195. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/generation/proposals.py +0 -0
  196. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/generation/service.py +0 -0
  197. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/generation/transmission.py +0 -0
  198. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/generation/writer.py +0 -0
  199. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/mcp/adapter.py +0 -0
  200. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/mcp/server.py +0 -0
  201. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/observability/__init__.py +0 -0
  202. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/observability/collector.py +0 -0
  203. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/observability/health.py +0 -0
  204. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/observability/metrics.py +0 -0
  205. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/observability/runner.py +0 -0
  206. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/observability/server.py +0 -0
  207. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/packs/__init__.py +0 -0
  208. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/packs/catalog.py +0 -0
  209. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/packs/core.yml +0 -0
  210. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/packs/metadata.py +0 -0
  211. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/packs/pii.yml +0 -0
  212. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/profiler.py +0 -0
  213. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/reporting/__init__.py +0 -0
  214. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/scoring.py +0 -0
  215. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/telemetry/__init__.py +0 -0
  216. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/telemetry/collector.py +0 -0
  217. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/telemetry/consent.py +0 -0
  218. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/telemetry/events.py +0 -0
  219. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/telemetry/inactive.py +0 -0
  220. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/telemetry/policy.py +0 -0
  221. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/telemetry/posthog.py +0 -0
  222. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/telemetry/release.py +0 -0
  223. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/telemetry/runtime.py +0 -0
  224. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/telemetry/types.py +0 -0
  225. {graphcheck-0.2.0 → graphcheck-0.3.0}/src/graphcheck/yaml_loader.py +0 -0
  226. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/__init__.py +0 -0
  227. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/integration/test_integration_engine.py +0 -0
  228. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/integration/test_performance_plans.py +0 -0
  229. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/performance/__init__.py +0 -0
  230. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/performance/budgets.json +0 -0
  231. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/performance/gates.py +0 -0
  232. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/performance/helpers.py +0 -0
  233. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/performance/test_cli_startup.py +0 -0
  234. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/performance/test_measurement_helpers.py +0 -0
  235. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/performance/test_memory_gates.py +0 -0
  236. {graphcheck-0.2.0 → graphcheck-0.3.0}/tests/performance/test_regression_gates.py +0 -0
  237. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/application}/test_debug_diagnostics.py +0 -0
  238. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/application}/test_profiler.py +0 -0
  239. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/cli}/test_monitor_cli.py +0 -0
  240. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/agent-suite.yml +0 -0
  241. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/baseline.json +0 -0
  242. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/suite.invalid-bad-expect.yml +0 -0
  243. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/suite.invalid-duplicate-key.yml +0 -0
  244. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/suite.invalid-unknown-check.yml +0 -0
  245. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/suite.invalid-unknown-key.yml +0 -0
  246. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/contracts/fixtures/suite.valid.yml +0 -0
  247. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/engine/test_baseline.py +0 -0
  248. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/engine/test_compiler.py +0 -0
  249. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/engine/test_core_pack_compiler.py +0 -0
  250. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/engine/test_evaluator.py +0 -0
  251. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/engine/test_executor.py +0 -0
  252. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/engine/test_identifiers.py +0 -0
  253. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/engine/test_parameters.py +0 -0
  254. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/engine/test_sampling.py +0 -0
  255. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/engine/test_sampling_runner.py +0 -0
  256. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit/engine}/test_scoring.py +0 -0
  257. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/generation/test_client.py +0 -0
  258. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/generation/test_generation.py +0 -0
  259. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/generation/test_service.py +0 -0
  260. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/observability/__init__.py +0 -0
  261. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/observability/test_collector.py +0 -0
  262. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/observability/test_health.py +0 -0
  263. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/telemetry/test_boundary_payloads.py +0 -0
  264. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/telemetry/test_cli_controls.py +0 -0
  265. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/telemetry/test_engine_events.py +0 -0
  266. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/telemetry/test_events.py +0 -0
  267. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/telemetry/test_policy.py +0 -0
  268. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/telemetry/test_posthog.py +0 -0
  269. {graphcheck-0.2.0/tests → graphcheck-0.3.0/tests/unit}/telemetry/test_privacy_properties.py +0 -0
  270. {graphcheck-0.2.0 → graphcheck-0.3.0}/uv.lock +0 -0
@@ -0,0 +1,2 @@
1
+ *.sh text eol=lf
2
+ docs/samples/reports/*.html text eol=lf
@@ -0,0 +1,2 @@
1
+ * @ezhilvendhan @kev-graphora
2
+ /tests/fixtures/ @jayachandra-bit
@@ -10,7 +10,7 @@
10
10
  - [ ] No `print` / `console.log`; structured logger only
11
11
  - [ ] No swallowed exceptions (`except: pass`)
12
12
  - [ ] No un-issued TODOs
13
- - [ ] Design detailed in `docs/specs/<name>.md` (where applicable)
13
+ - [ ] Design detailed in `docs/specifications/<name>.md` (where applicable)
14
14
  - [ ] CHANGELOG.md entry
15
15
  - [ ] No new third-party dependency without team review
16
16
  - [ ] No agent meta-commentary or AI attribution anywhere
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ : "${GRAPHCHECK_BOOTSTRAP_PYTHON:?Set GRAPHCHECK_BOOTSTRAP_PYTHON to the clean runner Python}"
5
+ : "${GRAPHCHECK_WHEEL_DIR:?Set GRAPHCHECK_WHEEL_DIR to the directory containing one wheel}"
6
+ : "${FIRST_RUN_PLATFORM:?Set FIRST_RUN_PLATFORM to the evidence platform name}"
7
+ : "${FIRST_RUN_TRIAL:?Set FIRST_RUN_TRIAL to the one-based trial number}"
8
+ : "${FIRST_RUN_TESTED_GITHUB_SHA:?Set FIRST_RUN_TESTED_GITHUB_SHA to github.sha}"
9
+ : "${FIRST_RUN_GITHUB_EVENT_NAME:?Set FIRST_RUN_GITHUB_EVENT_NAME to github.event_name}"
10
+
11
+ FIRST_RUN_DIR="${FIRST_RUN_DIR:-${RUNNER_TEMP:-/tmp}/graphcheck-first-run}"
12
+ FIRST_RUN_PR_HEAD_SHA="${FIRST_RUN_PR_HEAD_SHA:-}"
13
+ FIRST_RUN_RUNNER_OS="${FIRST_RUN_RUNNER_OS:-unknown}"
14
+ FIRST_RUN_RUNNER_IMAGE="${FIRST_RUN_RUNNER_IMAGE:-unknown}"
15
+ FIRST_RUN_VENV="${FIRST_RUN_VENV:-${RUNNER_TEMP:-/tmp}/graphcheck-first-run-venv-${FIRST_RUN_PLATFORM}-${FIRST_RUN_TRIAL}}"
16
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
+ evidence_script="$script_dir/first_run_evidence.py"
18
+ mkdir -p "$FIRST_RUN_DIR"
19
+
20
+ fail_stage() {
21
+ local stage="$1" fix="$2"
22
+ printf 'First-run failed during %s.\nFix: %s\n' "$stage" "$fix" | tee "$FIRST_RUN_DIR/failure.txt" >&2
23
+ exit 1
24
+ }
25
+
26
+ run_stage() {
27
+ local stage="$1" log="$2" fix="$3"
28
+ shift 3
29
+ set +e
30
+ "$@" 2>&1 | tee -a "$log"
31
+ local status=${PIPESTATUS[0]}
32
+ set -e
33
+ test "$status" -eq 0 || fail_stage "$stage" "$fix See $log for details."
34
+ }
35
+
36
+ shopt -s nullglob
37
+ wheels=("$GRAPHCHECK_WHEEL_DIR"/*.whl)
38
+ test "${#wheels[@]}" -eq 1 || fail_stage "wheel discovery" "Place exactly one built GraphCheck wheel in $GRAPHCHECK_WHEEL_DIR."
39
+ wheel="${wheels[0]}"
40
+
41
+ read -r started_at_ns started_monotonic_ns < <("$GRAPHCHECK_BOOTSTRAP_PYTHON" -c "import time; print(time.time_ns(), time.monotonic_ns())")
42
+ install_log="$FIRST_RUN_DIR/install.log"
43
+ run_stage "virtual environment creation" "$install_log" "Verify that Python includes venv support and the runner temp directory is writable." "$GRAPHCHECK_BOOTSTRAP_PYTHON" -m venv "$FIRST_RUN_VENV"
44
+ graphcheck_python="$FIRST_RUN_VENV/bin/python"
45
+ graphcheck_bin="$FIRST_RUN_VENV/bin/graphcheck"
46
+ run_stage "GraphCheck installation" "$install_log" "Verify package-index connectivity and that the wheel supports this Python version." "$graphcheck_python" -m pip install --disable-pip-version-check --no-cache-dir "$wheel"
47
+ install_finished_ns=$("$GRAPHCHECK_BOOTSTRAP_PYTHON" -c "import time; print(time.monotonic_ns())")
48
+
49
+ fixture_log="$FIRST_RUN_DIR/fixture.log"
50
+ run_stage "Neo4j fixture preparation" "$fixture_log" "Start the pinned Neo4j service and verify Bolt credentials." "$graphcheck_python" "$evidence_script" prepare-fixture
51
+ fixture_finished_ns=$("$GRAPHCHECK_BOOTSTRAP_PYTHON" -c "import time; print(time.monotonic_ns())")
52
+
53
+ project="$FIRST_RUN_DIR/project"
54
+ mkdir -p "$project"
55
+ cd "$project"
56
+ init_started_ns=$("$GRAPHCHECK_BOOTSTRAP_PYTHON" -c "import time; print(time.monotonic_ns())")
57
+ run_stage "graphcheck init" "$FIRST_RUN_DIR/init.log" "Run graphcheck debug --json and correct the reported connection or project configuration problem." "$graphcheck_bin" init
58
+ init_finished_ns=$("$GRAPHCHECK_BOOTSTRAP_PYTHON" -c "import time; print(time.monotonic_ns())")
59
+ grep -q "Detected Neo4j" "$FIRST_RUN_DIR/init.log" || fail_stage "graphcheck init connectivity" "Run graphcheck debug --json and correct the reported Neo4j connection problem."
60
+ grep -q "Traceback (most recent call last)" "$FIRST_RUN_DIR/init.log" && fail_stage "graphcheck init diagnostics" "Use the stable diagnostic instead of exposing a Python traceback."
61
+ for required in graphcheck.yml profiles.yml checks/example.yml; do
62
+ test -f "$required" || fail_stage "graphcheck init artifact validation" "Confirm the project directory is writable and retry graphcheck init."
63
+ done
64
+
65
+ run_started_ns=$("$GRAPHCHECK_BOOTSTRAP_PYTHON" -c "import time; print(time.monotonic_ns())")
66
+ set +e
67
+ "$graphcheck_bin" run 2>&1 | tee "$FIRST_RUN_DIR/run.log"
68
+ run_exit_code=${PIPESTATUS[0]}
69
+ set -e
70
+ run_finished_ns=$("$GRAPHCHECK_BOOTSTRAP_PYTHON" -c "import time; print(time.monotonic_ns())")
71
+ finished_at_ns=$("$GRAPHCHECK_BOOTSTRAP_PYTHON" -c "import time; print(time.time_ns())")
72
+ grep -q "Traceback (most recent call last)" "$FIRST_RUN_DIR/run.log" && fail_stage "graphcheck run diagnostics" "Use the stable diagnostic instead of exposing a Python traceback."
73
+
74
+ python_version=$("$graphcheck_python" -c "import platform; print(platform.python_version())")
75
+ run_stage "first-result validation" "$FIRST_RUN_DIR/validation.log" "Inspect results.json, report.html, and run.log; a valid result must be complete, scored, and execute at least one check." \
76
+ "$GRAPHCHECK_BOOTSTRAP_PYTHON" "$evidence_script" record \
77
+ --results .graphcheck/runs/latest/results.json \
78
+ --report .graphcheck/runs/latest/report.html \
79
+ --output "$FIRST_RUN_DIR/timing.json" \
80
+ --platform "$FIRST_RUN_PLATFORM" \
81
+ --trial "$FIRST_RUN_TRIAL" \
82
+ --run-exit-code "$run_exit_code" \
83
+ --started-at-ns "$started_at_ns" \
84
+ --finished-at-ns "$finished_at_ns" \
85
+ --install-ns "$((install_finished_ns - started_monotonic_ns))" \
86
+ --fixture-ns "$((fixture_finished_ns - install_finished_ns))" \
87
+ --init-ns "$((init_finished_ns - init_started_ns))" \
88
+ --run-ns "$((run_finished_ns - run_started_ns))" \
89
+ --total-ns "$((run_finished_ns - started_monotonic_ns))" \
90
+ --python-version "$python_version" \
91
+ --runner-os "$FIRST_RUN_RUNNER_OS" \
92
+ --runner-image "$FIRST_RUN_RUNNER_IMAGE" \
93
+ --tested-github-sha "$FIRST_RUN_TESTED_GITHUB_SHA" \
94
+ --pull-request-head-sha "$FIRST_RUN_PR_HEAD_SHA" \
95
+ --github-event-name "$FIRST_RUN_GITHUB_EVENT_NAME"
96
+
97
+ total_seconds=$("$GRAPHCHECK_BOOTSTRAP_PYTHON" -c "print(round(($run_finished_ns - $started_monotonic_ns) / 1_000_000_000, 3))")
98
+ printf 'Install -> init -> first valid run completed in %.3f seconds; median budget: <900 seconds.\n' "$total_seconds" | tee "$FIRST_RUN_DIR/timing.txt" | tee -a "$FIRST_RUN_DIR/run.log"
99
+
100
+ profile_log="$FIRST_RUN_DIR/profile.log"
101
+ run_stage "graphcheck profile smoke check" "$profile_log" "Run graphcheck debug --json and correct the reported profiling problem." "$graphcheck_bin" profile
102
+ grep -q "Traceback (most recent call last)" "$profile_log" && fail_stage "graphcheck profile diagnostics" "Use the stable diagnostic instead of exposing a Python traceback."
103
+ baseline_found=0
104
+ for baseline in .graphcheck/baselines/*.json; do
105
+ test -f "$baseline" && baseline_found=1 && break
106
+ done
107
+ test "$baseline_found" -eq 1 || fail_stage "graphcheck profile artifact validation" "Confirm the artifact directory is writable and retry graphcheck profile."
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env python3
2
+ """Record and aggregate clean-environment first-run evidence using only the stdlib."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import math
9
+ import statistics
10
+ import sys
11
+ import time
12
+ from datetime import UTC, datetime
13
+ from pathlib import Path
14
+
15
+ SCHEMA_VERSION = "1"
16
+ DEFAULT_PLATFORMS = ("linux-container", "macos", "wsl")
17
+
18
+
19
+ def _seconds(nanoseconds: int) -> float:
20
+ return round(nanoseconds / 1_000_000_000, 3)
21
+
22
+
23
+ def _timestamp(nanoseconds: int) -> str:
24
+ return datetime.fromtimestamp(nanoseconds / 1_000_000_000, UTC).isoformat()
25
+
26
+
27
+ def _read_results(path: Path, errors: list[str]) -> dict[str, object] | None:
28
+ try:
29
+ payload = json.loads(path.read_text(encoding="utf-8"))
30
+ except (OSError, json.JSONDecodeError) as exc:
31
+ errors.append(f"results.json could not be read: {type(exc).__name__}: {exc}")
32
+ return None
33
+ if not isinstance(payload, dict):
34
+ errors.append("results.json must contain a JSON object")
35
+ return None
36
+ return payload
37
+
38
+
39
+ def _result_facts(
40
+ payload: dict[str, object] | None, run_exit_code: int, errors: list[str]
41
+ ) -> tuple[int | None, int | float | None, str | None, str | None]:
42
+ if payload is None:
43
+ return None, None, None, None
44
+ run = payload.get("run")
45
+ totals = payload.get("totals")
46
+ score = payload.get("score")
47
+ if not isinstance(run, dict):
48
+ errors.append("results.json is missing the run object")
49
+ run = {}
50
+ if not isinstance(totals, dict):
51
+ errors.append("results.json is missing the totals object")
52
+ totals = {}
53
+ selected, skipped = totals.get("checks"), totals.get("skipped")
54
+ executed = (
55
+ selected - skipped
56
+ if isinstance(selected, int)
57
+ and not isinstance(selected, bool)
58
+ and isinstance(skipped, int)
59
+ and not isinstance(skipped, bool)
60
+ else None
61
+ )
62
+ if executed is None or executed < 1:
63
+ errors.append("results.json must contain at least one executed check")
64
+ run_status = run.get("run_status", run.get("status"))
65
+ if run_status != "complete":
66
+ errors.append("results.json run.run_status must be complete")
67
+ if run_exit_code not in {0, 1, 2}:
68
+ errors.append("a complete scored run must exit with code 0, 1, or 2")
69
+ if run.get("exit_code") != run_exit_code:
70
+ errors.append("the command exit code does not match results.json")
71
+ score_value = score.get("value") if isinstance(score, dict) else None
72
+ if (
73
+ isinstance(score_value, bool)
74
+ or not isinstance(score_value, (int, float))
75
+ or not math.isfinite(score_value)
76
+ ):
77
+ errors.append("results.json must contain a finite numeric score")
78
+ score_value = None
79
+ elif not 0 <= score_value <= 100:
80
+ errors.append("results.json score must be between 0 and 100")
81
+ target = run.get("target")
82
+ return (
83
+ executed,
84
+ score_value,
85
+ run.get("graphcheck_version") if isinstance(run.get("graphcheck_version"), str) else None,
86
+ (
87
+ target.get("server_version")
88
+ if isinstance(target, dict) and isinstance(target.get("server_version"), str)
89
+ else None
90
+ ),
91
+ )
92
+
93
+
94
+ def record(args: argparse.Namespace) -> int:
95
+ errors: list[str] = []
96
+ if args.trial < 1:
97
+ errors.append("trial must be a positive integer")
98
+ if args.budget_seconds < 1:
99
+ errors.append("budget_seconds must be a positive integer")
100
+ if args.finished_at_ns < args.started_at_ns:
101
+ errors.append("finished_at must not precede started_at")
102
+ if not args.tested_github_sha:
103
+ errors.append("tested_github_sha must identify the checkout tested by this trial")
104
+ if args.github_event_name == "pull_request" and not args.pull_request_head_sha:
105
+ errors.append("pull_request_head_sha is required for pull_request evidence")
106
+ for name in ("install", "fixture", "init", "run", "total"):
107
+ if getattr(args, f"{name}_ns") < 0:
108
+ errors.append(f"{name} timing must be non-negative")
109
+ results_path, report_path, output_path = map(Path, (args.results, args.report, args.output))
110
+ payload = _read_results(results_path, errors)
111
+ if not report_path.is_file() or report_path.stat().st_size < 1:
112
+ errors.append("report.html is missing or empty")
113
+ executed, score, graphcheck_version, neo4j_version = _result_facts(
114
+ payload, args.run_exit_code, errors
115
+ )
116
+ timings = {
117
+ name: _seconds(getattr(args, f"{name}_ns"))
118
+ for name in ("install", "fixture", "init", "run", "total")
119
+ }
120
+ evidence = {
121
+ "schema_version": SCHEMA_VERSION,
122
+ "platform": args.platform,
123
+ "trial": args.trial,
124
+ "github_event_name": args.github_event_name,
125
+ "tested_github_sha": args.tested_github_sha,
126
+ "pull_request_head_sha": args.pull_request_head_sha or None,
127
+ "provenance_note": (
128
+ "tested_github_sha is github.sha: the synthetic merge commit for pull_request events "
129
+ "and the pushed commit for push events"
130
+ ),
131
+ "runner_os": args.runner_os,
132
+ "runner_image": args.runner_image,
133
+ "python_version": args.python_version,
134
+ "graphcheck_version": graphcheck_version,
135
+ "neo4j_version": neo4j_version,
136
+ "timing_boundary": "clean-wheel-install-start_to_valid-init-run-result",
137
+ "database_ready_before_install": False,
138
+ "profile_smoke_after_timing": True,
139
+ "started_at": _timestamp(args.started_at_ns),
140
+ "finished_at": _timestamp(args.finished_at_ns),
141
+ "timings_seconds": timings,
142
+ "budget_seconds": args.budget_seconds,
143
+ "within_individual_budget": timings["total"] < args.budget_seconds,
144
+ "run_exit_code": args.run_exit_code,
145
+ "executed_checks": executed,
146
+ "score": score,
147
+ "valid": not errors,
148
+ "validation_errors": errors,
149
+ "artifacts": {"results": str(results_path), "report": str(report_path)},
150
+ }
151
+ output_path.parent.mkdir(parents=True, exist_ok=True)
152
+ output_path.write_text(json.dumps(evidence, indent=2) + "\n", encoding="utf-8")
153
+ if errors:
154
+ for error in errors:
155
+ print(f"First-run evidence invalid: {error}", file=sys.stderr)
156
+ print(
157
+ f"Fix: inspect the command logs and {output_path}, correct the failure, and retry.",
158
+ file=sys.stderr,
159
+ )
160
+ return 1
161
+ print(
162
+ f"Valid first result: {executed} executed check(s), score {score}, "
163
+ f"install-to-result {timings['total']:.3f}s."
164
+ )
165
+ return 0
166
+
167
+
168
+ def prepare_fixture(args: argparse.Namespace) -> int:
169
+ from neo4j import GraphDatabase
170
+
171
+ deadline, last_error = time.monotonic() + args.timeout_seconds, None
172
+ while time.monotonic() < deadline:
173
+ try:
174
+ with GraphDatabase.driver(
175
+ args.uri,
176
+ auth=(args.user, args.password),
177
+ connection_timeout=1,
178
+ ) as driver:
179
+ driver.verify_connectivity()
180
+ driver.execute_query(
181
+ "MERGE (:Customer {name: 'Ada'})",
182
+ database_=args.database,
183
+ )
184
+ print(f"Prepared first-run fixture at {args.uri}/{args.database}.")
185
+ return 0
186
+ except Exception as exc:
187
+ last_error = exc
188
+ time.sleep(2)
189
+ print(
190
+ "First-run fixture setup failed: Neo4j did not become ready "
191
+ f"within {args.timeout_seconds} seconds ({type(last_error).__name__}: {last_error}).\n"
192
+ "Fix: verify that Neo4j is running, Bolt is reachable, and the configured credentials "
193
+ "are correct.",
194
+ file=sys.stderr,
195
+ )
196
+ return 1
197
+
198
+
199
+ def _load_samples(root: Path) -> tuple[dict[tuple[str, int], dict[str, object]], list[str]]:
200
+ samples: dict[tuple[str, int], dict[str, object]] = {}
201
+ errors: list[str] = []
202
+ for path in sorted(root.rglob("timing.json")):
203
+ try:
204
+ sample = json.loads(path.read_text(encoding="utf-8"))
205
+ key = (str(sample["platform"]), int(sample["trial"]))
206
+ except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as exc:
207
+ errors.append(f"{path}: unreadable timing evidence ({type(exc).__name__}: {exc})")
208
+ continue
209
+ if key in samples:
210
+ errors.append(f"duplicate timing evidence for {key[0]} trial {key[1]}")
211
+ else:
212
+ samples[key] = sample
213
+ return samples, errors
214
+
215
+
216
+ def summarize(args: argparse.Namespace) -> int:
217
+ root, output_dir = Path(args.input), Path(args.output)
218
+ samples, errors = _load_samples(root)
219
+ tested_github_shas = sorted(
220
+ {
221
+ value
222
+ for sample in samples.values()
223
+ if isinstance((value := sample.get("tested_github_sha")), str) and value
224
+ }
225
+ )
226
+ pull_request_head_shas = sorted(
227
+ {
228
+ value
229
+ for sample in samples.values()
230
+ if isinstance((value := sample.get("pull_request_head_sha")), str) and value
231
+ }
232
+ )
233
+ github_event_names = sorted(
234
+ {
235
+ value
236
+ for sample in samples.values()
237
+ if isinstance((value := sample.get("github_event_name")), str) and value
238
+ }
239
+ )
240
+ for (platform_name, trial), sample in samples.items():
241
+ if not isinstance(sample.get("tested_github_sha"), str) or not sample["tested_github_sha"]:
242
+ errors.append(f"{platform_name} trial {trial} is missing tested_github_sha")
243
+ if sample.get("github_event_name") == "pull_request" and not sample.get(
244
+ "pull_request_head_sha"
245
+ ):
246
+ errors.append(f"{platform_name} trial {trial} is missing pull_request_head_sha")
247
+ if len(tested_github_shas) != 1:
248
+ errors.append("timing evidence must contain exactly one tested GitHub SHA")
249
+ if len(pull_request_head_shas) > 1:
250
+ errors.append("timing evidence contains more than one pull-request head SHA")
251
+ if len(github_event_names) != 1:
252
+ errors.append("timing evidence must contain exactly one GitHub event name")
253
+ platforms: dict[str, dict[str, object]] = {}
254
+ for platform_name in args.platforms:
255
+ observations: list[dict[str, object]] = []
256
+ for trial in range(1, args.trials + 1):
257
+ sample = samples.get((platform_name, trial))
258
+ if sample is None:
259
+ errors.append(f"missing timing evidence for {platform_name} trial {trial}")
260
+ continue
261
+ timings = sample.get("timings_seconds")
262
+ total = timings.get("total") if isinstance(timings, dict) else None
263
+ if sample.get("valid") is not True:
264
+ errors.append(f"{platform_name} trial {trial} did not produce a valid result")
265
+ if (
266
+ isinstance(total, bool)
267
+ or not isinstance(total, (int, float))
268
+ or not math.isfinite(total)
269
+ or total < 0
270
+ ):
271
+ errors.append(f"{platform_name} trial {trial} has invalid total timing")
272
+ continue
273
+ observations.append({"trial": trial, "total_seconds": total})
274
+ median_seconds = (
275
+ round(statistics.median(item["total_seconds"] for item in observations), 3)
276
+ if len(observations) == args.trials
277
+ else None
278
+ )
279
+ under_budget = median_seconds is not None and median_seconds < args.budget_seconds
280
+ if median_seconds is not None and not under_budget:
281
+ errors.append(
282
+ f"{platform_name} median {median_seconds:.3f}s is not under {args.budget_seconds}s"
283
+ )
284
+ platforms[platform_name] = {
285
+ "samples": observations,
286
+ "median_seconds": median_seconds,
287
+ "under_budget": under_budget,
288
+ }
289
+ passed = not errors
290
+ summary = {
291
+ "schema_version": SCHEMA_VERSION,
292
+ "metric": "clean-wheel-install-start_to_valid-init-run-result",
293
+ "tested_github_sha": tested_github_shas[0] if len(tested_github_shas) == 1 else None,
294
+ "pull_request_head_sha": (
295
+ pull_request_head_shas[0] if len(pull_request_head_shas) == 1 else None
296
+ ),
297
+ "github_event_name": github_event_names[0] if len(github_event_names) == 1 else None,
298
+ "budget_seconds": args.budget_seconds,
299
+ "trials_per_platform": args.trials,
300
+ "platforms": platforms,
301
+ "passed": passed,
302
+ "errors": errors,
303
+ }
304
+ lines = ["# GraphCheck first-run evidence", ""]
305
+ if len(tested_github_shas) == 1:
306
+ lines.extend((f"Tested GitHub SHA: `{tested_github_shas[0]}`", ""))
307
+ if len(pull_request_head_shas) == 1:
308
+ lines.extend((f"Pull-request head SHA: `{pull_request_head_shas[0]}`", ""))
309
+ lines.extend(
310
+ (
311
+ "Budget: median install-to-first-valid-result "
312
+ f"**under {args.budget_seconds // 60} minutes**.",
313
+ "",
314
+ "| Platform | Samples (seconds) | Median (seconds) | Result |",
315
+ "| --- | ---: | ---: | --- |",
316
+ )
317
+ )
318
+ for name, result in platforms.items():
319
+ sample_text = (
320
+ ", ".join(str(item["total_seconds"]) for item in result["samples"]) or "missing"
321
+ )
322
+ median_text = (
323
+ str(result["median_seconds"]) if result["median_seconds"] is not None else "n/a"
324
+ )
325
+ lines.append(
326
+ f"| {name} | {sample_text} | {median_text} | "
327
+ f"{'PASS' if result['under_budget'] else 'FAIL'} |"
328
+ )
329
+ if errors:
330
+ lines.extend(("", "## Errors", "", *(f"- {error}" for error in errors)))
331
+ lines.extend(("", f"Overall: **{'PASS' if passed else 'FAIL'}**", ""))
332
+ output_dir.mkdir(parents=True, exist_ok=True)
333
+ (output_dir / "first-run-summary.json").write_text(
334
+ json.dumps(summary, indent=2) + "\n", encoding="utf-8"
335
+ )
336
+ (output_dir / "first-run-summary.md").write_text("\n".join(lines), encoding="utf-8")
337
+ print("\n".join(lines))
338
+ return 0 if passed else 1
339
+
340
+
341
+ def parser() -> argparse.ArgumentParser:
342
+ root = argparse.ArgumentParser()
343
+ commands = root.add_subparsers(dest="command", required=True)
344
+ record_parser = commands.add_parser("record")
345
+ for name in (
346
+ "results",
347
+ "report",
348
+ "output",
349
+ "platform",
350
+ "python-version",
351
+ "runner-os",
352
+ "runner-image",
353
+ "tested-github-sha",
354
+ "github-event-name",
355
+ ):
356
+ record_parser.add_argument(f"--{name}", required=True)
357
+ for name in (
358
+ "started-at-ns",
359
+ "finished-at-ns",
360
+ "install-ns",
361
+ "fixture-ns",
362
+ "init-ns",
363
+ "run-ns",
364
+ "total-ns",
365
+ ):
366
+ record_parser.add_argument(f"--{name}", type=int, required=True)
367
+ record_parser.add_argument("--trial", type=int, required=True)
368
+ record_parser.add_argument("--run-exit-code", type=int, required=True)
369
+ record_parser.add_argument("--pull-request-head-sha", default="")
370
+ record_parser.add_argument("--budget-seconds", type=int, default=900)
371
+ record_parser.set_defaults(handler=record)
372
+ fixture_parser = commands.add_parser("prepare-fixture")
373
+ fixture_parser.add_argument("--uri", default="bolt://localhost:7687")
374
+ fixture_parser.add_argument("--user", default="neo4j")
375
+ fixture_parser.add_argument("--password", default="graphora")
376
+ fixture_parser.add_argument("--database", default="neo4j")
377
+ fixture_parser.add_argument("--timeout-seconds", type=int, default=120)
378
+ fixture_parser.set_defaults(handler=prepare_fixture)
379
+ summary_parser = commands.add_parser("summarize")
380
+ summary_parser.add_argument("--input", required=True)
381
+ summary_parser.add_argument("--output", required=True)
382
+ summary_parser.add_argument("--platforms", nargs="+", default=list(DEFAULT_PLATFORMS))
383
+ summary_parser.add_argument("--trials", type=int, default=3)
384
+ summary_parser.add_argument("--budget-seconds", type=int, default=900)
385
+ summary_parser.set_defaults(handler=summarize)
386
+ return root
387
+
388
+
389
+ def main() -> int:
390
+ try:
391
+ args = parser().parse_args()
392
+ return args.handler(args)
393
+ except Exception as exc:
394
+ print(
395
+ f"First-run evidence tooling failed: {type(exc).__name__}: {exc}\n"
396
+ "Fix: verify the evidence arguments and files, then retry.",
397
+ file=sys.stderr,
398
+ )
399
+ return 1
400
+
401
+
402
+ if __name__ == "__main__":
403
+ raise SystemExit(main())
@@ -0,0 +1,144 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [development, main]
7
+
8
+ jobs:
9
+ secret-scan:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ with:
16
+ fetch-depth: 0
17
+ - name: Install checksum-verified gitleaks
18
+ env:
19
+ GITLEAKS_VERSION: 8.29.1
20
+ GITLEAKS_SHA256: e4eb209d04e20339d77122a3bdf9cd41351255cfb27ebcb75e85325e04f88924
21
+ run: >-
22
+ archive="$RUNNER_TEMP/gitleaks.tar.gz"; curl --fail --silent --show-error --location
23
+ "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
24
+ --output "$archive"; echo "$GITLEAKS_SHA256 $archive" | sha256sum --check -;
25
+ tar -xzf "$archive" -C "$RUNNER_TEMP" gitleaks; echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
26
+ - name: Scan pull-request commits
27
+ if: github.event_name == 'pull_request'
28
+ env:
29
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
30
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
31
+ run: gitleaks git --redact --log-opts="${BASE_SHA}..${HEAD_SHA}" .
32
+ - name: Scan protected-branch history
33
+ if: github.event_name == 'push'
34
+ run: gitleaks git --redact .
35
+
36
+ lint:
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - uses: actions/checkout@v4
40
+ - uses: astral-sh/setup-uv@v5
41
+ - run: uv sync --group dev
42
+ - run: uv run ruff check .
43
+ - run: uv run ruff format --check .
44
+
45
+ # Repo-wide suite on the locked driver. The `test (3.12)` / `test (3.13)` contexts are
46
+ # required by the branch ruleset, so this job's name and matrix must stay stable.
47
+ test:
48
+ runs-on: ubuntu-latest
49
+ strategy:
50
+ fail-fast: false
51
+ matrix:
52
+ python-version: ["3.12", "3.13", "3.14"]
53
+ steps:
54
+ - uses: actions/checkout@v4
55
+ with:
56
+ submodules: recursive
57
+ - uses: astral-sh/setup-uv@v5
58
+ with:
59
+ python-version: ${{ matrix.python-version }}
60
+ - run: uv sync --group dev
61
+ - run: uv run pytest --cov=graphcheck --cov-report=term-missing --cov-fail-under=80
62
+
63
+ # Neo4j Python driver compatibility across the supported range, on a separate job name so
64
+ # it does not collide with the required `test` contexts above.
65
+ driver-compat:
66
+ runs-on: ubuntu-latest
67
+ strategy:
68
+ fail-fast: false
69
+ matrix:
70
+ include:
71
+ - python-version: "3.12"
72
+ driver-line: min
73
+ - python-version: "3.14"
74
+ driver-line: latest-6
75
+ steps:
76
+ - uses: actions/checkout@v4
77
+ with:
78
+ submodules: recursive
79
+ - name: Install uv and select the matrix Python
80
+ uses: astral-sh/setup-uv@v5
81
+ with:
82
+ # Sets UV_PYTHON, which overrides the committed .python-version so uv
83
+ # runs on the matrix interpreter (not the pinned 3.12).
84
+ python-version: ${{ matrix.python-version }}
85
+ - run: uv sync --group dev
86
+ - name: Select tested Neo4j Python driver line
87
+ env:
88
+ DRIVER_LINE: ${{ matrix.driver-line }}
89
+ run: |
90
+ if [ "$DRIVER_LINE" = "min" ]; then
91
+ uv pip install "neo4j==5.20.0"
92
+ else
93
+ uv pip install --upgrade "neo4j>=6,<7"
94
+ fi
95
+ - name: Assert the interpreter matches the matrix (guards against a single pinned run)
96
+ env:
97
+ WANT: ${{ matrix.python-version }}
98
+ run: uv run --no-sync python -c "import os, sys; want=tuple(int(p) for p in os.environ['WANT'].split('.')); assert sys.version_info[:2]==want, sys.version"
99
+ - name: Test installed driver line
100
+ env:
101
+ PYTHONDEVMODE: ${{ matrix.driver-line == 'min' && '1' || '0' }}
102
+ PYTHONWARNINGS: ${{ matrix.driver-line == 'min' && 'error::ResourceWarning' || 'default' }}
103
+ run: >
104
+ uv run --no-sync pytest
105
+ tests/unit/connectors/test_neo4j_adapter.py
106
+ tests/unit/engine/test_executor.py
107
+ tests/unit/application/test_profiler.py
108
+ tests/unit/application/test_debug_diagnostics.py
109
+
110
+ integration:
111
+ runs-on: ubuntu-latest
112
+ timeout-minutes: 15
113
+ strategy:
114
+ fail-fast: false
115
+ matrix:
116
+ neo4j-target: [lts-cypher-5, current-cypher-5, current-cypher-25]
117
+ env:
118
+ GRAPHCHECK_NEO4J_INTEGRATION: "1"
119
+ GRAPHCHECK_NEO4J_TARGET: ${{ matrix.neo4j-target }}
120
+ steps:
121
+ - uses: actions/checkout@v4
122
+ - uses: astral-sh/setup-uv@v5
123
+ - run: uv sync --group dev
124
+ - run: >
125
+ uv run pytest
126
+ tests/integration/test_integration_neo4j_adapter.py
127
+ tests/integration/test_integration_engine.py
128
+ tests/integration/test_hostile_graphs.py
129
+ tests/integration/test_mcp_neo4j_workflow.py
130
+ tests/integration/test_performance_plans.py
131
+ -v
132
+
133
+ installed-wheel:
134
+ runs-on: ubuntu-latest
135
+ steps:
136
+ - uses: actions/checkout@v4
137
+ - uses: astral-sh/setup-uv@v5
138
+ - run: uv build
139
+ - run: uv venv .wheel-venv
140
+ - run: uv pip install --python .wheel-venv/bin/python dist/*.whl
141
+ - run: .wheel-venv/bin/graphcheck --version
142
+ - run: .wheel-venv/bin/graphcheck --help
143
+ - name: Verify installed package resources
144
+ run: .wheel-venv/bin/python -c "from importlib.resources import files; assert files('graphcheck').joinpath('packs/core.yml').is_file(); assert files('graphcheck').joinpath('packs/pii.yml').is_file()"