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.
- {spanforge-2.0.0 → spanforge-2.0.1}/CONFORMANCE.md +4 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/PKG-INFO +4 -4
- {spanforge-2.0.0 → spanforge-2.0.1}/README.md +3 -3
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/redact.md +1 -1
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/changelog.md +32 -1
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/cli.md +1 -1
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/quickstart.md +22 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/pyproject.toml +1 -1
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/__init__.py +1 -1
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/core/compliance_mapping.py +10 -3
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/redact.py +97 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_redact.py +200 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/.gitattributes +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/.github/CODEOWNERS +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/.github/ISSUE_TEMPLATE/rfc.yml +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/.github/pull_request_template.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/.github/workflows/ci.yml +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/.github/workflows/release.yml +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/.gitignore +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/CNAME +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/CODE_OF_CONDUCT.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/LICENSE +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/MAINTAINERS.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/PRICING.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/README.md.bak +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/RELEASE.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/SECURITY.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/Makefile +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/_static/.gitkeep +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/auto.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/cache.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/compliance.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/consumer.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/debug.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/deprecations.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/event.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/exceptions.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/export.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/governance.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/hooks.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/index.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/integrations.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/lint.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/metrics.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/migrate.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/models.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/normalizer.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/signing.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/store.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/stream.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/testing.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/trace.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/types.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/ulid.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/api/validate.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/conf.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/configuration.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/contributing.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/deployment/air-gapped.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/deployment/kubernetes.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/index.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/installation.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/integrations/crewai.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/make.bat +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/migrations/from-langfuse.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/migrations/from-langsmith.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/migrations/from-openllmetry.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/audit.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/cache.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/consent.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/cost.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/diff.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/eval.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/explanation.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/fence.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/guard.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/hitl.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/index.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/model_registry.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/prompt.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/redact_ns.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/template.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/namespaces/trace.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/rfc/rfc-0001.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/runbook.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/README.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/envelope.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/agent-run.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/agent-step.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/audit.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/cache.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/consent.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/cost.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/diff.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/eval.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/explanation.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/fence.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/guard.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/hitl.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/model-registry.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/prompt.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/redact.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/span.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/payloads/template.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema/types/common.schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/schema-versioning.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/cache.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/compliance.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/custom_exporters.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/debugging.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/events.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/export.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/governance.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/index.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/linting.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/metrics.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/migration.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/redaction.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/signing.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/docs/user_guide/tracing.md +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/agent_workflow.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/budget_alert.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/docker/Dockerfile +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/docker/docker-compose.yml +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/docker/otel-config.yaml +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/langchain_chain.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/multi_agent_rag.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/multi_tenant.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/openai_chat.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/otlp_grafana.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/production_multi_agent.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/secure_pipeline.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/examples/streaming_response.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/sonar-project.properties +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_batch_exporter.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_cli.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_hooks.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_server.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_span.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_store.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_stream.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_trace.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/_tracer.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/actor.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/alerts.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/auto.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/baseline.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/config.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/consent.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/consumer.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/core/__init__.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/cost.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/debug.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/drift.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/egress.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/eval.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/event.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/exceptions.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/explain.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/__init__.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/append_only.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/cloud.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/datadog.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/grafana.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/jsonl.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/otel_bridge.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/otlp.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/otlp_bridge.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/redis_backend.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/export/webhook.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/exporters/__init__.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/exporters/console.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/exporters/jsonl.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/hitl.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/inspect.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/__init__.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/_pricing.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/anthropic.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/bedrock.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/crewai.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/gemini.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/groq.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/langchain.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/llamaindex.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/ollama.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/openai.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/integrations/together.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/metrics.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/metrics_export.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/migrate.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/model_registry.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/models.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/__init__.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/audit.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/cache.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/chain.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/confidence.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/consent.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/cost.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/decision.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/diff.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/drift.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/eval_.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/fence.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/guard.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/hitl.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/latency.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/prompt.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/redact.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/template.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/tool_call.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/namespaces/trace.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/normalizer.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/presidio_backend.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/processor.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/prompt_registry.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/py.typed +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/sampling.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/schemas/v1.0/schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/schemas/v2.0/schema.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/signing.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/stream.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/testing.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/trace.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/types.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/ulid.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/src/spanforge/validate.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/test_agent.jsonl +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/test_events.jsonl +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/__init__.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/__init__.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures/chain.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures/compliance.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures/key_security.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures/migration.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures/pii.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures/signing.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/fixtures.json +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/run_conformance.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/conformance/test_conformance.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/conftest.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_actor.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_alerts.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_auto.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_baseline.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_benchmarks.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_budget_alert.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_cli.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_compliance_mapping.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_consent.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_consumer.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_cost_event_emission.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_cost_tracker.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_costguard_gaps.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_drift.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_event.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_exceptions.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_explain.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_cloud.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_datadog.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_grafana.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_jsonl.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_otel_bridge.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_otlp.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_redis_backend.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_export_webhook.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_hitl.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_inspect.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_integration.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_integrations.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_migrate.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_model_registry.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_models.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_namespaces.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_otlp_bridge.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase10_features.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase11_security.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase1_context_trace.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase2_observability.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase3_debug_sampling.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase4_agent_instrumentation.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase4_metrics_store.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase5_console_exporter.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase5_coverage.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase5_hooks_crewai.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_phase6_openai_integration.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_processor_coverage.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_properties.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_rfc_namespaces.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sampling_coverage.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_config.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_coverage_boost.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_exporters.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_final_coverage.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_gap_filler.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_openai_integration.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_phase7_integrations.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_precision_coverage.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_span.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_stream.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_tracer.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sdk_validation_coverage.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_server.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sf11.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sf12.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sf13.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sf14.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sf15.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_sf16.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_signing.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_stream.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_trace_decorator.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_trace_pytest_fixtures.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_types.py +0 -0
- {spanforge-2.0.0 → spanforge-2.0.1}/tests/test_ulid.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|