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.
Files changed (101) hide show
  1. {falsify-0.5.0/falsify.egg-info → falsify-0.5.2}/PKG-INFO +9 -7
  2. {falsify-0.5.0 → falsify-0.5.2}/README.md +8 -6
  3. {falsify-0.5.0 → falsify-0.5.2/falsify.egg-info}/PKG-INFO +9 -7
  4. {falsify-0.5.0 → falsify-0.5.2}/falsify.egg-info/SOURCES.txt +3 -0
  5. {falsify-0.5.0 → falsify-0.5.2}/falsify.egg-info/top_level.txt +1 -0
  6. {falsify-0.5.0 → falsify-0.5.2}/falsify.py +1 -1
  7. falsify-0.5.2/falsify_linkage.py +256 -0
  8. {falsify-0.5.0 → falsify-0.5.2}/falsify_prml.py +98 -36
  9. {falsify-0.5.0 → falsify-0.5.2}/pyproject.toml +2 -2
  10. falsify-0.5.2/tests/test_packaging.py +102 -0
  11. falsify-0.5.2/tests/test_prml_json.py +81 -0
  12. {falsify-0.5.0 → falsify-0.5.2}/LICENSE +0 -0
  13. {falsify-0.5.0 → falsify-0.5.2}/NOTICE +0 -0
  14. {falsify-0.5.0 → falsify-0.5.2}/falsify.egg-info/dependency_links.txt +0 -0
  15. {falsify-0.5.0 → falsify-0.5.2}/falsify.egg-info/entry_points.txt +0 -0
  16. {falsify-0.5.0 → falsify-0.5.2}/falsify.egg-info/requires.txt +0 -0
  17. {falsify-0.5.0 → falsify-0.5.2}/falsify_receipt.py +0 -0
  18. {falsify-0.5.0 → falsify-0.5.2}/mcp_server/__init__.py +0 -0
  19. {falsify-0.5.0 → falsify-0.5.2}/mcp_server/__main__.py +0 -0
  20. {falsify-0.5.0 → falsify-0.5.2}/mcp_server/server.py +0 -0
  21. {falsify-0.5.0 → falsify-0.5.2}/setup.cfg +0 -0
  22. {falsify-0.5.0 → falsify-0.5.2}/tests/test_adversarial_doc.py +0 -0
  23. {falsify-0.5.0 → falsify-0.5.2}/tests/test_agent_claim_auditor.py +0 -0
  24. {falsify-0.5.0 → falsify-0.5.2}/tests/test_agent_verdict_refresher.py +0 -0
  25. {falsify-0.5.0 → falsify-0.5.2}/tests/test_architecture.py +0 -0
  26. {falsify-0.5.0 → falsify-0.5.2}/tests/test_attest.py +0 -0
  27. {falsify-0.5.0 → falsify-0.5.2}/tests/test_attest_test_result.py +0 -0
  28. {falsify-0.5.0 → falsify-0.5.2}/tests/test_bench.py +0 -0
  29. {falsify-0.5.0 → falsify-0.5.2}/tests/test_calibration_sample.py +0 -0
  30. {falsify-0.5.0 → falsify-0.5.2}/tests/test_case_studies_doc.py +0 -0
  31. {falsify-0.5.0 → falsify-0.5.2}/tests/test_changelog.py +0 -0
  32. {falsify-0.5.0 → falsify-0.5.2}/tests/test_ci_workflow.py +0 -0
  33. {falsify-0.5.0 → falsify-0.5.2}/tests/test_claude_md.py +0 -0
  34. {falsify-0.5.0 → falsify-0.5.2}/tests/test_code_of_conduct.py +0 -0
  35. {falsify-0.5.0 → falsify-0.5.2}/tests/test_comparison_doc.py +0 -0
  36. {falsify-0.5.0 → falsify-0.5.2}/tests/test_conform_cmd.py +0 -0
  37. {falsify-0.5.0 → falsify-0.5.2}/tests/test_contributing.py +0 -0
  38. {falsify-0.5.0 → falsify-0.5.2}/tests/test_demo_script.py +0 -0
  39. {falsify-0.5.0 → falsify-0.5.2}/tests/test_demo_script_doc.py +0 -0
  40. {falsify-0.5.0 → falsify-0.5.2}/tests/test_demo_shot_list.py +0 -0
  41. {falsify-0.5.0 → falsify-0.5.2}/tests/test_diff.py +0 -0
  42. {falsify-0.5.0 → falsify-0.5.2}/tests/test_docker.py +0 -0
  43. {falsify-0.5.0 → falsify-0.5.2}/tests/test_doctor.py +0 -0
  44. {falsify-0.5.0 → falsify-0.5.2}/tests/test_editorconfig.py +0 -0
  45. {falsify-0.5.0 → falsify-0.5.2}/tests/test_embed_example.py +0 -0
  46. {falsify-0.5.0 → falsify-0.5.2}/tests/test_examples_doc.py +0 -0
  47. {falsify-0.5.0 → falsify-0.5.2}/tests/test_export.py +0 -0
  48. {falsify-0.5.0 → falsify-0.5.2}/tests/test_faq.py +0 -0
  49. {falsify-0.5.0 → falsify-0.5.2}/tests/test_github_repo_maturity.py +0 -0
  50. {falsify-0.5.0 → falsify-0.5.2}/tests/test_github_templates.py +0 -0
  51. {falsify-0.5.0 → falsify-0.5.2}/tests/test_gitignore.py +0 -0
  52. {falsify-0.5.0 → falsify-0.5.2}/tests/test_glossary_doc.py +0 -0
  53. {falsify-0.5.0 → falsify-0.5.2}/tests/test_grammar.py +0 -0
  54. {falsify-0.5.0 → falsify-0.5.2}/tests/test_guard.py +0 -0
  55. {falsify-0.5.0 → falsify-0.5.2}/tests/test_hook_install.py +0 -0
  56. {falsify-0.5.0 → falsify-0.5.2}/tests/test_init.py +0 -0
  57. {falsify-0.5.0 → falsify-0.5.2}/tests/test_init_templates.py +0 -0
  58. {falsify-0.5.0 → falsify-0.5.2}/tests/test_integration_e2e.py +0 -0
  59. {falsify-0.5.0 → falsify-0.5.2}/tests/test_linkage.py +0 -0
  60. {falsify-0.5.0 → falsify-0.5.2}/tests/test_linkage_parity.py +0 -0
  61. {falsify-0.5.0 → falsify-0.5.2}/tests/test_list.py +0 -0
  62. {falsify-0.5.0 → falsify-0.5.2}/tests/test_lock.py +0 -0
  63. {falsify-0.5.0 → falsify-0.5.2}/tests/test_makefile.py +0 -0
  64. {falsify-0.5.0 → falsify-0.5.2}/tests/test_managed_agents.py +0 -0
  65. {falsify-0.5.0 → falsify-0.5.2}/tests/test_mcp.py +0 -0
  66. {falsify-0.5.0 → falsify-0.5.2}/tests/test_mcp_server.py +0 -0
  67. {falsify-0.5.0 → falsify-0.5.2}/tests/test_pitch.py +0 -0
  68. {falsify-0.5.0 → falsify-0.5.2}/tests/test_pre_commit.py +0 -0
  69. {falsify-0.5.0 → falsify-0.5.2}/tests/test_prml_cli.py +0 -0
  70. {falsify-0.5.0 → falsify-0.5.2}/tests/test_prml_init.py +0 -0
  71. {falsify-0.5.0 → falsify-0.5.2}/tests/test_prml_v02_candidates.py +0 -0
  72. {falsify-0.5.0 → falsify-0.5.2}/tests/test_prml_vectors.py +0 -0
  73. {falsify-0.5.0 → falsify-0.5.2}/tests/test_prml_verify_observed.py +0 -0
  74. {falsify-0.5.0 → falsify-0.5.2}/tests/test_pyproject.py +0 -0
  75. {falsify-0.5.0 → falsify-0.5.2}/tests/test_readme.py +0 -0
  76. {falsify-0.5.0 → falsify-0.5.2}/tests/test_receipt.py +0 -0
  77. {falsify-0.5.0 → falsify-0.5.2}/tests/test_reject_vectors.py +0 -0
  78. {falsify-0.5.0 → falsify-0.5.2}/tests/test_release_check.py +0 -0
  79. {falsify-0.5.0 → falsify-0.5.2}/tests/test_release_workflow.py +0 -0
  80. {falsify-0.5.0 → falsify-0.5.2}/tests/test_replay.py +0 -0
  81. {falsify-0.5.0 → falsify-0.5.2}/tests/test_roadmap.py +0 -0
  82. {falsify-0.5.0 → falsify-0.5.2}/tests/test_run.py +0 -0
  83. {falsify-0.5.0 → falsify-0.5.2}/tests/test_score.py +0 -0
  84. {falsify-0.5.0 → falsify-0.5.2}/tests/test_self_dogfood.py +0 -0
  85. {falsify-0.5.0 → falsify-0.5.2}/tests/test_skill_author.py +0 -0
  86. {falsify-0.5.0 → falsify-0.5.2}/tests/test_skill_ci_doctor.py +0 -0
  87. {falsify-0.5.0 → falsify-0.5.2}/tests/test_skill_claim_audit.py +0 -0
  88. {falsify-0.5.0 → falsify-0.5.2}/tests/test_skill_claim_review.py +0 -0
  89. {falsify-0.5.0 → falsify-0.5.2}/tests/test_skill_falsify.py +0 -0
  90. {falsify-0.5.0 → falsify-0.5.2}/tests/test_slash_commands.py +0 -0
  91. {falsify-0.5.0 → falsify-0.5.2}/tests/test_stats.py +0 -0
  92. {falsify-0.5.0 → falsify-0.5.2}/tests/test_stats_html.py +0 -0
  93. {falsify-0.5.0 → falsify-0.5.2}/tests/test_submission.py +0 -0
  94. {falsify-0.5.0 → falsify-0.5.2}/tests/test_submission_md.py +0 -0
  95. {falsify-0.5.0 → falsify-0.5.2}/tests/test_trend.py +0 -0
  96. {falsify-0.5.0 → falsify-0.5.2}/tests/test_tutorial.py +0 -0
  97. {falsify-0.5.0 → falsify-0.5.2}/tests/test_verdict.py +0 -0
  98. {falsify-0.5.0 → falsify-0.5.2}/tests/test_verify.py +0 -0
  99. {falsify-0.5.0 → falsify-0.5.2}/tests/test_version.py +0 -0
  100. {falsify-0.5.0 → falsify-0.5.2}/tests/test_why.py +0 -0
  101. {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.0
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
  [![CI](https://img.shields.io/github/actions/workflow/status/studio-11-co/falsify/falsify.yml?branch=main&label=CI&style=flat-square)](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
39
39
  [![Multi-lang Conformance](https://img.shields.io/github/actions/workflow/status/studio-11-co/falsify/multi-lang-conformance.yml?branch=main&label=conformance%20%C2%B7%204%20languages&style=flat-square)](https://github.com/studio-11-co/falsify/actions/workflows/multi-lang-conformance.yml)
40
- [![tests](https://img.shields.io/badge/tests-666%20passing-2ea44f?style=flat-square)](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
40
+ [![tests](https://img.shields.io/badge/tests-679%20passing-2ea44f?style=flat-square)](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
41
41
  [![vectors](https://img.shields.io/badge/vectors-21%20positive%20%C2%B7%2020%20reject%20%C2%B7%2092%20edge-2ea44f?style=flat-square)](spec/test-vectors/)
42
42
  [![OpenSSF Best Practices](https://img.shields.io/cii/level/13539?label=OpenSSF%20best%20practices&style=flat-square)](https://www.bestpractices.dev/projects/13539)
43
43
  [![honesty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/studio-11-co/falsify/main/.falsify/badge.json&label=honesty&style=flat-square)](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.0 (PRML CLI) · falsify-engine 0.5.0 — `falsify --version`.
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.0
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-28** · **falsify 0.5.0** on PyPI: `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.
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.0 on PyPI.**
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.0). Spec v0.3 work is in [`spec/v0.3-backlog/`](spec/v0.3-backlog/).
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
  [![CI](https://img.shields.io/github/actions/workflow/status/studio-11-co/falsify/falsify.yml?branch=main&label=CI&style=flat-square)](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
4
4
  [![Multi-lang Conformance](https://img.shields.io/github/actions/workflow/status/studio-11-co/falsify/multi-lang-conformance.yml?branch=main&label=conformance%20%C2%B7%204%20languages&style=flat-square)](https://github.com/studio-11-co/falsify/actions/workflows/multi-lang-conformance.yml)
5
- [![tests](https://img.shields.io/badge/tests-666%20passing-2ea44f?style=flat-square)](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
5
+ [![tests](https://img.shields.io/badge/tests-679%20passing-2ea44f?style=flat-square)](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
6
6
  [![vectors](https://img.shields.io/badge/vectors-21%20positive%20%C2%B7%2020%20reject%20%C2%B7%2092%20edge-2ea44f?style=flat-square)](spec/test-vectors/)
7
7
  [![OpenSSF Best Practices](https://img.shields.io/cii/level/13539?label=OpenSSF%20best%20practices&style=flat-square)](https://www.bestpractices.dev/projects/13539)
8
8
  [![honesty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/studio-11-co/falsify/main/.falsify/badge.json&label=honesty&style=flat-square)](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.0 (PRML CLI) · falsify-engine 0.5.0 — `falsify --version`.
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.0
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-28** · **falsify 0.5.0** on PyPI: `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.
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.0 on PyPI.**
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.0). Spec v0.3 work is in [`spec/v0.3-backlog/`](spec/v0.3-backlog/).
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.0
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
  [![CI](https://img.shields.io/github/actions/workflow/status/studio-11-co/falsify/falsify.yml?branch=main&label=CI&style=flat-square)](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
39
39
  [![Multi-lang Conformance](https://img.shields.io/github/actions/workflow/status/studio-11-co/falsify/multi-lang-conformance.yml?branch=main&label=conformance%20%C2%B7%204%20languages&style=flat-square)](https://github.com/studio-11-co/falsify/actions/workflows/multi-lang-conformance.yml)
40
- [![tests](https://img.shields.io/badge/tests-666%20passing-2ea44f?style=flat-square)](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
40
+ [![tests](https://img.shields.io/badge/tests-679%20passing-2ea44f?style=flat-square)](https://github.com/studio-11-co/falsify/actions/workflows/falsify.yml)
41
41
  [![vectors](https://img.shields.io/badge/vectors-21%20positive%20%C2%B7%2020%20reject%20%C2%B7%2092%20edge-2ea44f?style=flat-square)](spec/test-vectors/)
42
42
  [![OpenSSF Best Practices](https://img.shields.io/cii/level/13539?label=OpenSSF%20best%20practices&style=flat-square)](https://www.bestpractices.dev/projects/13539)
43
43
  [![honesty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/studio-11-co/falsify/main/.falsify/badge.json&label=honesty&style=flat-square)](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.0 (PRML CLI) · falsify-engine 0.5.0 — `falsify --version`.
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.0
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-28** · **falsify 0.5.0** on PyPI: `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.
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.0 on PyPI.**
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.0). Spec v0.3 work is in [`spec/v0.3-backlog/`](spec/v0.3-backlog/).
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
@@ -1,4 +1,5 @@
1
1
  falsify
2
+ falsify_linkage
2
3
  falsify_prml
3
4
  falsify_receipt
4
5
  mcp_server
@@ -25,7 +25,7 @@ from typing import Any, Callable
25
25
 
26
26
  import yaml
27
27
 
28
- __version__ = "0.5.0"
28
+ __version__ = "0.5.2"
29
29
 
30
30
  EXIT_PASS = 0
31
31
  EXIT_FAIL = 10
@@ -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}