Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ jobs:
run: python tools/check_spdx.py
- name: Dependency licences
run: python tools/check_licenses.py --report licenses.md && cat licenses.md >> "$GITHUB_STEP_SUMMARY"
- name: Claim register (every documented claim traces to a proof, test or report)
shell: bash # -o pipefail: the step fails when the check does
run: python tools/claims_check.py --json claims-resolved.json | tee -a "$GITHUB_STEP_SUMMARY"
- name: One version everywhere (package, README, docs, examples, images, Helm chart)
run: python tools/release.py check

Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable user-visible changes are recorded here, following [Keep a Changelog

## Unreleased

### Added

- **Compliance evidence and assurance case** (`docs/assurance/`; roadmap T17): an assurance case for a release (claims, evidence, assumptions, defeaters), a mapping to EDPB Guidelines 02/2026 and *EDPS v SRB*, the AI Act (Art. 10 and the new Art. 4a), HIPAA expert determination, NIST SP 800-226 and SP 800-188 and ISO/IEC 27559 and 20889, a DPIA template for a synthetic release, and release guidance for internal, partner and public recipients.
- **Claim register** (`docs/assurance/claims.yaml`, `tools/claims_check.py`, CI job `lint`): every claim of the README and of the privacy, DP, security and governance guides (96 claims) traced to a proposition, check, test or measured report; a README sentence that sums up a guide cites that guide's claim. CI fails when a claim changes, its evidence disappears, or a new claim is not registered.

### Changed

- README: "No row is copied" now reads "Rows are drawn from a fitted model; only tables you declare `mode: copy` are copied"; "Privacy checks run by default" now says the copy guards run by default and are not a privacy check; the 2.0 science checks are C600–C606 (privacy attacks), not C700–C706, which do not exist (the generator propositions are tested in `tests/unit/test_generators.py`).
- `docs/security.md` now describes 2.0: differential privacy is optional rather than absent, the distance guards are copy guards rather than privacy checks, seven core dependencies, four `subprocess` call sites, and the service's connections to the OIDC provider and Vault.

## 2.0.0 (2026-10-31)
Release 2.0 is the first release of the enterprise roadmap: IR 2.0, generator plug-ins, MySQL/MariaDB, cloud warehouses, Iceberg and Delta Lake, privacy attacks, differential privacy, governance, time series, partitioned generation, engine settings with autotune, and the service platform. **Draws for the same seed change** on databases with link tables (see Changed). The proofs PDF is rebuilt with all 2.0 propositions; the partitioned-runtime propositions are renumbered 950–954 (they clashed with DP's 900–904).

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ relsynth run spec.yaml --target "sqlite:synthetic.db" --fail-under 0.98
- **Statistics.** Each table's marginals and dependence come from a Bayesian Gaussian copula over mixed types. Links between tables come from latent segments, so customers who buy more of one product kind keep doing so. Counts, participation and multiplicities follow fitted laws.
- **Bayesian.** Each synthetic dataset is one draw from the posterior predictive distribution. Several datasets show the model's own uncertainty, and `posterior.mode: mean` gives stable test data.
- **Scale.** You set the size per table: `--scale 0.1` for a small test database, or `--scale 10` for load tests. Facts follow the tables they hang from by the fitted rates.
- **Privacy.** No row is copied. Distance-to-closest-record checks compare synthetic rows with real ones, against the real data's own spacing. Text cannot reproduce rare source content, and memorisation guards are on by default. `relsynth privacy` attacks a release per person (membership, attribute inference, singling out, linkability) and writes a report per release context.
- **Privacy.** Rows are drawn from a fitted model; only tables you declare `mode: copy` are copied. Distance-to-closest-record checks compare synthetic rows with real ones, against the real data's own spacing. Text cannot reproduce rare source content, and memorisation guards are on by default. `relsynth privacy` attacks a release per person (membership, attribute inference, singling out, linkability) and writes a report per release context.
- **Differential privacy (optional).** `relsynth run --epsilon 1` gives a user-level (ε, δ) guarantee for each person of the privacy unit and all their rows. The organisation sets ε; there is no default. Releases outside the organisation are refused without it.
- **Governance.** Signed, hash-chained manifests of every run, a privacy budget ledger per domain, an organisation policy and human approvals before release (`relsynth gov`).
- **Time.** Event sequences (a customer's orders) keep their gaps, life times and calendar; meter-style series keep their seasonal profiles and autocorrelation; `relsynth extend` adds new days to a synthetic dataset.
Expand Down Expand Up @@ -358,7 +358,7 @@ The relational part of the model is unchanged: Proposition 100 proves it, and a
- **SQL.** Every SQL statement is built with SQLAlchemy Core, with quoted identifiers and bound values. Table and column names cannot inject SQL; tests use hostile names on every database.
- **Specs.** A spec is YAML read with a safe loader that also refuses aliases, so an alias bomb fails. Rule expressions run in a sandboxed evaluator: no attribute access, calls only to a fixed list of functions, and size and depth limits. Regular expressions with catastrophic backtracking are refused.
- **Files.** Table names cannot leave the target folder (`..`, separators, absolute paths and symlinks are refused).
- **Data.** Rows never leave the source database for pushed-down facts. Bundles and profiles hold aggregates only, and `profile --top` must be asked for explicitly. Privacy checks run by default.
- **Data.** Rows never leave the source database for pushed-down facts. Bundles and profiles hold aggregates only, and `profile --top` must be asked for explicitly. Copy guards run by default; they are not a privacy check ([docs/privacy.md](docs/privacy.md) measures privacy).
- **Supply chain.** The code is scanned with bandit and ruff's security rules, and dependencies with pip-audit, in CI.

Details and the threat model: [docs/security.md](docs/security.md).
Expand All @@ -373,7 +373,7 @@ Tuning knobs and measurements: [docs/pushdown.md](docs/pushdown.md#performance).

## Validation

**Release 2.0.** The statistical check suite now has C00–C82 plus the 2.0 checks C700–C706 (generators), C800–C803 (governance), C900–C906 (differential privacy) and C1200–C1206 (time series): 113 science tests, all run in CI on every pull request in four shards, next to 775 unit, end-to-end, security and live-server integration tests. A benchmark harness (`bench/`) reruns the real databases (bi/COREP, Chinook, AdventureWorks DW, Contoso, plus Berka, ACS PUMS and a doubled Chinook) on every change and flags drift against a five-seed baseline. Draws for the same seed change on databases with link tables, because degree laws are now the default (`synthesis.facts.link_degree: segment` restores the 1.0 draws); the [changelog](CHANGELOG.md) lists every change.
**Release 2.0.** The statistical check suite now has C00–C82 plus the 2.0 checks C600–C606 (privacy attacks), C800–C803 (governance), C900–C906 (differential privacy) and C1200–C1206 (time series), with the generator propositions 700–706 tested in tests/unit/test_generators.py: 113 science tests, all run in CI on every pull request in four shards, next to about 800 unit, end-to-end, security and live-server integration tests. A benchmark harness (`bench/`) reruns the real databases (bi/COREP, Chinook, AdventureWorks DW, Contoso, plus Berka, ACS PUMS and a doubled Chinook) on every change and flags drift against a five-seed baseline. Draws for the same seed change on databases with link tables, because degree laws are now the default (`synthesis.facts.link_degree: segment` restores the 1.0 draws); the [changelog](CHANGELOG.md) lists every change.

relsynth 1.0.0 was checked at three levels. Every number below comes from the final code.

Expand Down Expand Up @@ -480,6 +480,7 @@ What each test module covers is in [docs/testing.md](docs/testing.md).
| [docs/datasets.md](docs/datasets.md) | Which datasets may be used, committed and where |
| [CONTRIBUTING.md](CONTRIBUTING.md) | How to contribute: workflow, commits, sign-off, tests |
| [SECURITY.md](SECURITY.md) | Reporting a vulnerability |
| [docs/assurance/](docs/assurance/README.md) | For privacy, legal and audit teams: the assurance case, the regulation mapping (GDPR and EDPB 02/2026, AI Act, HIPAA, NIST, ISO), a DPIA template, release guidance and the claim register |

## Licence

Expand Down
38 changes: 38 additions & 0 deletions docs/assurance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Compliance evidence and assurance

This folder is for the privacy, legal and audit teams who decide whether a synthetic dataset made with relsynth may be released. It does not decide for them and is not legal advice; it gives them the argument, the evidence and the templates.

| Document | Use it to |
|---|---|
| [assurance-case.md](assurance-case.md) | Follow the argument that a release is fit for its recipient: claims, the evidence for each, assumptions and known defeaters |
| [regulation-mapping.md](regulation-mapping.md) | See what relsynth contributes to each provision of the GDPR (EDPB Guidelines 02/2026, *EDPS v SRB*), the AI Act (Art. 10, Art. 4a), HIPAA expert determination, NIST SP 800-226 and SP 800-188, ISO/IEC 27559 and 20889, and what stays with the organisation |
| [dpia-template.md](dpia-template.md) | Document one release: a DPIA following GDPR Art. 35(7), pre-filled with what relsynth does |
| [release-guidance.md](release-guidance.md) | Decide how to make and approve a release for an internal, partner or public recipient |
| [claims.yaml](claims.yaml) | Look up the evidence behind a sentence of the documentation |
| [evidence/](evidence/README.md) | The aggregate measurement files that claims cite |

## The claim register

Every sentence of the documents listed under `scope` in [claims.yaml](claims.yaml) that reads as a claim (it says *never*, *every*, *exact*, *guarantee*, *refused* and similar) has an entry. Each entry quotes the sentence and lists its evidence: a proposition of the [proofs](../proofs/relsynth-proofs.tex) by label, a numbered check (C902), a pytest test, a measured report in this repository, the code that enforces it, or, for a sentence that sums up another registered claim (the README does this), that claim's id. A `status` says whether the evidence supports the claim fully, partly (with a note), or shows a limitation.

`python tools/claims_check.py` checks the register, and CI runs it on every pull request. It fails when:

- a quoted sentence is no longer in its document;
- a cited proposition, check, test or file does not exist;
- a claim about relsynth rests only on an external source or on code, without a proposition, check, test or report;
- a claim-like sentence of a document in scope has no entry, and is not listed under `not_claims` with a reason;
- a `Prop. N` in any Markdown document names a number the proofs do not define;
- these documents cite a claim id, a proposition label or a check that does not exist.

When your change makes it fail:

| Message | Fix |
|---|---|
| `quote not found` | You changed a registered sentence. Update `quote`, and check that the evidence still supports the new wording |
| `unregistered claim` | You wrote a new claim. Add an entry with its evidence (`python tools/claims_check.py --suggest docs/x.md` prints stubs), or, if it is not a claim about relsynth, add it to `not_claims` with a reason |
| `no test ...`, `no proposition ...` | You renamed or removed evidence. Point the entry at the new name |
| `needs a prop, check, test or report` | Write the test that shows the claim, or weaken the claim |

`python tools/claims_check.py --json claims.json` writes the register with every proposition label resolved to its printed number, for an auditor's tooling.

Documents in scope today: docs/privacy.md, docs/dp.md, docs/security.md and docs/governance/guide.md. The README and the remaining guides are added one by one; until then their `Prop. N` references are still checked.
Loading
Loading