falsify 0.5.0__tar.gz → 0.5.2__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.
- {falsify-0.5.0/falsify.egg-info → falsify-0.5.2}/PKG-INFO +9 -7
- {falsify-0.5.0 → falsify-0.5.2}/README.md +8 -6
- {falsify-0.5.0 → falsify-0.5.2/falsify.egg-info}/PKG-INFO +9 -7
- {falsify-0.5.0 → falsify-0.5.2}/falsify.egg-info/SOURCES.txt +3 -0
- {falsify-0.5.0 → falsify-0.5.2}/falsify.egg-info/top_level.txt +1 -0
- {falsify-0.5.0 → falsify-0.5.2}/falsify.py +1 -1
- falsify-0.5.2/falsify_linkage.py +256 -0
- {falsify-0.5.0 → falsify-0.5.2}/falsify_prml.py +98 -36
- {falsify-0.5.0 → falsify-0.5.2}/pyproject.toml +2 -2
- falsify-0.5.2/tests/test_packaging.py +102 -0
- falsify-0.5.2/tests/test_prml_json.py +81 -0
- {falsify-0.5.0 → falsify-0.5.2}/LICENSE +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/NOTICE +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/falsify.egg-info/dependency_links.txt +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/falsify.egg-info/entry_points.txt +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/falsify.egg-info/requires.txt +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/falsify_receipt.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/mcp_server/__init__.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/mcp_server/__main__.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/mcp_server/server.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/setup.cfg +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_adversarial_doc.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_agent_claim_auditor.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_agent_verdict_refresher.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_architecture.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_attest.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_attest_test_result.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_bench.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_calibration_sample.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_case_studies_doc.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_changelog.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_ci_workflow.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_claude_md.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_code_of_conduct.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_comparison_doc.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_conform_cmd.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_contributing.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_demo_script.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_demo_script_doc.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_demo_shot_list.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_diff.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_docker.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_doctor.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_editorconfig.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_embed_example.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_examples_doc.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_export.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_faq.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_github_repo_maturity.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_github_templates.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_gitignore.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_glossary_doc.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_grammar.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_guard.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_hook_install.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_init.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_init_templates.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_integration_e2e.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_linkage.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_linkage_parity.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_list.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_lock.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_makefile.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_managed_agents.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_mcp.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_mcp_server.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_pitch.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_pre_commit.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_prml_cli.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_prml_init.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_prml_v02_candidates.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_prml_vectors.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_prml_verify_observed.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_pyproject.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_readme.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_receipt.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_reject_vectors.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_release_check.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_release_workflow.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_replay.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_roadmap.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_run.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_score.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_self_dogfood.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_skill_author.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_skill_ci_doctor.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_skill_claim_audit.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_skill_claim_review.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_skill_falsify.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_slash_commands.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_stats.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_stats_html.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_submission.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_submission_md.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_trend.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_tutorial.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_verdict.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_verify.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_version.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_why.py +0 -0
- {falsify-0.5.0 → falsify-0.5.2}/tests/test_yaml_parse_parity.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: falsify
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: PRML reference CLI (`falsify`) — pre-register an ML evaluation claim as a SHA-256 manifest; verify PASS/FAIL/TAMPERED. Also ships `falsify-engine`, a separate pre-registration workflow CLI with its own schema.
|
|
5
5
|
Author: Cüneyt Öztürk
|
|
6
6
|
License: MIT
|
|
@@ -37,7 +37,7 @@ Dynamic: license-file
|
|
|
37
37
|
|
|
38
38
|
[](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
|
|
39
39
|
[](https://github.com/studio-11-co/falsify/actions/workflows/multi-lang-conformance.yml)
|
|
40
|
-
[](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
|
|
41
41
|
[](spec/test-vectors/)
|
|
42
42
|
[](https://www.bestpractices.dev/projects/13539)
|
|
43
43
|
[](claims/self/)
|
|
@@ -82,6 +82,8 @@ TAMPERED
|
|
|
82
82
|
# exit 3
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
**For CI steps and coding agents.** Add `--json` to `verify`, `lock` or `hash` for one JSON object on stdout with stable keys (`status`, `exit_code`, `sha256`, `recorded`, `observed`, ...); exit codes do not change. `status` tells apart the two exit-11 cases (`NO_LOCK`, `DATASET_MISMATCH`), and errors come back as `"status": "ERROR"`.
|
|
86
|
+
|
|
85
87
|
**Timestamped receipt from the terminal.** `falsify anchor` posts the manifest to the public registry and saves the receipt next to it; `falsify receipt verify` checks it offline, without trusting the registry and without OpenSSL: the RFC 3161 token is checked against Sigstore's timestamp authority root, and the Rekor v2 entry by its inclusion proof and signed checkpoint.
|
|
86
88
|
|
|
87
89
|
```bash
|
|
@@ -123,7 +125,7 @@ No install? Verify any manifest in-browser at [registry.falsify.dev](https://reg
|
|
|
123
125
|
| Your own platform | [`docs/EMBED.md`](docs/EMBED.md): three pure functions and a five-line hook |
|
|
124
126
|
| A real AI delivery | [Acceptance Evidence Pack](https://falsify.dev/pricing/#pilot), contracted through Falsify OÜ (reg. 17574308, Tallinn, Estonia) |
|
|
125
127
|
|
|
126
|
-
**Current version:** falsify 0.5.
|
|
128
|
+
**Current version:** falsify 0.5.2 (PRML CLI) · falsify-engine 0.5.2 — `falsify --version`.
|
|
127
129
|
|
|
128
130
|
PRML v0.2 frozen 2026-05-22. Four reference implementations (Python, JavaScript, Go, Rust), byte-equivalent on all 21 conformance vectors (13 v0.1 + 8 v0.2), 20 reject vectors and the 92-vector edge suite derived from the §3.6 formal grammar.
|
|
129
131
|
|
|
@@ -339,7 +341,7 @@ Consume falsify's hooks from your own repo:
|
|
|
339
341
|
```yaml
|
|
340
342
|
repos:
|
|
341
343
|
- repo: https://github.com/studio-11-co/falsify
|
|
342
|
-
rev: v0.5.
|
|
344
|
+
rev: v0.5.2
|
|
343
345
|
hooks:
|
|
344
346
|
- id: falsify-guard
|
|
345
347
|
- id: falsify-doctor
|
|
@@ -611,17 +613,17 @@ Run `make dogfood` to re-verify. CI runs these on every PR.
|
|
|
611
613
|
|
|
612
614
|
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
613
615
|
|
|
614
|
-
> **Latest — 2026-10-
|
|
616
|
+
> **Latest — 2026-10-31** · **falsify 0.5.2** on PyPI: the wheel now ships `falsify_linkage.py` and `falsify linkage start|finish|verify` works from the `falsify` command (0.5.1 left the module out); 0.5.1 added `--json` on `verify`, `lock` and `hash`; 0.5.0 added `falsify anchor` and `falsify receipt verify` (RFC 3161 + Rekor, offline), `init` writes a lockable manifest, `attest --as test-result`; v0.3.13 (2026-08-23) fixed three specification defects and expanded the negative-conformance suite to 20 reject vectors. The conformance suite stays at 21 positive + 20 reject vectors across the four reference implementations. Registry receipts carry an RFC 3161 countersignature and a Rekor v2 mirror.
|
|
615
617
|
|
|
616
618
|
<a id="roadmap"></a>
|
|
617
619
|
### Roadmap
|
|
618
620
|
|
|
619
621
|
Two roadmaps run alongside each other:
|
|
620
622
|
|
|
621
|
-
- **CLI tool roadmap:** [ROADMAP.md](ROADMAP.md) — `falsify` features, integrations, dependencies. **Current CLI: falsify 0.5.
|
|
623
|
+
- **CLI tool roadmap:** [ROADMAP.md](ROADMAP.md) — `falsify` features, integrations, dependencies. **Current CLI: falsify 0.5.2 on PyPI.**
|
|
622
624
|
- **Specification roadmap:** [spec/v0.2/ROADMAP.md](spec/v0.2/ROADMAP.md) — PRML format evolution, canonicalization grammar, conformance. **Spec v0.2 frozen 2026-05-22.** v0.3 design backlog open under [`spec/v0.3-backlog/`](spec/v0.3-backlog/) (claim trees, suite manifests, selective-disclosure resistance via `leaves_total`).
|
|
623
625
|
|
|
624
|
-
The CLI is downstream of the spec: spec v0.2 frozen 2026-05-22; the CLI tracks it (currently falsify 0.5.
|
|
626
|
+
The CLI is downstream of the spec: spec v0.2 frozen 2026-05-22; the CLI tracks it (currently falsify 0.5.2). Spec v0.3 work is in [`spec/v0.3-backlog/`](spec/v0.3-backlog/).
|
|
625
627
|
|
|
626
628
|
</details>
|
|
627
629
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
|
|
4
4
|
[](https://github.com/studio-11-co/falsify/actions/workflows/multi-lang-conformance.yml)
|
|
5
|
-
[](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
|
|
6
6
|
[](spec/test-vectors/)
|
|
7
7
|
[](https://www.bestpractices.dev/projects/13539)
|
|
8
8
|
[](claims/self/)
|
|
@@ -47,6 +47,8 @@ TAMPERED
|
|
|
47
47
|
# exit 3
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
**For CI steps and coding agents.** Add `--json` to `verify`, `lock` or `hash` for one JSON object on stdout with stable keys (`status`, `exit_code`, `sha256`, `recorded`, `observed`, ...); exit codes do not change. `status` tells apart the two exit-11 cases (`NO_LOCK`, `DATASET_MISMATCH`), and errors come back as `"status": "ERROR"`.
|
|
51
|
+
|
|
50
52
|
**Timestamped receipt from the terminal.** `falsify anchor` posts the manifest to the public registry and saves the receipt next to it; `falsify receipt verify` checks it offline, without trusting the registry and without OpenSSL: the RFC 3161 token is checked against Sigstore's timestamp authority root, and the Rekor v2 entry by its inclusion proof and signed checkpoint.
|
|
51
53
|
|
|
52
54
|
```bash
|
|
@@ -88,7 +90,7 @@ No install? Verify any manifest in-browser at [registry.falsify.dev](https://reg
|
|
|
88
90
|
| Your own platform | [`docs/EMBED.md`](docs/EMBED.md): three pure functions and a five-line hook |
|
|
89
91
|
| A real AI delivery | [Acceptance Evidence Pack](https://falsify.dev/pricing/#pilot), contracted through Falsify OÜ (reg. 17574308, Tallinn, Estonia) |
|
|
90
92
|
|
|
91
|
-
**Current version:** falsify 0.5.
|
|
93
|
+
**Current version:** falsify 0.5.2 (PRML CLI) · falsify-engine 0.5.2 — `falsify --version`.
|
|
92
94
|
|
|
93
95
|
PRML v0.2 frozen 2026-05-22. Four reference implementations (Python, JavaScript, Go, Rust), byte-equivalent on all 21 conformance vectors (13 v0.1 + 8 v0.2), 20 reject vectors and the 92-vector edge suite derived from the §3.6 formal grammar.
|
|
94
96
|
|
|
@@ -304,7 +306,7 @@ Consume falsify's hooks from your own repo:
|
|
|
304
306
|
```yaml
|
|
305
307
|
repos:
|
|
306
308
|
- repo: https://github.com/studio-11-co/falsify
|
|
307
|
-
rev: v0.5.
|
|
309
|
+
rev: v0.5.2
|
|
308
310
|
hooks:
|
|
309
311
|
- id: falsify-guard
|
|
310
312
|
- id: falsify-doctor
|
|
@@ -576,17 +578,17 @@ Run `make dogfood` to re-verify. CI runs these on every PR.
|
|
|
576
578
|
|
|
577
579
|
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
578
580
|
|
|
579
|
-
> **Latest — 2026-10-
|
|
581
|
+
> **Latest — 2026-10-31** · **falsify 0.5.2** on PyPI: the wheel now ships `falsify_linkage.py` and `falsify linkage start|finish|verify` works from the `falsify` command (0.5.1 left the module out); 0.5.1 added `--json` on `verify`, `lock` and `hash`; 0.5.0 added `falsify anchor` and `falsify receipt verify` (RFC 3161 + Rekor, offline), `init` writes a lockable manifest, `attest --as test-result`; v0.3.13 (2026-08-23) fixed three specification defects and expanded the negative-conformance suite to 20 reject vectors. The conformance suite stays at 21 positive + 20 reject vectors across the four reference implementations. Registry receipts carry an RFC 3161 countersignature and a Rekor v2 mirror.
|
|
580
582
|
|
|
581
583
|
<a id="roadmap"></a>
|
|
582
584
|
### Roadmap
|
|
583
585
|
|
|
584
586
|
Two roadmaps run alongside each other:
|
|
585
587
|
|
|
586
|
-
- **CLI tool roadmap:** [ROADMAP.md](ROADMAP.md) — `falsify` features, integrations, dependencies. **Current CLI: falsify 0.5.
|
|
588
|
+
- **CLI tool roadmap:** [ROADMAP.md](ROADMAP.md) — `falsify` features, integrations, dependencies. **Current CLI: falsify 0.5.2 on PyPI.**
|
|
587
589
|
- **Specification roadmap:** [spec/v0.2/ROADMAP.md](spec/v0.2/ROADMAP.md) — PRML format evolution, canonicalization grammar, conformance. **Spec v0.2 frozen 2026-05-22.** v0.3 design backlog open under [`spec/v0.3-backlog/`](spec/v0.3-backlog/) (claim trees, suite manifests, selective-disclosure resistance via `leaves_total`).
|
|
588
590
|
|
|
589
|
-
The CLI is downstream of the spec: spec v0.2 frozen 2026-05-22; the CLI tracks it (currently falsify 0.5.
|
|
591
|
+
The CLI is downstream of the spec: spec v0.2 frozen 2026-05-22; the CLI tracks it (currently falsify 0.5.2). Spec v0.3 work is in [`spec/v0.3-backlog/`](spec/v0.3-backlog/).
|
|
590
592
|
|
|
591
593
|
</details>
|
|
592
594
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: falsify
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: PRML reference CLI (`falsify`) — pre-register an ML evaluation claim as a SHA-256 manifest; verify PASS/FAIL/TAMPERED. Also ships `falsify-engine`, a separate pre-registration workflow CLI with its own schema.
|
|
5
5
|
Author: Cüneyt Öztürk
|
|
6
6
|
License: MIT
|
|
@@ -37,7 +37,7 @@ Dynamic: license-file
|
|
|
37
37
|
|
|
38
38
|
[](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
|
|
39
39
|
[](https://github.com/studio-11-co/falsify/actions/workflows/multi-lang-conformance.yml)
|
|
40
|
-
[](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
|
|
41
41
|
[](spec/test-vectors/)
|
|
42
42
|
[](https://www.bestpractices.dev/projects/13539)
|
|
43
43
|
[](claims/self/)
|
|
@@ -82,6 +82,8 @@ TAMPERED
|
|
|
82
82
|
# exit 3
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
**For CI steps and coding agents.** Add `--json` to `verify`, `lock` or `hash` for one JSON object on stdout with stable keys (`status`, `exit_code`, `sha256`, `recorded`, `observed`, ...); exit codes do not change. `status` tells apart the two exit-11 cases (`NO_LOCK`, `DATASET_MISMATCH`), and errors come back as `"status": "ERROR"`.
|
|
86
|
+
|
|
85
87
|
**Timestamped receipt from the terminal.** `falsify anchor` posts the manifest to the public registry and saves the receipt next to it; `falsify receipt verify` checks it offline, without trusting the registry and without OpenSSL: the RFC 3161 token is checked against Sigstore's timestamp authority root, and the Rekor v2 entry by its inclusion proof and signed checkpoint.
|
|
86
88
|
|
|
87
89
|
```bash
|
|
@@ -123,7 +125,7 @@ No install? Verify any manifest in-browser at [registry.falsify.dev](https://reg
|
|
|
123
125
|
| Your own platform | [`docs/EMBED.md`](docs/EMBED.md): three pure functions and a five-line hook |
|
|
124
126
|
| A real AI delivery | [Acceptance Evidence Pack](https://falsify.dev/pricing/#pilot), contracted through Falsify OÜ (reg. 17574308, Tallinn, Estonia) |
|
|
125
127
|
|
|
126
|
-
**Current version:** falsify 0.5.
|
|
128
|
+
**Current version:** falsify 0.5.2 (PRML CLI) · falsify-engine 0.5.2 — `falsify --version`.
|
|
127
129
|
|
|
128
130
|
PRML v0.2 frozen 2026-05-22. Four reference implementations (Python, JavaScript, Go, Rust), byte-equivalent on all 21 conformance vectors (13 v0.1 + 8 v0.2), 20 reject vectors and the 92-vector edge suite derived from the §3.6 formal grammar.
|
|
129
131
|
|
|
@@ -339,7 +341,7 @@ Consume falsify's hooks from your own repo:
|
|
|
339
341
|
```yaml
|
|
340
342
|
repos:
|
|
341
343
|
- repo: https://github.com/studio-11-co/falsify
|
|
342
|
-
rev: v0.5.
|
|
344
|
+
rev: v0.5.2
|
|
343
345
|
hooks:
|
|
344
346
|
- id: falsify-guard
|
|
345
347
|
- id: falsify-doctor
|
|
@@ -611,17 +613,17 @@ Run `make dogfood` to re-verify. CI runs these on every PR.
|
|
|
611
613
|
|
|
612
614
|
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
613
615
|
|
|
614
|
-
> **Latest — 2026-10-
|
|
616
|
+
> **Latest — 2026-10-31** · **falsify 0.5.2** on PyPI: the wheel now ships `falsify_linkage.py` and `falsify linkage start|finish|verify` works from the `falsify` command (0.5.1 left the module out); 0.5.1 added `--json` on `verify`, `lock` and `hash`; 0.5.0 added `falsify anchor` and `falsify receipt verify` (RFC 3161 + Rekor, offline), `init` writes a lockable manifest, `attest --as test-result`; v0.3.13 (2026-08-23) fixed three specification defects and expanded the negative-conformance suite to 20 reject vectors. The conformance suite stays at 21 positive + 20 reject vectors across the four reference implementations. Registry receipts carry an RFC 3161 countersignature and a Rekor v2 mirror.
|
|
615
617
|
|
|
616
618
|
<a id="roadmap"></a>
|
|
617
619
|
### Roadmap
|
|
618
620
|
|
|
619
621
|
Two roadmaps run alongside each other:
|
|
620
622
|
|
|
621
|
-
- **CLI tool roadmap:** [ROADMAP.md](ROADMAP.md) — `falsify` features, integrations, dependencies. **Current CLI: falsify 0.5.
|
|
623
|
+
- **CLI tool roadmap:** [ROADMAP.md](ROADMAP.md) — `falsify` features, integrations, dependencies. **Current CLI: falsify 0.5.2 on PyPI.**
|
|
622
624
|
- **Specification roadmap:** [spec/v0.2/ROADMAP.md](spec/v0.2/ROADMAP.md) — PRML format evolution, canonicalization grammar, conformance. **Spec v0.2 frozen 2026-05-22.** v0.3 design backlog open under [`spec/v0.3-backlog/`](spec/v0.3-backlog/) (claim trees, suite manifests, selective-disclosure resistance via `leaves_total`).
|
|
623
625
|
|
|
624
|
-
The CLI is downstream of the spec: spec v0.2 frozen 2026-05-22; the CLI tracks it (currently falsify 0.5.
|
|
626
|
+
The CLI is downstream of the spec: spec v0.2 frozen 2026-05-22; the CLI tracks it (currently falsify 0.5.2). Spec v0.3 work is in [`spec/v0.3-backlog/`](spec/v0.3-backlog/).
|
|
625
627
|
|
|
626
628
|
</details>
|
|
627
629
|
|
|
@@ -2,6 +2,7 @@ LICENSE
|
|
|
2
2
|
NOTICE
|
|
3
3
|
README.md
|
|
4
4
|
falsify.py
|
|
5
|
+
falsify_linkage.py
|
|
5
6
|
falsify_prml.py
|
|
6
7
|
falsify_receipt.py
|
|
7
8
|
pyproject.toml
|
|
@@ -59,10 +60,12 @@ tests/test_makefile.py
|
|
|
59
60
|
tests/test_managed_agents.py
|
|
60
61
|
tests/test_mcp.py
|
|
61
62
|
tests/test_mcp_server.py
|
|
63
|
+
tests/test_packaging.py
|
|
62
64
|
tests/test_pitch.py
|
|
63
65
|
tests/test_pre_commit.py
|
|
64
66
|
tests/test_prml_cli.py
|
|
65
67
|
tests/test_prml_init.py
|
|
68
|
+
tests/test_prml_json.py
|
|
66
69
|
tests/test_prml_v02_candidates.py
|
|
67
70
|
tests/test_prml_vectors.py
|
|
68
71
|
tests/test_prml_verify_observed.py
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Reference implementation of prml-linkage/0 (draft).
|
|
2
|
+
|
|
3
|
+
Spec: spec/linkage/prml-linkage-0.md. Non-normative draft; the format may
|
|
4
|
+
change until draft 0 is frozen.
|
|
5
|
+
|
|
6
|
+
Canonicalization intentionally mirrors falsify._canonicalize so that a
|
|
7
|
+
linkage record hashes under the same rules as a PRML manifest. The parity
|
|
8
|
+
is asserted by tests/test_linkage.py rather than by importing falsify.py,
|
|
9
|
+
so this module stays dependency-free beyond PyYAML.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import re
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
LINKAGE_VERSION = "prml-linkage/0"
|
|
22
|
+
|
|
23
|
+
START_FIELDS = {"linkage_version", "manifest_hash", "receipt", "run"}
|
|
24
|
+
FINAL_FIELDS = START_FIELDS | {"start_hash", "result"}
|
|
25
|
+
RUN_FIELDS = {"id", "started_at", "environment", "model_version", "dataset_hash"}
|
|
26
|
+
RESULT_FIELDS = {"observed", "digest", "exit_code", "finished_at"}
|
|
27
|
+
VALID_EXIT_CODES = {0, 3, 10, 11}
|
|
28
|
+
|
|
29
|
+
_SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def canonicalize(record: Any) -> str:
|
|
33
|
+
"""Stable YAML rendering, identical rules to falsify._canonicalize."""
|
|
34
|
+
return yaml.safe_dump(
|
|
35
|
+
record,
|
|
36
|
+
sort_keys=True,
|
|
37
|
+
default_flow_style=False,
|
|
38
|
+
allow_unicode=True,
|
|
39
|
+
width=4096,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def linkage_hash(record: dict) -> str:
|
|
44
|
+
return hashlib.sha256(canonicalize(record).encode("utf-8")).hexdigest()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _parse_rfc3339(value: str) -> datetime:
|
|
48
|
+
dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
49
|
+
if dt.tzinfo is None:
|
|
50
|
+
raise ValueError(f"timestamp lacks timezone: {value!r}")
|
|
51
|
+
return dt.astimezone(timezone.utc)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_start(
|
|
55
|
+
manifest_hash: str,
|
|
56
|
+
run_id: str,
|
|
57
|
+
environment: str,
|
|
58
|
+
dataset_hash: str,
|
|
59
|
+
*,
|
|
60
|
+
receipt: Optional[str] = None,
|
|
61
|
+
model_version: Optional[str] = None,
|
|
62
|
+
started_at: Optional[str] = None,
|
|
63
|
+
) -> dict:
|
|
64
|
+
"""Create a start record at run start. `started_at` defaults to now (UTC)."""
|
|
65
|
+
if not _SHA256_RE.match(manifest_hash):
|
|
66
|
+
raise ValueError("manifest_hash must be 64 lowercase hex chars")
|
|
67
|
+
if not _SHA256_RE.match(dataset_hash):
|
|
68
|
+
raise ValueError("dataset_hash must be 64 lowercase hex chars")
|
|
69
|
+
if started_at is None:
|
|
70
|
+
# Full microsecond precision: sub-second runs must still satisfy
|
|
71
|
+
# the spec's strict started_at < finished_at chronology.
|
|
72
|
+
started_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
73
|
+
_parse_rfc3339(started_at)
|
|
74
|
+
return {
|
|
75
|
+
"linkage_version": LINKAGE_VERSION,
|
|
76
|
+
"manifest_hash": manifest_hash,
|
|
77
|
+
"receipt": receipt,
|
|
78
|
+
"run": {
|
|
79
|
+
"id": run_id,
|
|
80
|
+
"started_at": started_at,
|
|
81
|
+
"environment": environment,
|
|
82
|
+
"model_version": model_version,
|
|
83
|
+
"dataset_hash": dataset_hash,
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def finalize(
|
|
89
|
+
start_record: dict,
|
|
90
|
+
observed: float,
|
|
91
|
+
result_digest: str,
|
|
92
|
+
exit_code: int,
|
|
93
|
+
*,
|
|
94
|
+
finished_at: Optional[str] = None,
|
|
95
|
+
) -> dict:
|
|
96
|
+
"""Create the final record from a start record plus the run result."""
|
|
97
|
+
problems = _validate_shape(start_record, final=False)
|
|
98
|
+
if problems:
|
|
99
|
+
raise ValueError(f"invalid start record: {problems}")
|
|
100
|
+
if exit_code not in VALID_EXIT_CODES:
|
|
101
|
+
raise ValueError(f"exit_code must be one of {sorted(VALID_EXIT_CODES)}")
|
|
102
|
+
if not _SHA256_RE.match(result_digest):
|
|
103
|
+
raise ValueError("result digest must be 64 lowercase hex chars")
|
|
104
|
+
if finished_at is None:
|
|
105
|
+
finished_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
106
|
+
_parse_rfc3339(finished_at)
|
|
107
|
+
final = {k: start_record[k] for k in START_FIELDS}
|
|
108
|
+
final["run"] = dict(start_record["run"])
|
|
109
|
+
final["start_hash"] = linkage_hash(start_record)
|
|
110
|
+
final["result"] = {
|
|
111
|
+
# Spec float rule: observed is float64; integer values render as "x.0"
|
|
112
|
+
"observed": float(observed),
|
|
113
|
+
"digest": result_digest,
|
|
114
|
+
"exit_code": exit_code,
|
|
115
|
+
"finished_at": finished_at,
|
|
116
|
+
}
|
|
117
|
+
return final
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _validate_shape(record: dict, *, final: bool) -> list[str]:
|
|
121
|
+
problems: list[str] = []
|
|
122
|
+
expected = FINAL_FIELDS if final else START_FIELDS
|
|
123
|
+
if not isinstance(record, dict):
|
|
124
|
+
return ["record is not a mapping"]
|
|
125
|
+
if record.get("linkage_version") != LINKAGE_VERSION:
|
|
126
|
+
problems.append(f"linkage_version must be {LINKAGE_VERSION!r}")
|
|
127
|
+
missing = expected - set(record)
|
|
128
|
+
extra = set(record) - expected
|
|
129
|
+
if missing:
|
|
130
|
+
problems.append(f"missing fields: {sorted(missing)}")
|
|
131
|
+
if extra:
|
|
132
|
+
problems.append(f"unknown fields: {sorted(extra)}")
|
|
133
|
+
run = record.get("run")
|
|
134
|
+
if not isinstance(run, dict):
|
|
135
|
+
problems.append("run is not a mapping")
|
|
136
|
+
else:
|
|
137
|
+
if set(run) != RUN_FIELDS:
|
|
138
|
+
problems.append(
|
|
139
|
+
f"run fields must be exactly {sorted(RUN_FIELDS)}, got {sorted(run)}"
|
|
140
|
+
)
|
|
141
|
+
dh = run.get("dataset_hash")
|
|
142
|
+
if not (isinstance(dh, str) and _SHA256_RE.match(dh)):
|
|
143
|
+
problems.append("run.dataset_hash must be 64 lowercase hex chars")
|
|
144
|
+
try:
|
|
145
|
+
_parse_rfc3339(str(run.get("started_at")))
|
|
146
|
+
except Exception:
|
|
147
|
+
problems.append("run.started_at is not RFC 3339")
|
|
148
|
+
mh = record.get("manifest_hash")
|
|
149
|
+
if not (isinstance(mh, str) and _SHA256_RE.match(mh)):
|
|
150
|
+
problems.append("manifest_hash must be 64 lowercase hex chars")
|
|
151
|
+
if final:
|
|
152
|
+
sh = record.get("start_hash")
|
|
153
|
+
if not (isinstance(sh, str) and _SHA256_RE.match(sh)):
|
|
154
|
+
problems.append("start_hash must be 64 lowercase hex chars")
|
|
155
|
+
result = record.get("result")
|
|
156
|
+
if not isinstance(result, dict):
|
|
157
|
+
problems.append("result is not a mapping")
|
|
158
|
+
else:
|
|
159
|
+
if set(result) != RESULT_FIELDS:
|
|
160
|
+
problems.append(
|
|
161
|
+
f"result fields must be exactly {sorted(RESULT_FIELDS)}, got {sorted(result)}"
|
|
162
|
+
)
|
|
163
|
+
if result.get("exit_code") not in VALID_EXIT_CODES:
|
|
164
|
+
problems.append(f"result.exit_code must be one of {sorted(VALID_EXIT_CODES)}")
|
|
165
|
+
dg = result.get("digest")
|
|
166
|
+
if not (isinstance(dg, str) and _SHA256_RE.match(dg)):
|
|
167
|
+
problems.append("result.digest must be 64 lowercase hex chars")
|
|
168
|
+
try:
|
|
169
|
+
_parse_rfc3339(str(result.get("finished_at")))
|
|
170
|
+
except Exception:
|
|
171
|
+
problems.append("result.finished_at is not RFC 3339")
|
|
172
|
+
return problems
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
_COMPARATORS = {
|
|
176
|
+
">=": lambda a, b: a >= b,
|
|
177
|
+
">": lambda a, b: a > b,
|
|
178
|
+
"<=": lambda a, b: a <= b,
|
|
179
|
+
"<": lambda a, b: a < b,
|
|
180
|
+
"==": lambda a, b: a == b,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def verify(
|
|
185
|
+
final_record: dict,
|
|
186
|
+
*,
|
|
187
|
+
start_record: Optional[dict] = None,
|
|
188
|
+
manifest: Optional[dict] = None,
|
|
189
|
+
manifest_hash: Optional[str] = None,
|
|
190
|
+
) -> dict:
|
|
191
|
+
"""Verify a final linkage record per spec §4.
|
|
192
|
+
|
|
193
|
+
Returns {"ok": bool, "tier": "L1"|"L2", "failures": [...], "skipped": [...]}.
|
|
194
|
+
Tier L3 (anchored) cannot be established offline by this function alone;
|
|
195
|
+
callers holding anchor evidence check §4.8 themselves.
|
|
196
|
+
"""
|
|
197
|
+
failures: list[dict] = []
|
|
198
|
+
skipped: list[str] = []
|
|
199
|
+
|
|
200
|
+
problems = _validate_shape(final_record, final=True)
|
|
201
|
+
if problems:
|
|
202
|
+
return {
|
|
203
|
+
"ok": False,
|
|
204
|
+
"tier": None,
|
|
205
|
+
"failures": [{"check": "malformed", "detail": p} for p in problems],
|
|
206
|
+
"skipped": [],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
tier = "L1"
|
|
210
|
+
if start_record is not None:
|
|
211
|
+
tier = "L2"
|
|
212
|
+
if linkage_hash(start_record) != final_record["start_hash"]:
|
|
213
|
+
failures.append({"check": "chain-broken", "detail": "hash(start) != start_hash"})
|
|
214
|
+
else:
|
|
215
|
+
for key in START_FIELDS:
|
|
216
|
+
if start_record.get(key) != final_record.get(key):
|
|
217
|
+
failures.append(
|
|
218
|
+
{"check": "chain-broken", "detail": f"field {key!r} differs between start and final"}
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
skipped.append("chain (no start record supplied)")
|
|
222
|
+
|
|
223
|
+
started = _parse_rfc3339(final_record["run"]["started_at"])
|
|
224
|
+
finished = _parse_rfc3339(final_record["result"]["finished_at"])
|
|
225
|
+
if not started < finished:
|
|
226
|
+
failures.append({"check": "chronology", "detail": "started_at is not before finished_at"})
|
|
227
|
+
|
|
228
|
+
if manifest is not None and manifest_hash is None:
|
|
229
|
+
manifest_hash = hashlib.sha256(canonicalize(manifest).encode("utf-8")).hexdigest()
|
|
230
|
+
|
|
231
|
+
if manifest_hash is not None:
|
|
232
|
+
if final_record["manifest_hash"] != manifest_hash:
|
|
233
|
+
failures.append({"check": "manifest-mismatch", "detail": "manifest_hash differs"})
|
|
234
|
+
else:
|
|
235
|
+
skipped.append("manifest hash (no manifest supplied)")
|
|
236
|
+
|
|
237
|
+
if manifest is not None:
|
|
238
|
+
m_dataset = (manifest.get("dataset") or {}).get("hash")
|
|
239
|
+
if final_record["run"]["dataset_hash"] != m_dataset:
|
|
240
|
+
failures.append({"check": "dataset-mismatch", "detail": "run.dataset_hash != manifest dataset.hash"})
|
|
241
|
+
comparator = manifest.get("comparator")
|
|
242
|
+
threshold = manifest.get("threshold")
|
|
243
|
+
exit_code = final_record["result"]["exit_code"]
|
|
244
|
+
if comparator in _COMPARATORS and isinstance(threshold, (int, float)) and exit_code in (0, 10):
|
|
245
|
+
passed = _COMPARATORS[comparator](final_record["result"]["observed"], threshold)
|
|
246
|
+
expected = 0 if passed else 10
|
|
247
|
+
if exit_code != expected:
|
|
248
|
+
failures.append(
|
|
249
|
+
{"check": "verdict-mismatch", "detail": f"observed vs threshold implies exit {expected}, record says {exit_code}"}
|
|
250
|
+
)
|
|
251
|
+
elif exit_code in (3, 11):
|
|
252
|
+
skipped.append("verdict recompute (error exit code)")
|
|
253
|
+
else:
|
|
254
|
+
skipped.append("dataset + verdict (no manifest supplied)")
|
|
255
|
+
|
|
256
|
+
return {"ok": not failures, "tier": tier, "failures": failures, "skipped": skipped}
|