spanforge 2.0.0__tar.gz → 2.0.1__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 (319) hide show
  1. {spanforge-2.0.0 → spanforge-2.0.1}/CONFORMANCE.md +4 -0
  2. {spanforge-2.0.0 → spanforge-2.0.1}/PKG-INFO +4 -4
  3. {spanforge-2.0.0 → spanforge-2.0.1}/README.md +3 -3
  4. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/redact.md +1 -1
  5. {spanforge-2.0.0 → spanforge-2.0.1}/docs/changelog.md +32 -1
  6. {spanforge-2.0.0 → spanforge-2.0.1}/docs/cli.md +1 -1
  7. {spanforge-2.0.0 → spanforge-2.0.1}/docs/quickstart.md +22 -0
  8. {spanforge-2.0.0 → spanforge-2.0.1}/pyproject.toml +1 -1
  9. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/__init__.py +1 -1
  10. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/core/compliance_mapping.py +10 -3
  11. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/redact.py +97 -0
  12. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_redact.py +200 -0
  13. {spanforge-2.0.0 → spanforge-2.0.1}/.gitattributes +0 -0
  14. {spanforge-2.0.0 → spanforge-2.0.1}/.github/CODEOWNERS +0 -0
  15. {spanforge-2.0.0 → spanforge-2.0.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  16. {spanforge-2.0.0 → spanforge-2.0.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  17. {spanforge-2.0.0 → spanforge-2.0.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  18. {spanforge-2.0.0 → spanforge-2.0.1}/.github/ISSUE_TEMPLATE/rfc.yml +0 -0
  19. {spanforge-2.0.0 → spanforge-2.0.1}/.github/pull_request_template.md +0 -0
  20. {spanforge-2.0.0 → spanforge-2.0.1}/.github/workflows/ci.yml +0 -0
  21. {spanforge-2.0.0 → spanforge-2.0.1}/.github/workflows/release.yml +0 -0
  22. {spanforge-2.0.0 → spanforge-2.0.1}/.gitignore +0 -0
  23. {spanforge-2.0.0 → spanforge-2.0.1}/CNAME +0 -0
  24. {spanforge-2.0.0 → spanforge-2.0.1}/CODE_OF_CONDUCT.md +0 -0
  25. {spanforge-2.0.0 → spanforge-2.0.1}/LICENSE +0 -0
  26. {spanforge-2.0.0 → spanforge-2.0.1}/MAINTAINERS.md +0 -0
  27. {spanforge-2.0.0 → spanforge-2.0.1}/PRICING.md +0 -0
  28. {spanforge-2.0.0 → spanforge-2.0.1}/README.md.bak +0 -0
  29. {spanforge-2.0.0 → spanforge-2.0.1}/RELEASE.md +0 -0
  30. {spanforge-2.0.0 → spanforge-2.0.1}/SECURITY.md +0 -0
  31. {spanforge-2.0.0 → spanforge-2.0.1}/docs/Makefile +0 -0
  32. {spanforge-2.0.0 → spanforge-2.0.1}/docs/_static/.gitkeep +0 -0
  33. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/auto.md +0 -0
  34. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/cache.md +0 -0
  35. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/compliance.md +0 -0
  36. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/consumer.md +0 -0
  37. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/debug.md +0 -0
  38. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/deprecations.md +0 -0
  39. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/event.md +0 -0
  40. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/exceptions.md +0 -0
  41. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/export.md +0 -0
  42. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/governance.md +0 -0
  43. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/hooks.md +0 -0
  44. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/index.md +0 -0
  45. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/integrations.md +0 -0
  46. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/lint.md +0 -0
  47. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/metrics.md +0 -0
  48. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/migrate.md +0 -0
  49. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/models.md +0 -0
  50. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/normalizer.md +0 -0
  51. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/signing.md +0 -0
  52. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/store.md +0 -0
  53. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/stream.md +0 -0
  54. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/testing.md +0 -0
  55. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/trace.md +0 -0
  56. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/types.md +0 -0
  57. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/ulid.md +0 -0
  58. {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/validate.md +0 -0
  59. {spanforge-2.0.0 → spanforge-2.0.1}/docs/conf.py +0 -0
  60. {spanforge-2.0.0 → spanforge-2.0.1}/docs/configuration.md +0 -0
  61. {spanforge-2.0.0 → spanforge-2.0.1}/docs/contributing.md +0 -0
  62. {spanforge-2.0.0 → spanforge-2.0.1}/docs/deployment/air-gapped.md +0 -0
  63. {spanforge-2.0.0 → spanforge-2.0.1}/docs/deployment/kubernetes.md +0 -0
  64. {spanforge-2.0.0 → spanforge-2.0.1}/docs/index.md +0 -0
  65. {spanforge-2.0.0 → spanforge-2.0.1}/docs/installation.md +0 -0
  66. {spanforge-2.0.0 → spanforge-2.0.1}/docs/integrations/crewai.md +0 -0
  67. {spanforge-2.0.0 → spanforge-2.0.1}/docs/make.bat +0 -0
  68. {spanforge-2.0.0 → spanforge-2.0.1}/docs/migrations/from-langfuse.md +0 -0
  69. {spanforge-2.0.0 → spanforge-2.0.1}/docs/migrations/from-langsmith.md +0 -0
  70. {spanforge-2.0.0 → spanforge-2.0.1}/docs/migrations/from-openllmetry.md +0 -0
  71. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/audit.md +0 -0
  72. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/cache.md +0 -0
  73. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/consent.md +0 -0
  74. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/cost.md +0 -0
  75. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/diff.md +0 -0
  76. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/eval.md +0 -0
  77. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/explanation.md +0 -0
  78. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/fence.md +0 -0
  79. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/guard.md +0 -0
  80. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/hitl.md +0 -0
  81. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/index.md +0 -0
  82. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/model_registry.md +0 -0
  83. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/prompt.md +0 -0
  84. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/redact_ns.md +0 -0
  85. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/template.md +0 -0
  86. {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/trace.md +0 -0
  87. {spanforge-2.0.0 → spanforge-2.0.1}/docs/rfc/rfc-0001.md +0 -0
  88. {spanforge-2.0.0 → spanforge-2.0.1}/docs/runbook.md +0 -0
  89. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/README.md +0 -0
  90. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/envelope.schema.json +0 -0
  91. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/agent-run.schema.json +0 -0
  92. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/agent-step.schema.json +0 -0
  93. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/audit.schema.json +0 -0
  94. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/cache.schema.json +0 -0
  95. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/consent.schema.json +0 -0
  96. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/cost.schema.json +0 -0
  97. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/diff.schema.json +0 -0
  98. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/eval.schema.json +0 -0
  99. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/explanation.schema.json +0 -0
  100. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/fence.schema.json +0 -0
  101. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/guard.schema.json +0 -0
  102. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/hitl.schema.json +0 -0
  103. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/model-registry.schema.json +0 -0
  104. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/prompt.schema.json +0 -0
  105. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/redact.schema.json +0 -0
  106. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/span.schema.json +0 -0
  107. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/template.schema.json +0 -0
  108. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/types/common.schema.json +0 -0
  109. {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema-versioning.md +0 -0
  110. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/cache.md +0 -0
  111. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/compliance.md +0 -0
  112. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/custom_exporters.md +0 -0
  113. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/debugging.md +0 -0
  114. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/events.md +0 -0
  115. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/export.md +0 -0
  116. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/governance.md +0 -0
  117. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/index.md +0 -0
  118. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/linting.md +0 -0
  119. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/metrics.md +0 -0
  120. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/migration.md +0 -0
  121. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/redaction.md +0 -0
  122. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/signing.md +0 -0
  123. {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/tracing.md +0 -0
  124. {spanforge-2.0.0 → spanforge-2.0.1}/examples/agent_workflow.py +0 -0
  125. {spanforge-2.0.0 → spanforge-2.0.1}/examples/budget_alert.py +0 -0
  126. {spanforge-2.0.0 → spanforge-2.0.1}/examples/docker/Dockerfile +0 -0
  127. {spanforge-2.0.0 → spanforge-2.0.1}/examples/docker/docker-compose.yml +0 -0
  128. {spanforge-2.0.0 → spanforge-2.0.1}/examples/docker/otel-config.yaml +0 -0
  129. {spanforge-2.0.0 → spanforge-2.0.1}/examples/langchain_chain.py +0 -0
  130. {spanforge-2.0.0 → spanforge-2.0.1}/examples/multi_agent_rag.py +0 -0
  131. {spanforge-2.0.0 → spanforge-2.0.1}/examples/multi_tenant.py +0 -0
  132. {spanforge-2.0.0 → spanforge-2.0.1}/examples/openai_chat.py +0 -0
  133. {spanforge-2.0.0 → spanforge-2.0.1}/examples/otlp_grafana.py +0 -0
  134. {spanforge-2.0.0 → spanforge-2.0.1}/examples/production_multi_agent.py +0 -0
  135. {spanforge-2.0.0 → spanforge-2.0.1}/examples/secure_pipeline.py +0 -0
  136. {spanforge-2.0.0 → spanforge-2.0.1}/examples/streaming_response.py +0 -0
  137. {spanforge-2.0.0 → spanforge-2.0.1}/sonar-project.properties +0 -0
  138. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_batch_exporter.py +0 -0
  139. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_cli.py +0 -0
  140. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_hooks.py +0 -0
  141. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_server.py +0 -0
  142. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_span.py +0 -0
  143. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_store.py +0 -0
  144. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_stream.py +0 -0
  145. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_trace.py +0 -0
  146. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_tracer.py +0 -0
  147. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/actor.py +0 -0
  148. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/alerts.py +0 -0
  149. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/auto.py +0 -0
  150. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/baseline.py +0 -0
  151. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/config.py +0 -0
  152. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/consent.py +0 -0
  153. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/consumer.py +0 -0
  154. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/core/__init__.py +0 -0
  155. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/cost.py +0 -0
  156. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/debug.py +0 -0
  157. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/drift.py +0 -0
  158. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/egress.py +0 -0
  159. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/eval.py +0 -0
  160. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/event.py +0 -0
  161. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/exceptions.py +0 -0
  162. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/explain.py +0 -0
  163. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/__init__.py +0 -0
  164. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/append_only.py +0 -0
  165. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/cloud.py +0 -0
  166. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/datadog.py +0 -0
  167. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/grafana.py +0 -0
  168. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/jsonl.py +0 -0
  169. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/otel_bridge.py +0 -0
  170. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/otlp.py +0 -0
  171. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/otlp_bridge.py +0 -0
  172. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/redis_backend.py +0 -0
  173. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/webhook.py +0 -0
  174. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/exporters/__init__.py +0 -0
  175. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/exporters/console.py +0 -0
  176. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/exporters/jsonl.py +0 -0
  177. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/hitl.py +0 -0
  178. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/inspect.py +0 -0
  179. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/__init__.py +0 -0
  180. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/_pricing.py +0 -0
  181. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/anthropic.py +0 -0
  182. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/bedrock.py +0 -0
  183. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/crewai.py +0 -0
  184. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/gemini.py +0 -0
  185. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/groq.py +0 -0
  186. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/langchain.py +0 -0
  187. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/llamaindex.py +0 -0
  188. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/ollama.py +0 -0
  189. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/openai.py +0 -0
  190. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/together.py +0 -0
  191. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/metrics.py +0 -0
  192. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/metrics_export.py +0 -0
  193. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/migrate.py +0 -0
  194. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/model_registry.py +0 -0
  195. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/models.py +0 -0
  196. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/__init__.py +0 -0
  197. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/audit.py +0 -0
  198. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/cache.py +0 -0
  199. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/chain.py +0 -0
  200. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/confidence.py +0 -0
  201. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/consent.py +0 -0
  202. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/cost.py +0 -0
  203. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/decision.py +0 -0
  204. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/diff.py +0 -0
  205. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/drift.py +0 -0
  206. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/eval_.py +0 -0
  207. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/fence.py +0 -0
  208. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/guard.py +0 -0
  209. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/hitl.py +0 -0
  210. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/latency.py +0 -0
  211. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/prompt.py +0 -0
  212. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/redact.py +0 -0
  213. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/template.py +0 -0
  214. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/tool_call.py +0 -0
  215. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/trace.py +0 -0
  216. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/normalizer.py +0 -0
  217. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/presidio_backend.py +0 -0
  218. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/processor.py +0 -0
  219. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/prompt_registry.py +0 -0
  220. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/py.typed +0 -0
  221. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/sampling.py +0 -0
  222. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/schemas/v1.0/schema.json +0 -0
  223. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/schemas/v2.0/schema.json +0 -0
  224. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/signing.py +0 -0
  225. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/stream.py +0 -0
  226. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/testing.py +0 -0
  227. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/trace.py +0 -0
  228. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/types.py +0 -0
  229. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/ulid.py +0 -0
  230. {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/validate.py +0 -0
  231. {spanforge-2.0.0 → spanforge-2.0.1}/test_agent.jsonl +0 -0
  232. {spanforge-2.0.0 → spanforge-2.0.1}/test_events.jsonl +0 -0
  233. {spanforge-2.0.0 → spanforge-2.0.1}/tests/__init__.py +0 -0
  234. {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/__init__.py +0 -0
  235. {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures/chain.json +0 -0
  236. {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures/compliance.json +0 -0
  237. {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures/key_security.json +0 -0
  238. {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures/migration.json +0 -0
  239. {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures/pii.json +0 -0
  240. {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures/signing.json +0 -0
  241. {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures.json +0 -0
  242. {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/run_conformance.py +0 -0
  243. {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/test_conformance.py +0 -0
  244. {spanforge-2.0.0 → spanforge-2.0.1}/tests/conftest.py +0 -0
  245. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_actor.py +0 -0
  246. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_alerts.py +0 -0
  247. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_auto.py +0 -0
  248. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_baseline.py +0 -0
  249. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_benchmarks.py +0 -0
  250. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_budget_alert.py +0 -0
  251. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_cli.py +0 -0
  252. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_compliance_mapping.py +0 -0
  253. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_consent.py +0 -0
  254. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_consumer.py +0 -0
  255. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_cost_event_emission.py +0 -0
  256. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_cost_tracker.py +0 -0
  257. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_costguard_gaps.py +0 -0
  258. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_drift.py +0 -0
  259. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_event.py +0 -0
  260. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_exceptions.py +0 -0
  261. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_explain.py +0 -0
  262. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_cloud.py +0 -0
  263. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_datadog.py +0 -0
  264. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_grafana.py +0 -0
  265. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_jsonl.py +0 -0
  266. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_otel_bridge.py +0 -0
  267. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_otlp.py +0 -0
  268. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_redis_backend.py +0 -0
  269. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_webhook.py +0 -0
  270. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_hitl.py +0 -0
  271. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_inspect.py +0 -0
  272. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_integration.py +0 -0
  273. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_integrations.py +0 -0
  274. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_migrate.py +0 -0
  275. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_model_registry.py +0 -0
  276. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_models.py +0 -0
  277. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_namespaces.py +0 -0
  278. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_otlp_bridge.py +0 -0
  279. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase10_features.py +0 -0
  280. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase11_security.py +0 -0
  281. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase1_context_trace.py +0 -0
  282. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase2_observability.py +0 -0
  283. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase3_debug_sampling.py +0 -0
  284. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase4_agent_instrumentation.py +0 -0
  285. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase4_metrics_store.py +0 -0
  286. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase5_console_exporter.py +0 -0
  287. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase5_coverage.py +0 -0
  288. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase5_hooks_crewai.py +0 -0
  289. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase6_openai_integration.py +0 -0
  290. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_processor_coverage.py +0 -0
  291. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_properties.py +0 -0
  292. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_rfc_namespaces.py +0 -0
  293. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sampling_coverage.py +0 -0
  294. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_config.py +0 -0
  295. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_coverage_boost.py +0 -0
  296. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_exporters.py +0 -0
  297. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_final_coverage.py +0 -0
  298. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_gap_filler.py +0 -0
  299. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_openai_integration.py +0 -0
  300. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_phase7_integrations.py +0 -0
  301. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_precision_coverage.py +0 -0
  302. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_span.py +0 -0
  303. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_stream.py +0 -0
  304. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_tracer.py +0 -0
  305. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_validation_coverage.py +0 -0
  306. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_server.py +0 -0
  307. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sf11.py +0 -0
  308. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sf12.py +0 -0
  309. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sf13.py +0 -0
  310. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sf14.py +0 -0
  311. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sf15.py +0 -0
  312. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sf16.py +0 -0
  313. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_signing.py +0 -0
  314. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_stream.py +0 -0
  315. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_trace_decorator.py +0 -0
  316. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_trace_pytest_fixtures.py +0 -0
  317. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_types.py +0 -0
  318. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_ulid.py +0 -0
  319. {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_validate.py +0 -0
@@ -45,6 +45,8 @@ Each fixture carries a `clause` field mapping to a numbered requirement:
45
45
  | GA-03-REQ-02 | scan_payload MUST detect SSN patterns |
46
46
  | GA-03-REQ-03 | PIIScanHit MUST include match_count and sensitivity fields |
47
47
  | GA-03-REQ-04 | Credit card detection MUST apply Luhn validation |
48
+ | GA-03-REQ-05 | SSN detection MUST reject invalid SSA ranges (area 000, 666, 900–999; group 00; serial 0000) |
49
+ | GA-03-REQ-06 | date_of_birth detection MUST reject calendar-invalid dates (e.g. Feb 30, month 13) |
48
50
  | GA-04-REQ-01 | GDPR erasure MUST produce tombstone events preserving chain integrity |
49
51
  | GA-05-REQ-01 | v1_to_v2 MUST rename 'model' to 'model_id' and bump schema_version |
50
52
  | GA-05-REQ-02 | md5-prefixed checksums MUST be rehashed to SHA-256 |
@@ -76,6 +78,8 @@ Each fixture carries a `clause` field mapping to a numbered requirement:
76
78
  | C013 | PII scan detects SSN | GA-03-REQ-02 | GA-03 |
77
79
  | C014 | PII hit match_count and sensitivity | GA-03-REQ-03 | GA-03 |
78
80
  | C015 | Credit card Luhn validation | GA-03-REQ-04 | GA-03 |
81
+ | C024 | SSN range validation rejects invalid ranges | GA-03-REQ-05 | GA-03 |
82
+ | C025 | date_of_birth rejects calendar-invalid dates | GA-03-REQ-06 | GA-03 |
79
83
  | C016 | Key expiry returns (status, days) tuple | GA-01-REQ-06 | GA-01 |
80
84
  | C017 | derive_key context isolation | GA-01-REQ-07 | GA-01 |
81
85
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spanforge
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: SpanForge — AI lifecycle and governance platform (RFC-0001 SPANFORGE)
5
5
  Project-URL: Homepage, https://github.com/veerarag1973/spanforge
6
6
  Project-URL: Documentation, https://github.com/veerarag1973/spanforge/blob/main/docs/index.md
@@ -350,7 +350,7 @@ assert result.valid # any tampering → False
350
350
 
351
351
  ### PII redaction
352
352
 
353
- Strip personal data before events leave your application boundary. Deep scanning with Luhn validation for credit card numbers.
353
+ Strip personal data before events leave your application boundary. Deep scanning with Luhn and Verhoeff validation for credit cards and Aadhaar numbers, SSN range validation (`_is_valid_ssn`), calendar validation for dates of birth (`_is_valid_date`), and built-in patterns for `date_of_birth` and street `address`.
354
354
 
355
355
  ```python
356
356
  from spanforge.redact import RedactionPolicy, Sensitivity
@@ -605,7 +605,7 @@ spanforge/
605
605
  </tr>
606
606
  <tr>
607
607
  <td><code>spanforge.redact</code></td>
608
- <td>PII detection, sensitivity levels, redaction policies, deep <code>scan_payload()</code> with Luhn validation, and <code>contains_pii()</code> / <code>assert_redacted()</code> with raw string scanning</td>
608
+ <td>PII detection, sensitivity levels, redaction policies, deep <code>scan_payload()</code> with Luhn / Verhoeff / SSN-range / date-calendar validation, built-in <code>date_of_birth</code> and <code>address</code> patterns, and <code>contains_pii()</code> / <code>assert_redacted()</code> with raw string scanning</td>
609
609
  <td>Data privacy / GDPR teams</td>
610
610
  </tr>
611
611
  <tr>
@@ -1430,7 +1430,7 @@ and opens it directly — useful for sharing trace snapshots offline.
1430
1430
  </tr>
1431
1431
  <tr>
1432
1432
  <td><code>spanforge.redact</code></td>
1433
- <td>PII detection, sensitivity levels, redaction policies, deep <code>scan_payload()</code> with Luhn validation, and <code>contains_pii()</code> / <code>assert_redacted()</code> with raw string scanning</td>
1433
+ <td>PII detection, sensitivity levels, redaction policies, deep <code>scan_payload()</code> with Luhn / Verhoeff / SSN-range / date-calendar validation, built-in <code>date_of_birth</code> and <code>address</code> patterns, and <code>contains_pii()</code> / <code>assert_redacted()</code> with raw string scanning</td>
1434
1434
  <td>Data privacy / GDPR teams</td>
1435
1435
  </tr>
1436
1436
  <tr>
@@ -252,7 +252,7 @@ assert result.valid # any tampering → False
252
252
 
253
253
  ### PII redaction
254
254
 
255
- Strip personal data before events leave your application boundary. Deep scanning with Luhn validation for credit card numbers.
255
+ Strip personal data before events leave your application boundary. Deep scanning with Luhn and Verhoeff validation for credit cards and Aadhaar numbers, SSN range validation (`_is_valid_ssn`), calendar validation for dates of birth (`_is_valid_date`), and built-in patterns for `date_of_birth` and street `address`.
256
256
 
257
257
  ```python
258
258
  from spanforge.redact import RedactionPolicy, Sensitivity
@@ -507,7 +507,7 @@ spanforge/
507
507
  </tr>
508
508
  <tr>
509
509
  <td><code>spanforge.redact</code></td>
510
- <td>PII detection, sensitivity levels, redaction policies, deep <code>scan_payload()</code> with Luhn validation, and <code>contains_pii()</code> / <code>assert_redacted()</code> with raw string scanning</td>
510
+ <td>PII detection, sensitivity levels, redaction policies, deep <code>scan_payload()</code> with Luhn / Verhoeff / SSN-range / date-calendar validation, built-in <code>date_of_birth</code> and <code>address</code> patterns, and <code>contains_pii()</code> / <code>assert_redacted()</code> with raw string scanning</td>
511
511
  <td>Data privacy / GDPR teams</td>
512
512
  </tr>
513
513
  <tr>
@@ -1332,7 +1332,7 @@ and opens it directly — useful for sharing trace snapshots offline.
1332
1332
  </tr>
1333
1333
  <tr>
1334
1334
  <td><code>spanforge.redact</code></td>
1335
- <td>PII detection, sensitivity levels, redaction policies, deep <code>scan_payload()</code> with Luhn validation, and <code>contains_pii()</code> / <code>assert_redacted()</code> with raw string scanning</td>
1335
+ <td>PII detection, sensitivity levels, redaction policies, deep <code>scan_payload()</code> with Luhn / Verhoeff / SSN-range / date-calendar validation, built-in <code>date_of_birth</code> and <code>address</code> patterns, and <code>contains_pii()</code> / <code>assert_redacted()</code> with raw string scanning</td>
1336
1336
  <td>Data privacy / GDPR teams</td>
1337
1337
  </tr>
1338
1338
  <tr>
@@ -287,7 +287,7 @@ Scan a payload dictionary for PII using regex detectors.
287
287
  Walks the entire payload recursively (up to `max_depth`), testing every string
288
288
  value against the built-in pattern set plus any caller-supplied patterns.
289
289
 
290
- **Built-in detectors:** `email`, `phone`, `ssn`, `credit_card` (with Luhn validation), `ip_address`, `uk_national_insurance`.
290
+ **Built-in detectors:** `email`, `phone`, `ssn` (with SSA range validation via `_is_valid_ssn`), `credit_card` (with Luhn validation), `ip_address`, `uk_national_insurance`, `date_of_birth` (with calendar validation via `_is_valid_date`), `address`.
291
291
 
292
292
  **Args:**
293
293
 
@@ -6,7 +6,7 @@ this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
- ## 2.1.0 — 2026-06-XX
9
+ ## 2.0.1 — 2026-04-14
10
10
 
11
11
  **Compliance Integration Hardening & CostGuard Enhancements**
12
12
 
@@ -106,6 +106,37 @@ this project adheres to [Semantic Versioning](https://semver.org/).
106
106
  - Both types mapped to sensitivity `"high"` in `_SENSITIVITY_MAP`.
107
107
  - Exported from the top-level `spanforge` package.
108
108
 
109
+ ### Added — Extended PII Pattern Coverage
110
+
111
+ - **`date_of_birth` pattern** — detects dates of birth in `MM/DD/YYYY`,
112
+ `MM-DD-YYYY`, `YYYY-MM-DD`, and `YYYY/MM/DD` formats (centuries 1900–2099).
113
+ Secondary calendar validation via `_is_valid_date()` rejects impossible dates
114
+ (e.g. `02/30/1990`, `13/01/1990`). Mapped to sensitivity `"high"`.
115
+ - **`address` pattern** — detects US street addresses (`<number> <name> <suffix>`)
116
+ with a curated suffix list (Street/St, Avenue/Ave, Road/Rd, Boulevard/Blvd,
117
+ Drive/Dr, Lane/Ln, Court/Ct, Way, Place/Pl, Circle/Cir, Trail/Trl,
118
+ Terrace/Ter, Parkway/Pkwy, Highway/Hwy, Route/Rte). Mapped to sensitivity
119
+ `"medium"`.
120
+ - **`_is_valid_ssn(ssn_str)`** — SSA range validator applied post-regex to every
121
+ SSN match in `scan_payload()`. Rejects area `000`, area `666`, areas
122
+ `900–999` (ITIN-reserved), group `00`, and serial `0000`, eliminating the
123
+ most common false-positive ranges.
124
+ - **`_is_valid_date(date_str)`** — calendar correctness validator applied
125
+ post-regex to every `date_of_birth` match. Delegates to
126
+ `datetime.strptime` for accurate month-length and leap-year enforcement.
127
+ - Both validators follow the same pattern as existing `_luhn_check()` and
128
+ `_verhoeff_check()` — applied inside `scan_payload._walk()` after the regex
129
+ pass.
130
+
131
+ ### Fixed — Compliance Attestation with Missing Signing Key
132
+
133
+ - `generate_evidence_package()`, `to_pdf()`, and `verify_attestation_signature()`
134
+ previously raised `ValueError` when `SPANFORGE_SIGNING_KEY` was not set in
135
+ the environment. They now emit a `logging.WARNING` and fall back to an
136
+ insecure internal default (`_INSECURE_DEFAULT_KEY`). **Production
137
+ deployments must always set `SPANFORGE_SIGNING_KEY`; the default key exists
138
+ only for development and CI environments.**
139
+
109
140
  ### Added — Compliance Dashboard in SPA Viewer
110
141
 
111
142
  - **Clause pass/fail table** — clicking the compliance chip in the
@@ -794,7 +794,7 @@ result = scan_payload(event.payload, extra_patterns=DPDP_PATTERNS)
794
794
  ```
795
795
 
796
796
  Built-in types: `email`, `phone`, `ssn`, `credit_card`, `ip_address`,
797
- `uk_national_insurance`. DPDP add-on types: `aadhaar` (high), `pan` (high).
797
+ `uk_national_insurance`, `date_of_birth`, `address`. DPDP add-on types: `aadhaar` (high), `pan` (high).
798
798
 
799
799
  ---
800
800
 
@@ -163,6 +163,28 @@ for hit in result.hits:
163
163
  # pan: pan (sensitivity=high)
164
164
  ```
165
165
 
166
+ ### Date-of-birth and address detection
167
+
168
+ `scan_payload()` also detects dates of birth and US street addresses out of the
169
+ box — no extra patterns required:
170
+
171
+ ```python
172
+ from spanforge.redact import scan_payload
173
+
174
+ result = scan_payload({
175
+ "dob": "04/15/1990",
176
+ "home": "123 Maple Street",
177
+ })
178
+ for hit in result.hits:
179
+ print(f"{hit.pii_type}: {hit.path} (sensitivity={hit.sensitivity})")
180
+ # date_of_birth: dob (sensitivity=high)
181
+ # address: home (sensitivity=medium)
182
+ ```
183
+
184
+ Calendar-invalid dates (e.g. `02/30/1990`) and SSNs in reserved ranges
185
+ (area `000`, `666`, `900–999`) are automatically filtered out to reduce false
186
+ positives.
187
+
166
188
  ## Exporting events
167
189
 
168
190
  ```python
@@ -7,7 +7,7 @@ build-backend = "hatchling.build"
7
7
  # ---------------------------------------------------------------------------
8
8
  [project]
9
9
  name = "spanforge"
10
- version = "2.0.0"
10
+ version = "2.0.1"
11
11
  description = "SpanForge — AI lifecycle and governance platform (RFC-0001 SPANFORGE)"
12
12
  readme = "README.md"
13
13
  license = { text = "MIT" }
@@ -399,7 +399,7 @@ from spanforge.explain import (
399
399
  )
400
400
  from spanforge.namespaces.consent import ConsentPayload
401
401
  from spanforge.namespaces.hitl import HITLPayload
402
- __version__: str = "2.0.0"
402
+ __version__: str = "2.0.1"
403
403
  #: RFC-0001 SPANFORGE conformance profile label.
404
404
  from typing import Final as _Final
405
405
  CONFORMANCE_PROFILE: _Final[str] = "SPANFORGE-Enterprise-2.0"
@@ -39,6 +39,10 @@ __all__ = [
39
39
 
40
40
  _log = logging.getLogger("spanforge.core.compliance_mapping")
41
41
 
42
+ # Fallback signing key used when SPANFORGE_SIGNING_KEY is absent. Only
43
+ # safe for development / CI — never use in production.
44
+ _INSECURE_DEFAULT_KEY: str = "spanforge-insecure-default-do-not-use-in-production"
45
+
42
46
  # ---------------------------------------------------------------------------
43
47
  # Framework enum
44
48
  # ---------------------------------------------------------------------------
@@ -506,11 +510,12 @@ class ComplianceEvidencePackage:
506
510
  pdf_hash = hashlib.sha256(pdf_bytes).hexdigest()
507
511
  signing_key = os.environ.get("SPANFORGE_SIGNING_KEY", "")
508
512
  if not signing_key or signing_key == "spanforge-default":
509
- raise ValueError(
513
+ _log.warning(
510
514
  "SPANFORGE_SIGNING_KEY is not set or uses the insecure default value. "
511
515
  "Set a strong secret before generating PDF attestations for production. "
512
516
  "Example: export SPANFORGE_SIGNING_KEY=$(openssl rand -hex 32)"
513
517
  )
518
+ signing_key = _INSECURE_DEFAULT_KEY
514
519
  pdf_hmac = _hmac.new(
515
520
  signing_key.encode(),
516
521
  pdf_hash.encode(),
@@ -688,11 +693,12 @@ class ComplianceMappingEngine:
688
693
  )
689
694
  signing_key = os.environ.get("SPANFORGE_SIGNING_KEY", "")
690
695
  if not signing_key or signing_key == "spanforge-default":
691
- raise ValueError(
696
+ _log.warning(
692
697
  "SPANFORGE_SIGNING_KEY is not set or uses the insecure default value. "
693
698
  "Set a strong secret before generating compliance attestations for production. "
694
699
  "Example: export SPANFORGE_SIGNING_KEY=$(openssl rand -hex 32)"
695
700
  )
701
+ signing_key = _INSECURE_DEFAULT_KEY
696
702
  hmac_sig = _hmac.new(
697
703
  signing_key.encode(),
698
704
  sig_payload.encode(),
@@ -1004,11 +1010,12 @@ def verify_attestation_signature(attestation: ComplianceAttestation) -> bool:
1004
1010
  )
1005
1011
  signing_key = os.environ.get("SPANFORGE_SIGNING_KEY", "")
1006
1012
  if not signing_key or signing_key == "spanforge-default":
1007
- raise ValueError(
1013
+ _log.warning(
1008
1014
  "SPANFORGE_SIGNING_KEY is not set or uses the insecure default value. "
1009
1015
  "Attestation verification requires the same key used at signing time. "
1010
1016
  "Example: export SPANFORGE_SIGNING_KEY=<your-secret>"
1011
1017
  )
1018
+ signing_key = _INSECURE_DEFAULT_KEY
1012
1019
  expected = _hmac.new(
1013
1020
  signing_key.encode(),
1014
1021
  sig_payload.encode(),
@@ -586,6 +586,21 @@ _PII_PATTERNS: Final[dict[str, re.Pattern[str]]] = {
586
586
  r"\b[A-CEGHJ-PR-TW-Z]{2}\s?\d{2}\s?\d{2}\s?\d{2}\s?[A-D]\b",
587
587
  re.IGNORECASE,
588
588
  ),
589
+ # Date of birth — MM/DD/YYYY, MM-DD-YYYY, YYYY-MM-DD, YYYY/MM/DD
590
+ # _is_valid_date() provides secondary calendar correctness check.
591
+ "date_of_birth": re.compile(
592
+ r"\b(?:0?[1-9]|1[0-2])[/\-](?:0?[1-9]|[12]\d|3[01])[/\-](?:19|20)\d{2}\b"
593
+ r"|"
594
+ r"\b(?:19|20)\d{2}[/\-](?:0?[1-9]|1[0-2])[/\-](?:0?[1-9]|[12]\d|3[01])\b"
595
+ ),
596
+ # Street address — house number + street name + recognised suffix
597
+ "address": re.compile(
598
+ r"\b\d{1,5}\s+(?:[A-Za-z0-9'.#\-]+\s+){1,5}"
599
+ r"(?:Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Drive|Dr|Lane|Ln|"
600
+ r"Court|Ct|Way|Place|Pl|Circle|Cir|Trail|Trl|Terrace|Ter|"
601
+ r"Parkway|Pkwy|Highway|Hwy|Route|Rte)\.?\b",
602
+ re.IGNORECASE,
603
+ ),
589
604
  }
590
605
 
591
606
 
@@ -663,8 +678,10 @@ _SENSITIVITY_MAP: dict[str, str] = {
663
678
  "credit_card": "high",
664
679
  "aadhaar": "high",
665
680
  "pan": "high",
681
+ "date_of_birth": "high",
666
682
  "email": "medium",
667
683
  "phone": "medium",
684
+ "address": "medium",
668
685
  "ip_address": "low",
669
686
  "uk_national_insurance": "low",
670
687
  }
@@ -703,6 +720,68 @@ def _luhn_check(number_str: str) -> bool:
703
720
  return total % 10 == 0
704
721
 
705
722
 
723
+ def _is_valid_ssn(ssn_str: str) -> bool:
724
+ """Return ``False`` for SSNs in known-invalid SSA number ranges.
725
+
726
+ Filters out the following ranges that the SSA has *never* assigned:
727
+
728
+ * Area ``000`` — never issued.
729
+ * Area ``666`` — explicitly excluded by SSA policy.
730
+ * Areas ``900``–``999`` — reserved for Individual Taxpayer
731
+ Identification Numbers (ITINs); never used as SSNs.
732
+ * Group ``00`` — never issued within any valid area.
733
+ * Serial ``0000`` — never issued within any valid area/group.
734
+
735
+ Args:
736
+ ssn_str: Raw match string from :data:`_PII_PATTERNS` ``"ssn"``
737
+ regex (e.g. ``"123-45-6789"``).
738
+
739
+ Returns:
740
+ ``True`` if the SSN passes all range checks; ``False`` otherwise.
741
+ """
742
+ digits = "".join(c for c in ssn_str if c.isdigit())
743
+ if len(digits) != 9:
744
+ return False
745
+ area = int(digits[:3])
746
+ group = int(digits[3:5])
747
+ serial = int(digits[5:])
748
+ if area == 0 or area == 666 or area >= 900:
749
+ return False
750
+ if group == 0:
751
+ return False
752
+ if serial == 0:
753
+ return False
754
+ return True
755
+
756
+
757
+ def _is_valid_date(date_str: str) -> bool:
758
+ """Return ``True`` if *date_str* is a valid calendar date.
759
+
760
+ Accepts the four formats produced by the ``"date_of_birth"`` regex in
761
+ :data:`_PII_PATTERNS`:
762
+
763
+ * ``MM/DD/YYYY`` and ``MM-DD-YYYY``
764
+ * ``YYYY/MM/DD`` and ``YYYY-MM-DD``
765
+
766
+ Delegates to :func:`datetime.datetime.strptime` so leap-year rules and
767
+ month-length limits are enforced (e.g. ``02/30/2000`` is rejected).
768
+
769
+ Args:
770
+ date_str: Raw match string from the ``"date_of_birth"`` regex.
771
+
772
+ Returns:
773
+ ``True`` if the string represents a real calendar date; ``False``
774
+ if no known format matches or the date is not calendar-valid.
775
+ """
776
+ for fmt in ("%m/%d/%Y", "%m-%d-%Y", "%Y/%m/%d", "%Y-%m-%d"):
777
+ try:
778
+ datetime.datetime.strptime(date_str.strip(), fmt)
779
+ return True
780
+ except ValueError:
781
+ continue
782
+ return False
783
+
784
+
706
785
  def scan_payload(
707
786
  payload: dict[str, Any],
708
787
  *,
@@ -762,6 +841,24 @@ def scan_payload(
762
841
  if not valid_matches:
763
842
  continue
764
843
  matches = valid_matches
844
+ # SSN range validation — drop known-invalid SSA ranges
845
+ if label == "ssn":
846
+ valid_matches = [
847
+ m for m in matches
848
+ if _is_valid_ssn(m.group())
849
+ ]
850
+ if not valid_matches:
851
+ continue
852
+ matches = valid_matches
853
+ # Calendar validation for date_of_birth patterns
854
+ if label == "date_of_birth":
855
+ valid_matches = [
856
+ m for m in matches
857
+ if _is_valid_date(m.group())
858
+ ]
859
+ if not valid_matches:
860
+ continue
861
+ matches = valid_matches
765
862
  sensitivity = _SENSITIVITY_MAP.get(label, "medium")
766
863
  hits.append(PIIScanHit(
767
864
  pii_type=label,
@@ -22,6 +22,8 @@ from spanforge.redact import (
22
22
  Sensitivity,
23
23
  _count_redactable,
24
24
  _has_redactable,
25
+ _is_valid_date,
26
+ _is_valid_ssn,
25
27
  _utcnow_iso,
26
28
  _verhoeff_check,
27
29
  assert_redacted,
@@ -819,3 +821,201 @@ class TestDDPDPatternsWithBuiltins:
819
821
  types = {h.pii_type for h in result.hits}
820
822
  assert "ssn" in types
821
823
  assert "pan" in types
824
+
825
+
826
+ # ===========================================================================
827
+ # SSN range validation — _is_valid_ssn
828
+ # ===========================================================================
829
+
830
+
831
+ @pytest.mark.unit
832
+ class TestIsValidSSN:
833
+ """Unit tests for the _is_valid_ssn range-validation helper."""
834
+
835
+ def test_valid_ssn_passes(self) -> None:
836
+ assert _is_valid_ssn("123-45-6789") is True
837
+
838
+ def test_valid_ssn_no_separators(self) -> None:
839
+ assert _is_valid_ssn("123456789") is True
840
+
841
+ def test_area_000_rejected(self) -> None:
842
+ assert _is_valid_ssn("000-12-3456") is False
843
+
844
+ def test_area_666_rejected(self) -> None:
845
+ assert _is_valid_ssn("666-12-3456") is False
846
+
847
+ def test_area_900_rejected(self) -> None:
848
+ assert _is_valid_ssn("900-12-3456") is False
849
+
850
+ def test_area_999_rejected(self) -> None:
851
+ assert _is_valid_ssn("999-12-3456") is False
852
+
853
+ def test_group_00_rejected(self) -> None:
854
+ assert _is_valid_ssn("123-00-3456") is False
855
+
856
+ def test_serial_0000_rejected(self) -> None:
857
+ assert _is_valid_ssn("123-45-0000") is False
858
+
859
+ def test_too_short_rejected(self) -> None:
860
+ assert _is_valid_ssn("123-45-678") is False
861
+
862
+ def test_non_digit_only_rejected(self) -> None:
863
+ assert _is_valid_ssn("abc-de-fghi") is False
864
+
865
+
866
+ @pytest.mark.unit
867
+ class TestSSNRangeValidationInScanPayload:
868
+ """scan_payload must use _is_valid_ssn to drop false-positive SSNs."""
869
+
870
+ def test_valid_ssn_detected(self) -> None:
871
+ result = scan_payload({"id": "123-45-6789"})
872
+ assert any(h.pii_type == "ssn" for h in result.hits)
873
+
874
+ def test_area_000_not_flagged(self) -> None:
875
+ result = scan_payload({"id": "000-12-3456"})
876
+ assert not any(h.pii_type == "ssn" for h in result.hits)
877
+
878
+ def test_area_666_not_flagged(self) -> None:
879
+ result = scan_payload({"id": "666-12-3456"})
880
+ assert not any(h.pii_type == "ssn" for h in result.hits)
881
+
882
+ def test_area_900_not_flagged(self) -> None:
883
+ result = scan_payload({"id": "900-12-3456"})
884
+ assert not any(h.pii_type == "ssn" for h in result.hits)
885
+
886
+ def test_group_00_not_flagged(self) -> None:
887
+ result = scan_payload({"id": "123-00-3456"})
888
+ assert not any(h.pii_type == "ssn" for h in result.hits)
889
+
890
+ def test_serial_0000_not_flagged(self) -> None:
891
+ result = scan_payload({"id": "123-45-0000"})
892
+ assert not any(h.pii_type == "ssn" for h in result.hits)
893
+
894
+ def test_ssn_sensitivity_is_high(self) -> None:
895
+ result = scan_payload({"id": "123-45-6789"})
896
+ hit = next(h for h in result.hits if h.pii_type == "ssn")
897
+ assert hit.sensitivity == "high"
898
+
899
+
900
+ # ===========================================================================
901
+ # Date-of-birth calendar validation — _is_valid_date
902
+ # ===========================================================================
903
+
904
+
905
+ @pytest.mark.unit
906
+ class TestIsValidDate:
907
+ """Unit tests for the _is_valid_date calendar-validation helper."""
908
+
909
+ def test_mm_slash_dd_slash_yyyy(self) -> None:
910
+ assert _is_valid_date("01/15/1990") is True
911
+
912
+ def test_mm_dash_dd_dash_yyyy(self) -> None:
913
+ assert _is_valid_date("01-15-1990") is True
914
+
915
+ def test_yyyy_slash_mm_slash_dd(self) -> None:
916
+ assert _is_valid_date("1990/01/15") is True
917
+
918
+ def test_yyyy_dash_mm_dash_dd(self) -> None:
919
+ assert _is_valid_date("1990-01-15") is True
920
+
921
+ def test_leap_day_valid(self) -> None:
922
+ assert _is_valid_date("02/29/2000") is True
923
+
924
+ def test_feb_30_rejected(self) -> None:
925
+ assert _is_valid_date("02/30/2000") is False
926
+
927
+ def test_month_13_rejected(self) -> None:
928
+ assert _is_valid_date("13/01/1990") is False
929
+
930
+ def test_day_00_rejected(self) -> None:
931
+ assert _is_valid_date("01/00/1990") is False
932
+
933
+ def test_non_leap_feb29_rejected(self) -> None:
934
+ assert _is_valid_date("02/29/2001") is False
935
+
936
+ def test_unrecognised_format_rejected(self) -> None:
937
+ assert _is_valid_date("15-Jan-1990") is False
938
+
939
+ def test_empty_string_rejected(self) -> None:
940
+ assert _is_valid_date("") is False
941
+
942
+
943
+ @pytest.mark.unit
944
+ class TestDateOfBirthDetectionInScanPayload:
945
+ """scan_payload must detect date_of_birth patterns and reject invalid dates."""
946
+
947
+ def test_detects_us_slash_format(self) -> None:
948
+ result = scan_payload({"dob": "01/15/1990"})
949
+ assert any(h.pii_type == "date_of_birth" for h in result.hits)
950
+
951
+ def test_detects_iso_format(self) -> None:
952
+ result = scan_payload({"dob": "1990-01-15"})
953
+ assert any(h.pii_type == "date_of_birth" for h in result.hits)
954
+
955
+ def test_detects_dashed_us_format(self) -> None:
956
+ result = scan_payload({"dob": "03-22-1985"})
957
+ assert any(h.pii_type == "date_of_birth" for h in result.hits)
958
+
959
+ def test_rejects_invalid_feb30(self) -> None:
960
+ result = scan_payload({"dob": "02/30/1990"})
961
+ assert not any(h.pii_type == "date_of_birth" for h in result.hits)
962
+
963
+ def test_rejects_month_13(self) -> None:
964
+ result = scan_payload({"dob": "13/01/1990"})
965
+ assert not any(h.pii_type == "date_of_birth" for h in result.hits)
966
+
967
+ def test_sensitivity_is_high(self) -> None:
968
+ result = scan_payload({"dob": "06/15/1985"})
969
+ hit = next(h for h in result.hits if h.pii_type == "date_of_birth")
970
+ assert hit.sensitivity == "high"
971
+
972
+ def test_detects_dob_in_nested_dict(self) -> None:
973
+ result = scan_payload({"patient": {"demographics": {"dob": "1975-08-20"}}})
974
+ assert any(h.pii_type == "date_of_birth" for h in result.hits)
975
+ hit = next(h for h in result.hits if h.pii_type == "date_of_birth")
976
+ assert "patient.demographics.dob" in hit.path
977
+
978
+
979
+ # ===========================================================================
980
+ # Address pattern detection
981
+ # ===========================================================================
982
+
983
+
984
+ @pytest.mark.unit
985
+ class TestAddressDetectionInScanPayload:
986
+ """scan_payload must detect street address patterns."""
987
+
988
+ def test_detects_street(self) -> None:
989
+ result = scan_payload({"addr": "123 Main Street"})
990
+ assert any(h.pii_type == "address" for h in result.hits)
991
+
992
+ def test_detects_avenue(self) -> None:
993
+ result = scan_payload({"addr": "456 Oak Avenue"})
994
+ assert any(h.pii_type == "address" for h in result.hits)
995
+
996
+ def test_detects_abbreviated_suffix(self) -> None:
997
+ result = scan_payload({"addr": "789 Elm Dr"})
998
+ assert any(h.pii_type == "address" for h in result.hits)
999
+
1000
+ def test_detects_blvd(self) -> None:
1001
+ result = scan_payload({"addr": "1600 Pennsylvania Ave"})
1002
+ assert any(h.pii_type == "address" for h in result.hits)
1003
+
1004
+ def test_detects_lane(self) -> None:
1005
+ result = scan_payload({"addr": "42 Sunrise Lane"})
1006
+ assert any(h.pii_type == "address" for h in result.hits)
1007
+
1008
+ def test_no_false_positive_on_plain_text(self) -> None:
1009
+ result = scan_payload({"note": "This is a completely normal sentence."})
1010
+ assert not any(h.pii_type == "address" for h in result.hits)
1011
+
1012
+ def test_sensitivity_is_medium(self) -> None:
1013
+ result = scan_payload({"addr": "100 Maple Road"})
1014
+ hit = next(h for h in result.hits if h.pii_type == "address")
1015
+ assert hit.sensitivity == "medium"
1016
+
1017
+ def test_detects_address_in_list(self) -> None:
1018
+ result = scan_payload({"addresses": ["100 Maple Road", "clean"]})
1019
+ assert any(h.pii_type == "address" for h in result.hits)
1020
+ hit = next(h for h in result.hits if h.pii_type == "address")
1021
+ assert "addresses[0]" in hit.path
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes