diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc30a27..dcd121e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,13 +34,21 @@ jobs: cache: pip - run: python -m pip install -r requirements-dev.txt - run: python -m pip install --no-deps --editable . + - name: Verify repository manifest + run: python tools/rebuild_manifest.py --check - name: Verify canon lock run: python tools/verify_canon_lock.py --canon-root .aset-spec - - name: Run profile and semantic conformance gates + - name: Run complete profile and release gate run: python tools/profile_gate.py --canon-root .aset-spec - uses: actions/upload-artifact@v4 if: always() with: name: aset-python-sqlite-conformance if-no-files-found: warn - path: dist/conformance-results.json + path: | + dist/conformance-results.json + dist/aset-python-sqlite-source.zip + dist/aset-python-sqlite-source.zip.sha256 + dist/wheel/*.whl + dist/blackbox-release-audit.json + dist/blackbox-release-audit.md diff --git a/.github/workflows/update-canon-lock.yml b/.github/workflows/update-canon-lock.yml index 9388d7c..6a9d6eb 100644 --- a/.github/workflows/update-canon-lock.yml +++ b/.github/workflows/update-canon-lock.yml @@ -129,15 +129,16 @@ jobs: ) PY + python tools/rebuild_manifest.py git diff --check - if git diff --quiet -- canon.lock.json; then + if git diff --quiet -- canon.lock.json MANIFEST.json; then printf 'changed=false\n' >> "$GITHUB_OUTPUT" printf 'CANON_LOCK_ALREADY_CURRENT=true\n' else printf 'changed=true\n' >> "$GITHUB_OUTPUT" printf 'CANON_LOCK_UPDATE_REQUIRED=true\n' - git diff -- canon.lock.json + git diff -- canon.lock.json MANIFEST.json fi - name: Set up Python @@ -184,7 +185,7 @@ jobs: git switch -c "$BRANCH" - git add canon.lock.json + git add canon.lock.json MANIFEST.json git diff --cached --check git diff --cached --stat diff --git a/MANIFEST.json b/MANIFEST.json index f0bee09..288a78f 100644 --- a/MANIFEST.json +++ b/MANIFEST.json @@ -1,6 +1,21 @@ { "document_type": "aset-python-sqlite-manifest", "files": [ + { + "path": ".github/workflows/ci.yml", + "sha256": "sha256:913d28cd051125ebc03671c601cc1668d3557e90a9da53038c0997d7f69d83e3", + "size_bytes": 1788 + }, + { + "path": ".github/workflows/update-canon-lock.yml", + "sha256": "sha256:8b6acfc528be82f69465b7c751bb0c9fd60237ba000ac787f72dfdbb45997289", + "size_bytes": 6593 + }, + { + "path": ".gitignore", + "sha256": "sha256:fffcfeac766a4c93019032c2a30eebdbdf7d91a187990e09a50cecba44189b7c", + "size_bytes": 125 + }, { "path": "AUTHORS.md", "sha256": "sha256:1424b7b7dc298def67f25c6debc28040b580eb9d227c66d2ee4a12748116913e", @@ -28,23 +43,23 @@ }, { "path": "ORIGIN.json", - "sha256": "sha256:7bf99415d610be4b9601d02cc4dd4e0d2093c7d2e50ff499b25d268d391937c9", - "size_bytes": 728 + "sha256": "sha256:6ab2461270ca82e0921a0d44dd338e0827f3e66762c558ae8097625ccf3470d5", + "size_bytes": 1193 }, { "path": "README.md", - "sha256": "sha256:902eadc37021cf1125249d941523d1f937eefd3817fa34fc509b3c2bb3ff86fa", - "size_bytes": 1479 + "sha256": "sha256:4e022823c5098bbd7dbe8e9e0c5150291abf9c7b6cc1f950d86cf90087366e41", + "size_bytes": 2117 }, { "path": "README.pt-BR.md", - "sha256": "sha256:d66bf529e42c3faba2801688ede0aac1feb74e38e1a9c59ee33c915fbac37e8d", - "size_bytes": 758 + "sha256": "sha256:8bc44962a333cdd3a20a2e6d8021c92a1022420be7f989f4d479b29ec3784ca7", + "size_bytes": 1432 }, { "path": "README.ru.md", - "sha256": "sha256:ea348226560c5bc17d0881340e1bc6c3430d4b9545f116e38b1dce6b70221572", - "size_bytes": 946 + "sha256": "sha256:bdae01a140d43d00d067531bc7f60abf7c0212d3eaa041f33384b0ae26445952", + "size_bytes": 1881 }, { "path": "SECURITY.md", @@ -58,23 +73,28 @@ }, { "path": "canon.lock.json", - "sha256": "sha256:3c454deb42bde8ebea9964e94a5cad738cd402708d41c8ad3afe23f0fca74758", - "size_bytes": 578 + "sha256": "sha256:1b1e4623bafc4011e161dc27f7a5bba483a9a1ecbee2eb201518600567dd7af8", + "size_bytes": 598 }, { "path": "docs/ARCHITECTURE.md", - "sha256": "sha256:dc4535e57f3e6503f8af5ec1dd19920b95a21c04e7b24cbcf66ff8fac4567c93", - "size_bytes": 382 + "sha256": "sha256:ff1443c9ead28700a2c2ddde67c339d89194700723cd8c227484f93ccdb8aadb", + "size_bytes": 901 }, { "path": "docs/CONFORMANCE.md", - "sha256": "sha256:a8465bc73fe1b133c4f0035ee4c32ed7b9e1e20ca5131ee75a6288ba07db8a3a", - "size_bytes": 386 + "sha256": "sha256:caddb7a3bc2b3f8115b87542c0466ff1267ed9a1350a0e7172d0e810d61d97a4", + "size_bytes": 1325 }, { "path": "profile/PROFILE.json", - "sha256": "sha256:4b435d75dd41b4094a2fb6830cd98fd390609045af68a03e94aaf0d2d03ad83b", - "size_bytes": 23213 + "sha256": "sha256:7beee4b3369345e94c87334c9327bea8903a64409414817b4dace82a41667a2b", + "size_bytes": 16304 + }, + { + "path": "profile/verification-map.json", + "sha256": "sha256:0f878d25fc6eed152b714f5a25dd43bae1e687665926b32dafc990296ad7e3ea", + "size_bytes": 3025 }, { "path": "pyproject.toml", @@ -346,6 +366,11 @@ "sha256": "sha256:1337dded55421f38ac071d30d570d9598c64f1cba99370aeb58643f04fea12d0", "size_bytes": 9661 }, + { + "path": "tests/test_release_assurance.py", + "sha256": "sha256:d7303899534a14d98de4b867d728f28f271c0f3785b9a2c74ec98443c0731634", + "size_bytes": 1786 + }, { "path": "tests/test_runtime.py", "sha256": "sha256:801fbc26611f8555604879e2ee379d18777af1442185cfcf04ec7cacf04c5296", @@ -356,15 +381,30 @@ "sha256": "sha256:49320eff4a7bc29faa0fc73b9831000e0d5586a821c953a1e382d5ad9f46a81d", "size_bytes": 293 }, + { + "path": "tools/blackbox_release_audit.py", + "sha256": "sha256:e1da402786d84357003606287675d09e642579807ec38699a0f315920e75331b", + "size_bytes": 12900 + }, + { + "path": "tools/build_release.py", + "sha256": "sha256:5fda7bcc5e2f4c79c91b8d26c2be9972187312fea364986b043767d066b8eeb0", + "size_bytes": 2069 + }, { "path": "tools/profile_gate.py", - "sha256": "sha256:5b5f0656f0b70cd017578c08df952147466b24fdadbf7f140af6a3c76c2b3be2", - "size_bytes": 1018 + "sha256": "sha256:0fa84f7cd4f6e24b6c16a25078ac0bf23707e768d73a9edee761cee84d0162a9", + "size_bytes": 2663 }, { "path": "tools/rebuild_manifest.py", - "sha256": "sha256:8e3745bd224509a51f94dcfbe1574e8d5328d4e68d833bb23511806adc229c90", - "size_bytes": 1355 + "sha256": "sha256:8ffc1090eee86df1327fc1fa71022825d86b006e886444138af782f6c5892b46", + "size_bytes": 1442 + }, + { + "path": "tools/validate_profile_traceability.py", + "sha256": "sha256:26655a2ce9a39b0072af5ed47d602815ec0820b3597a1e841df437d2bfb62153", + "size_bytes": 4406 }, { "path": "tools/verify_canon_lock.py", @@ -372,6 +412,6 @@ "size_bytes": 1103 } ], - "files_count": 74, + "files_count": 82, "schema_version": 1 } diff --git a/ORIGIN.json b/ORIGIN.json index d546397..3ea0cf5 100644 --- a/ORIGIN.json +++ b/ORIGIN.json @@ -1,7 +1,7 @@ { "creator": "Dzmitry Prychyna", "derived_paths": [ - "src/aset_seed", + "src/aset_python_sqlite", "tests/test_runtime.py", "tests/test_prefreeze_hardening.py", "tests/test_cli_security.py" @@ -11,9 +11,14 @@ "public_pseudonym": "Attractor Set", "rights_transfer": "NONE", "schema_version": 1, + "separation_statement": "The independent profile preserves provenance to the exact source archive while its current semantics are governed only by canon.lock.json and the external ASET conformance verdict.", "source_archive": "ASET-main(10)(1).zip", "source_archive_sha256": "sha256:223f19a79287475e87fbf4841a86735d1a41d7d0574b6508ca66991a07b979bd", + "source_manifest_files_count": 708, + "source_manifest_path": "MANIFEST.json", + "source_manifest_sha256": "sha256:6b996d3723e80a791a0c9c6934b3530de084fed863322ef2ce7c70b9255fb315", "source_project": "ASET", "source_repository": "https://github.com/attractor-set/ASET", + "source_revision_status": "NOT_EMBEDDED_IN_SOURCE_ARCHIVE", "statement": "Repository separation and Apache-2.0 licensing do not transfer authorship or ownership of the underlying Background IP." } diff --git a/README.md b/README.md index 632ee9a..881ad18 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ python tools/profile_gate.py --canon-root ../ASET The external runner in the specification repository invokes `python -m aset_python_sqlite.adapter` and determines the conformance verdict. The adapter never declares itself conformant. +## Release assurance + +`python tools/profile_gate.py --canon-root ../ASET` is the complete profile gate. It checks the repository manifest and profile traceability, runs the unit and durability tests, verifies the exact canon lock, executes the specification-owned conformance runner, builds a deterministic source archive and wheel, installs the wheel in an isolated environment, and performs documentation and runtime black-box checks against the built artifacts. + +The resulting artifacts and reports are written under `dist/`. A release must not be published unless `PYTHON_SQLITE_PROFILE_GATE=PASS` and `BLACKBOX_RELEASE_AUDIT=PASS`. + ## Rights and licence Licensed under Apache License 2.0. Copyright and authorship remain with Dzmitry Prychyna unless expressly assigned in writing. See [`LICENSE`](LICENSE), [`NOTICE`](NOTICE), [`ORIGIN.json`](ORIGIN.json), [`BACKGROUND_IP_NOTICE.md`](BACKGROUND_IP_NOTICE.md), [`AUTHORS.md`](AUTHORS.md) and [`TRADEMARKS.md`](TRADEMARKS.md). diff --git a/README.pt-BR.md b/README.pt-BR.md index c09546e..d885c2d 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -14,3 +14,9 @@ python tools/profile_gate.py --canon-root ../ASET ``` A licença Apache 2.0 não transfere autoria nem titularidade. Consulte [`LICENSE`](LICENSE), [`NOTICE`](NOTICE), [`ORIGIN.json`](ORIGIN.json) e [`BACKGROUND_IP_NOTICE.md`](BACKGROUND_IP_NOTICE.md). + +## Garantia do release + +`python tools/profile_gate.py --canon-root ../ASET` é o gate completo do perfil. Ele verifica o manifesto e a rastreabilidade dos requisitos do perfil, executa os testes unitários e de durabilidade, verifica o canon lock exato, executa o conformance runner pertencente à especificação, constrói um arquivo-fonte determinístico e um wheel, instala o wheel em um ambiente isolado e realiza verificações de caixa-preta da documentação e do runtime sobre os artefatos construídos. + +Os artefatos e relatórios são gravados em `dist/`. Um release não deve ser publicado sem `PYTHON_SQLITE_PROFILE_GATE=PASS` e `BLACKBOX_RELEASE_AUDIT=PASS`. diff --git a/README.ru.md b/README.ru.md index 7c7e30d..14779b8 100644 --- a/README.ru.md +++ b/README.ru.md @@ -14,3 +14,9 @@ python tools/profile_gate.py --canon-root ../ASET ``` Лицензия Apache 2.0 не передаёт авторство или право собственности. См. [`LICENSE`](LICENSE), [`NOTICE`](NOTICE), [`ORIGIN.json`](ORIGIN.json) и [`BACKGROUND_IP_NOTICE.md`](BACKGROUND_IP_NOTICE.md). + +## Проверка релиза + +`python tools/profile_gate.py --canon-root ../ASET` является полным гейтом профиля. Он проверяет manifest и трассируемость требований профиля, запускает unit- и durability-тесты, проверяет точную canon lock, выполняет принадлежащий спецификации conformance runner, собирает детерминированный source archive и wheel, устанавливает wheel в изолированную среду и проводит блэк-бокс проверку документации и среды исполнения по собранным артефактам. + +Артефакты и отчёты создаются в `dist/`. Релиз нельзя публиковать без `PYTHON_SQLITE_PROFILE_GATE=PASS` и `BLACKBOX_RELEASE_AUDIT=PASS`. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9d879d5..126953e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,3 +1,10 @@ # Architecture -The package has three boundaries: the semantic core (`core.py`), the local durable profile (`runtime.py` and `store.py`), and the black-box conformance adapter (`adapter.py`). The semantic core consumes canonical documents; SQLite maps accepted transitions and audit attempts into one local transaction. Profile-specific durability does not become a Seed invariant. +The package has four explicit boundaries: + +1. the semantic core (`core.py`), which implements the currently pinned Seed semantics without claiming normative authority; +2. the local durable profile (`runtime.py` and `store.py`), which maps accepted transitions and audit attempts into one SQLite transaction; +3. the black-box conformance adapter (`adapter.py`), which returns observations to the specification-owned runner and never returns a conformance verdict; +4. the release-assurance boundary (`profile/verification-map.json` and `tools/*release*`), which binds every profile requirement to executable checks and verifies built source and wheel artifacts independently from the working tree. + +Profile-specific durability does not become a Seed invariant. The exact semantic dependency is `canon.lock.json`; historical extraction provenance is recorded separately in `ORIGIN.json`. diff --git a/docs/CONFORMANCE.md b/docs/CONFORMANCE.md index e429d49..f569481 100644 --- a/docs/CONFORMANCE.md +++ b/docs/CONFORMANCE.md @@ -1,3 +1,24 @@ -# Canon consumption +# Canon consumption and executable assurance -CI checks out the ASET specification repository, verifies [`canon.lock.json`](../canon.lock.json), and then executes the specification-owned external runner against the implementation adapter. The lock binds the implementation to exact canon-package bytes. After the specification patch is merged, pin `source.ref` to an immutable commit SHA or signed release tag. +CI checks out the exact ASET specification revision recorded in [`canon.lock.json`](../canon.lock.json), verifies the canon-package identity, and executes the specification-owned external runner against the implementation adapter. The adapter returns observations and never determines its own conformance verdict. + +The profile requirements and invariants are bound to executable checks in [`profile/verification-map.json`](../profile/verification-map.json). [`tools/validate_profile_traceability.py`](../tools/validate_profile_traceability.py) rejects missing identifiers, stale pytest references and an incomplete release-audit binding. + +The complete gate is: + +```text +python tools/profile_gate.py --canon-root ../ASET +``` + +It requires, in order: + +1. exact repository-manifest parity; +2. complete profile requirement/invariant traceability; +3. unit, durability and boundary tests; +4. exact canon-lock verification; +5. external semantic conformance; +6. deterministic source-archive construction; +7. wheel construction and isolated installation; +8. black-box documentation and runtime verification from the built artifacts. + +The source archive, wheel and reports are generated under `dist/`. `ASET-PYSQL-REQ-012` is satisfied only when the black-box release audit returns `PASS`. diff --git a/profile/PROFILE.json b/profile/PROFILE.json index c45678a..363767f 100644 --- a/profile/PROFILE.json +++ b/profile/PROFILE.json @@ -25,8 +25,8 @@ "id": "ASET-PYSQL-INV-001", "texts": { "en": "Durable state and its accepted-attempt audit record commit in one transaction.", - "pt-BR": "O estado dur\u00e1vel e o registro da tentativa aceita s\u00e3o confirmados em uma \u00fanica transa\u00e7\u00e3o.", - "ru": "\u0423\u0441\u0442\u043e\u0439\u0447\u0438\u0432\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0438 \u0437\u0430\u043f\u0438\u0441\u044c \u043f\u0440\u0438\u043d\u044f\u0442\u043e\u0439 \u043f\u043e\u043f\u044b\u0442\u043a\u0438 \u0444\u0438\u043a\u0441\u0438\u0440\u0443\u044e\u0442\u0441\u044f \u043e\u0434\u043d\u043e\u0439 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0435\u0439." + "pt-BR": "O estado durável e o registro da tentativa aceita são confirmados em uma única transação.", + "ru": "Устойчивое состояние и запись принятой попытки фиксируются одной транзакцией." }, "verification": [ "profile unit tests", @@ -38,8 +38,8 @@ "id": "ASET-PYSQL-INV-002", "texts": { "en": "A rejected attempt does not change the durable state root.", - "pt-BR": "Uma tentativa rejeitada n\u00e3o altera a raiz do estado dur\u00e1vel.", - "ru": "\u041e\u0442\u043a\u043b\u043e\u043d\u0451\u043d\u043d\u0430\u044f \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043a\u043e\u0440\u0435\u043d\u044c \u0443\u0441\u0442\u043e\u0439\u0447\u0438\u0432\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f." + "pt-BR": "Uma tentativa rejeitada não altera a raiz do estado durável.", + "ru": "Отклонённая попытка не изменяет корень устойчивого состояния." }, "verification": [ "profile unit tests", @@ -51,8 +51,8 @@ "id": "ASET-PYSQL-INV-003", "texts": { "en": "Every audit entry contains the hash of the previous entry for the same trust space.", - "pt-BR": "Cada entrada de auditoria cont\u00e9m o hash da entrada anterior do mesmo espa\u00e7o de confian\u00e7a.", - "ru": "\u041a\u0430\u0436\u0434\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0436\u0443\u0440\u043d\u0430\u043b\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0445\u0435\u0448 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0442\u043e\u0433\u043e \u0436\u0435 \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0430 \u0434\u043e\u0432\u0435\u0440\u0438\u044f." + "pt-BR": "Cada entrada de auditoria contém o hash da entrada anterior do mesmo espaço de confiança.", + "ru": "Каждая запись журнала содержит хеш предыдущей записи того же пространства доверия." }, "verification": [ "profile unit tests", @@ -64,8 +64,8 @@ "id": "ASET-PYSQL-INV-004", "texts": { "en": "A database is bound to one runtime profile and one store-schema version.", - "pt-BR": "Um banco de dados est\u00e1 vinculado a um perfil de runtime e uma vers\u00e3o do esquema.", - "ru": "\u0411\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u0432\u044f\u0437\u0430\u043d\u0430 \u0441 \u043e\u0434\u043d\u0438\u043c \u043f\u0440\u043e\u0444\u0438\u043b\u0435\u043c \u0438\u0441\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0438 \u043e\u0434\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0435\u0439 \u0441\u0445\u0435\u043c\u044b \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430." + "pt-BR": "Um banco de dados está vinculado a um perfil de runtime e uma versão do esquema.", + "ru": "База данных связана с одним профилем исполнения и одной версией схемы хранилища." }, "verification": [ "profile unit tests", @@ -77,8 +77,8 @@ "id": "ASET-PYSQL-INV-005", "texts": { "en": "Without positive proof verification, a transition does not reach the semantic core.", - "pt-BR": "Sem verifica\u00e7\u00e3o positiva da prova, uma transi\u00e7\u00e3o n\u00e3o alcan\u00e7a o n\u00facleo sem\u00e2ntico.", - "ru": "\u0411\u0435\u0437 \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0434\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u0430 \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u043d\u0435 \u0434\u043e\u0441\u0442\u0438\u0433\u0430\u0435\u0442 \u0441\u0435\u043c\u0430\u043d\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u044f\u0434\u0440\u0430." + "pt-BR": "Sem verificação positiva da prova, uma transição não alcança o núcleo semântico.", + "ru": "Без положительной проверки доказательства переход не достигает семантического ядра." }, "verification": [ "profile unit tests", @@ -90,8 +90,8 @@ "id": "ASET-PYSQL-INV-006", "texts": { "en": "Reopening the store preserves the same state root and a valid audit chain.", - "pt-BR": "A reabertura do armazenamento preserva a mesma raiz de estado e uma cadeia de auditoria v\u00e1lida.", - "ru": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0435 \u043e\u0442\u043a\u0440\u044b\u0442\u0438\u0435 \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u0442\u043e\u0442 \u0436\u0435 \u043a\u043e\u0440\u0435\u043d\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u0443\u044e \u0446\u0435\u043f\u043e\u0447\u043a\u0443 \u0430\u0443\u0434\u0438\u0442\u0430." + "pt-BR": "A reabertura do armazenamento preserva a mesma raiz de estado e uma cadeia de auditoria válida.", + "ru": "Повторное открытие хранилища сохраняет тот же корень состояния и действительную цепочку аудита." }, "verification": [ "profile unit tests", @@ -103,8 +103,8 @@ "id": "ASET-PYSQL-INV-007", "texts": { "en": "A backup passes SQLite integrity_check and contains the same store profile.", - "pt-BR": "Um backup passa no SQLite integrity_check e cont\u00e9m o mesmo perfil de armazenamento.", - "ru": "\u0420\u0435\u0437\u0435\u0440\u0432\u043d\u0430\u044f \u043a\u043e\u043f\u0438\u044f \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 SQLite integrity_check \u0438 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0442\u043e\u0442 \u0436\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430." + "pt-BR": "Um backup passa no SQLite integrity_check e contém o mesmo perfil de armazenamento.", + "ru": "Резервная копия проходит SQLite integrity_check и содержит тот же профиль хранилища." }, "verification": [ "profile unit tests", @@ -116,8 +116,8 @@ "id": "ASET-PYSQL-INV-008", "texts": { "en": "The public runtime performs no external effects beyond result recognition.", - "pt-BR": "O runtime p\u00fablico n\u00e3o executa efeitos externos al\u00e9m do reconhecimento do resultado.", - "ru": "\u041f\u0443\u0431\u043b\u0438\u0447\u043d\u0430\u044f \u0441\u0440\u0435\u0434\u0430 \u0438\u0441\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u043d\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442 \u0432\u043d\u0435\u0448\u043d\u0438\u0435 \u044d\u0444\u0444\u0435\u043a\u0442\u044b \u0437\u0430 \u0433\u0440\u0430\u043d\u0438\u0446\u0435\u0439 \u043f\u0440\u0438\u0437\u043d\u0430\u043d\u0438\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430." + "pt-BR": "O runtime público não executa efeitos externos além do reconhecimento do resultado.", + "ru": "Публичная среда исполнения не выполняет внешние эффекты за границей признания результата." }, "verification": [ "profile unit tests", @@ -138,7 +138,7 @@ "texts": { "en": "The runtime MUST require an explicitly configured proof verifier and fail closed when it is absent.", "pt-BR": "O runtime DEVE exigir um verificador de provas configurado explicitamente e falhar de modo fechado quando ele estiver ausente.", - "ru": "\u0418\u0441\u043f\u043e\u043b\u043d\u044f\u0435\u043c\u0430\u044f \u0441\u0440\u0435\u0434\u0430 \u0414\u041e\u041b\u0416\u041d\u0410 \u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c \u044f\u0432\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u044e\u0449\u0438\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c \u0434\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u044c\u0441\u0442\u0432 \u0438 \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0442\u044c\u0441\u044f \u043f\u0440\u0438 \u0435\u0433\u043e \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0438." + "ru": "Исполняемая среда ДОЛЖНА требовать явно настроенный проверяющий механизм доказательств и закрываться при его отсутствии." }, "verification": [ "profile unit tests", @@ -155,8 +155,8 @@ "subject": "seed.runtime_store", "texts": { "en": "State mutation and audit recording MUST commit in one durable-store transaction.", - "pt-BR": "A altera\u00e7\u00e3o do estado e o registro de auditoria DEVEM ser confirmados em uma \u00fanica transa\u00e7\u00e3o dur\u00e1vel.", - "ru": "\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438 \u0437\u0430\u043f\u0438\u0441\u044c \u0436\u0443\u0440\u043d\u0430\u043b\u0430 \u0414\u041e\u041b\u0416\u041d\u042b \u0444\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u043e\u0434\u043d\u043e\u0439 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0435\u0439 \u0443\u0441\u0442\u043e\u0439\u0447\u0438\u0432\u043e\u0433\u043e \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430." + "pt-BR": "A alteração do estado e o registro de auditoria DEVEM ser confirmados em uma única transação durável.", + "ru": "Изменение состояния и запись журнала ДОЛЖНЫ фиксироваться одной транзакцией устойчивого хранилища." }, "verification": [ "profile unit tests", @@ -173,8 +173,8 @@ "subject": "seed.runtime_store", "texts": { "en": "Concurrent writers to one store MUST be serialized before state read and mutation.", - "pt-BR": "Escritores concorrentes de um mesmo armazenamento DEVEM ser serializados antes da leitura e altera\u00e7\u00e3o do estado.", - "ru": "\u041a\u043e\u043d\u043a\u0443\u0440\u0438\u0440\u0443\u044e\u0449\u0438\u0435 \u043f\u0438\u0441\u0430\u0442\u0435\u043b\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430 \u0414\u041e\u041b\u0416\u041d\u042b \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043e \u0447\u0442\u0435\u043d\u0438\u044f \u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f." + "pt-BR": "Escritores concorrentes de um mesmo armazenamento DEVEM ser serializados antes da leitura e alteração do estado.", + "ru": "Конкурирующие писатели одного хранилища ДОЛЖНЫ сериализоваться до чтения и изменения состояния." }, "verification": [ "profile unit tests", @@ -191,8 +191,8 @@ "subject": "seed.runtime_store", "texts": { "en": "The SQLite profile MUST use WAL, synchronous=FULL, and integrity checking.", - "pt-BR": "O perfil SQLite DEVE usar WAL, synchronous=FULL e verifica\u00e7\u00e3o de integridade.", - "ru": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c SQLite \u0414\u041e\u041b\u0416\u0415\u041d \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c WAL, synchronous=FULL \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0446\u0435\u043b\u043e\u0441\u0442\u043d\u043e\u0441\u0442\u0438." + "pt-BR": "O perfil SQLite DEVE usar WAL, synchronous=FULL e verificação de integridade.", + "ru": "Профиль SQLite ДОЛЖЕН использовать WAL, synchronous=FULL и проверку целостности." }, "verification": [ "profile unit tests", @@ -209,8 +209,8 @@ "subject": "seed.runtime", "texts": { "en": "The embedded runtime MUST reject non-JSON values with a stable code before transition admission; strict JSON documents MUST reject duplicate members.", - "pt-BR": "O runtime incorporado DEVE rejeitar valores n\u00e3o JSON com um c\u00f3digo est\u00e1vel antes da admiss\u00e3o da transi\u00e7\u00e3o; documentos JSON estritos DEVEM rejeitar membros duplicados.", - "ru": "\u0412\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0430\u044f \u0441\u0440\u0435\u0434\u0430 \u0438\u0441\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u041e\u0411\u042f\u0417\u0410\u041d\u0410 \u043e\u0442\u043a\u043b\u043e\u043d\u044f\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0432\u043d\u0435 JSON \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u043c \u043a\u043e\u0434\u043e\u043c \u0434\u043e \u0434\u043e\u043f\u0443\u0441\u043a\u0430 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430; \u0441\u0442\u0440\u043e\u0433\u0438\u0435 JSON-\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u041e\u0411\u042f\u0417\u0410\u041d\u042b \u043e\u0442\u043a\u043b\u043e\u043d\u044f\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u044e\u0449\u0438\u0435\u0441\u044f \u043f\u043e\u043b\u044f." + "pt-BR": "O runtime incorporado DEVE rejeitar valores não JSON com um código estável antes da admissão da transição; documentos JSON estritos DEVEM rejeitar membros duplicados.", + "ru": "Встроенная среда исполнения ОБЯЗАНА отклонять значения вне JSON стабильным кодом до допуска перехода; строгие JSON-документы ОБЯЗАНЫ отклонять повторяющиеся поля." }, "verification": [ "profile unit tests", @@ -227,8 +227,8 @@ "subject": "seed.runtime_store", "texts": { "en": "Every JSON transition document targeting an existing trust space MUST be appended to its hash-chained audit log; oversized documents MAY be represented by their exact digest and byte size.", - "pt-BR": "Cada documento JSON de transi\u00e7\u00e3o destinado a um espa\u00e7o de confian\u00e7a existente DEVE ser adicionado ao seu log encadeado por hash; documentos excedentes PODEM ser representados pelo resumo exato e tamanho em bytes.", - "ru": "\u041a\u0430\u0436\u0434\u044b\u0439 JSON-\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430, \u0430\u0434\u0440\u0435\u0441\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u043c\u0443 \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0443 \u0434\u043e\u0432\u0435\u0440\u0438\u044f, \u041e\u0411\u042f\u0417\u0410\u041d \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c\u0441\u044f \u0432 \u0435\u0433\u043e \u0445\u0435\u0448-\u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0439 \u0436\u0443\u0440\u043d\u0430\u043b; \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 \u0441\u0432\u0435\u0440\u0445 \u043b\u0438\u043c\u0438\u0442\u0430 \u041c\u041e\u0416\u0415\u0422 \u0431\u044b\u0442\u044c \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0442\u043e\u0447\u043d\u044b\u043c \u0445\u0435\u0448\u0435\u043c \u0438 \u0440\u0430\u0437\u043c\u0435\u0440\u043e\u043c \u0432 \u0431\u0430\u0439\u0442\u0430\u0445." + "pt-BR": "Cada documento JSON de transição destinado a um espaço de confiança existente DEVE ser adicionado ao seu log encadeado por hash; documentos excedentes PODEM ser representados pelo resumo exato e tamanho em bytes.", + "ru": "Каждый JSON-документ перехода, адресованный существующему пространству доверия, ОБЯЗАН добавляться в его хеш-связанный журнал; документ сверх лимита МОЖЕТ быть представлен точным хешем и размером в байтах." }, "verification": [ "profile unit tests", @@ -245,8 +245,8 @@ "subject": "seed.runtime_store", "texts": { "en": "The runtime MUST create a SQLite backup only after database, state, and audit-chain health validation passes.", - "pt-BR": "O runtime DEVE criar um backup SQLite somente ap\u00f3s a aprova\u00e7\u00e3o das verifica\u00e7\u00f5es de integridade do banco, estado e cadeia de auditoria.", - "ru": "\u0421\u0440\u0435\u0434\u0430 \u0438\u0441\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u041e\u0411\u042f\u0417\u0410\u041d\u0410 \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432\u043d\u0443\u044e \u043a\u043e\u043f\u0438\u044e SQLite \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0441\u043b\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u043e\u0439 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0431\u0430\u0437\u044b, \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0438 \u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u0430\u0443\u0434\u0438\u0442\u0430." + "pt-BR": "O runtime DEVE criar um backup SQLite somente após a aprovação das verificações de integridade do banco, estado e cadeia de auditoria.", + "ru": "Среда исполнения ОБЯЗАНА создавать резервную копию SQLite только после успешной проверки базы, состояния и цепочки аудита." }, "verification": [ "profile unit tests", @@ -263,8 +263,8 @@ "subject": "seed.runtime_store", "texts": { "en": "A database MUST be bound to the exact store-schema version and runtime profile.", - "pt-BR": "O banco de dados DEVE estar vinculado \u00e0 vers\u00e3o exata do esquema e ao perfil de runtime.", - "ru": "\u0411\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u0414\u041e\u041b\u0416\u041d\u0410 \u0431\u044b\u0442\u044c \u0441\u0432\u044f\u0437\u0430\u043d\u0430 \u0441 \u0442\u043e\u0447\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0435\u0439 \u0441\u0445\u0435\u043c\u044b \u0438 \u043f\u0440\u043e\u0444\u0438\u043b\u0435\u043c \u0441\u0440\u0435\u0434\u044b \u0438\u0441\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f." + "pt-BR": "O banco de dados DEVE estar vinculado à versão exata do esquema e ao perfil de runtime.", + "ru": "База данных ДОЛЖНА быть связана с точной версией схемы и профилем среды исполнения." }, "verification": [ "profile unit tests", @@ -281,8 +281,8 @@ "subject": "seed.runtime", "texts": { "en": "The runtime MUST expose state validation, database integrity, and audit-chain health checks.", - "pt-BR": "O runtime DEVE expor valida\u00e7\u00e3o de estado, integridade do banco e verifica\u00e7\u00e3o da cadeia de auditoria.", - "ru": "\u0421\u0440\u0435\u0434\u0430 \u0438\u0441\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0414\u041e\u041b\u0416\u041d\u0410 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f, \u0446\u0435\u043b\u043e\u0441\u0442\u043d\u043e\u0441\u0442\u0438 \u0431\u0430\u0437\u044b \u0438 \u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u0430\u0443\u0434\u0438\u0442\u0430." + "pt-BR": "O runtime DEVE expor validação de estado, integridade do banco e verificação da cadeia de auditoria.", + "ru": "Среда исполнения ДОЛЖНА предоставлять проверку состояния, целостности базы и цепочки аудита." }, "verification": [ "profile unit tests", @@ -299,8 +299,8 @@ "subject": "seed.runtime", "texts": { "en": "The Seed core MUST NOT perform network, tool, or physical external effects by itself.", - "pt-BR": "O n\u00facleo Seed N\u00c3O DEVE executar por si s\u00f3 efeitos externos de rede, ferramentas ou efeitos f\u00edsicos.", - "ru": "\u042f\u0434\u0440\u043e Seed \u041d\u0415 \u0414\u041e\u041b\u0416\u041d\u041e \u0441\u0430\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0442\u044c \u0441\u0435\u0442\u044c, \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b \u0438\u043b\u0438 \u0444\u0438\u0437\u0438\u0447\u0435\u0441\u043a\u0438\u0435 \u0432\u043d\u0435\u0448\u043d\u0438\u0435 \u044d\u0444\u0444\u0435\u043a\u0442\u044b." + "pt-BR": "O núcleo Seed NÃO DEVE executar por si só efeitos externos de rede, ferramentas ou efeitos físicos.", + "ru": "Ядро Seed НЕ ДОЛЖНО само выполнять сеть, инструменты или физические внешние эффекты." }, "verification": [ "profile unit tests", @@ -317,8 +317,8 @@ "subject": "seed.runtime", "texts": { "en": "The rc12 implementation MUST preserve the acceptance and rejection of all 55 rc11 reference vectors without changing frozen bytes.", - "pt-BR": "A implementa\u00e7\u00e3o rc12 DEVE preservar a aceita\u00e7\u00e3o e rejei\u00e7\u00e3o dos 55 vetores de refer\u00eancia rc11 sem alterar os bytes congelados.", - "ru": "\u0420\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f rc12 \u0414\u041e\u041b\u0416\u041d\u0410 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u043f\u0440\u0438\u043d\u044f\u0442\u0438\u0435 \u0438 \u043e\u0442\u043a\u043b\u043e\u043d\u0435\u043d\u0438\u0435 \u0432\u0441\u0435\u0445 55 \u044d\u0442\u0430\u043b\u043e\u043d\u043d\u044b\u0445 \u0432\u0435\u043a\u0442\u043e\u0440\u043e\u0432 rc11 \u0431\u0435\u0437 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u0437\u0430\u043c\u043e\u0440\u043e\u0436\u0435\u043d\u043d\u044b\u0445 \u0431\u0430\u0439\u0442\u043e\u0432." + "pt-BR": "A implementação rc12 DEVE preservar a aceitação e rejeição dos 55 vetores de referência rc11 sem alterar os bytes congelados.", + "ru": "Реализация rc12 ДОЛЖНА сохранять принятие и отклонение всех 55 эталонных векторов rc11 без изменения замороженных байтов." }, "verification": [ "profile unit tests", @@ -335,13 +335,14 @@ "subject": "seed.release", "texts": { "en": "An rc12 candidate MUST pass an independent black-box documentation and runtime audit from the built archive.", - "pt-BR": "Um candidato rc12 DEVE passar por auditoria independente de caixa-preta da documenta\u00e7\u00e3o e do runtime a partir do arquivo constru\u00eddo.", - "ru": "\u041a\u0430\u043d\u0434\u0438\u0434\u0430\u0442 rc12 \u0414\u041e\u041b\u0416\u0415\u041d \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442\u044c \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u0443\u044e \u0431\u043b\u044d\u043a-\u0431\u043e\u043a\u0441 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 \u0438 \u0441\u0440\u0435\u0434\u044b \u0438\u0441\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043e\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0430\u0440\u0445\u0438\u0432\u0430." + "pt-BR": "Um candidato rc12 DEVE passar por auditoria independente de caixa-preta da documentação e do runtime a partir do arquivo construído.", + "ru": "Кандидат rc12 ДОЛЖЕН проходить независимую блэк-бокс проверку документации и среды исполнения из собранного архива." }, "verification": [ - "profile unit tests", - "profile durability tests", - "profile gate" + "repository manifest check", + "deterministic source archive build", + "wheel build and isolated installation", + "black-box documentation and runtime audit" ] } ], diff --git a/profile/verification-map.json b/profile/verification-map.json new file mode 100644 index 0000000..d787de3 --- /dev/null +++ b/profile/verification-map.json @@ -0,0 +1,76 @@ +{ + "document_type": "aset-python-sqlite-verification-map", + "invariants": { + "ASET-PYSQL-INV-001": [ + "pytest::tests/test_runtime.py::test_hmac_transition_commits_state_and_audit_atomically" + ], + "ASET-PYSQL-INV-002": [ + "pytest::tests/test_runtime.py::test_wrong_hmac_is_rejected_without_state_change" + ], + "ASET-PYSQL-INV-003": [ + "pytest::tests/test_runtime.py::test_audit_chain_rejects_redundant_column_tampering", + "pytest::tests/test_runtime.py::test_audit_chain_is_bound_to_current_state_revision" + ], + "ASET-PYSQL-INV-004": [ + "pytest::tests/test_runtime.py::test_store_profile_mismatch_is_rejected" + ], + "ASET-PYSQL-INV-005": [ + "pytest::tests/test_runtime.py::test_default_runtime_fails_closed_on_proof" + ], + "ASET-PYSQL-INV-006": [ + "pytest::tests/test_runtime.py::test_reopen_and_backup_preserve_integrity" + ], + "ASET-PYSQL-INV-007": [ + "pytest::tests/test_prefreeze_hardening.py::test_backup_rejects_logically_invalid_state" + ], + "ASET-PYSQL-INV-008": [ + "pytest::tests/test_prefreeze_hardening.py::test_runtime_guarantees_are_profile_local" + ] + }, + "requirements": { + "ASET-PYSQL-REQ-001": [ + "pytest::tests/test_runtime.py::test_default_runtime_fails_closed_on_proof" + ], + "ASET-PYSQL-REQ-002": [ + "pytest::tests/test_runtime.py::test_hmac_transition_commits_state_and_audit_atomically" + ], + "ASET-PYSQL-REQ-003": [ + "pytest::tests/test_runtime.py::test_concurrent_replay_is_serialized" + ], + "ASET-PYSQL-REQ-004": [ + "pytest::tests/test_release_assurance.py::test_sqlite_connection_uses_wal_and_full_synchronous" + ], + "ASET-PYSQL-REQ-005": [ + "pytest::tests/test_runtime.py::test_strict_json_rejects_duplicate_members" + ], + "ASET-PYSQL-REQ-006": [ + "pytest::tests/test_runtime.py::test_audit_chain_rejects_redundant_column_tampering", + "pytest::tests/test_runtime.py::test_audit_chain_is_bound_to_current_state_revision" + ], + "ASET-PYSQL-REQ-007": [ + "pytest::tests/test_runtime.py::test_reopen_and_backup_preserve_integrity", + "pytest::tests/test_prefreeze_hardening.py::test_backup_rejects_logically_invalid_state" + ], + "ASET-PYSQL-REQ-008": [ + "pytest::tests/test_runtime.py::test_store_profile_mismatch_is_rejected" + ], + "ASET-PYSQL-REQ-009": [ + "pytest::tests/test_prefreeze_hardening.py::test_health_fails_on_schema_invalid_persisted_state", + "pytest::tests/test_prefreeze_hardening.py::test_health_fails_on_stored_root_mismatch" + ], + "ASET-PYSQL-REQ-010": [ + "pytest::tests/test_prefreeze_hardening.py::test_runtime_guarantees_are_profile_local" + ], + "ASET-PYSQL-REQ-011": [ + "external-conformance", + "pytest::tests/test_adapter.py::test_execute_case_returns_observation_not_verdict" + ], + "ASET-PYSQL-REQ-012": [ + "blackbox-release-audit", + "manifest-check", + "source-archive-build", + "wheel-build-install" + ] + }, + "schema_version": 1 +} diff --git a/tests/test_release_assurance.py b/tests/test_release_assurance.py new file mode 100644 index 0000000..252ede1 --- /dev/null +++ b/tests/test_release_assurance.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +from aset_python_sqlite.store import SqliteStore + +ROOT = Path(__file__).resolve().parents[1] + + +def test_sqlite_connection_uses_wal_and_full_synchronous(tmp_path: Path) -> None: + store = SqliteStore(tmp_path / "profile.db") + connection = store.connect() + try: + journal_mode = connection.execute("PRAGMA journal_mode").fetchone()[0] + synchronous = connection.execute("PRAGMA synchronous").fetchone()[0] + finally: + connection.close() + assert str(journal_mode).lower() == "wal" + assert synchronous == 2 + + +def test_origin_paths_are_exact_and_present() -> None: + origin = json.loads((ROOT / "ORIGIN.json").read_text(encoding="utf-8")) + assert origin["creator"] == "Dzmitry Prychyna" + assert origin["public_pseudonym"] == "Attractor Set" + for relative in origin["derived_paths"]: + assert (ROOT / relative).exists(), relative + assert "src/aset_seed" not in origin["derived_paths"] + + +def test_manifest_covers_repository_control_files() -> None: + manifest = json.loads((ROOT / "MANIFEST.json").read_text(encoding="utf-8")) + paths = {entry["path"] for entry in manifest["files"]} + assert ".github/workflows/ci.yml" in paths + assert ".github/workflows/update-canon-lock.yml" in paths + assert ".gitignore" in paths + assert "ORIGIN.json" in paths + + +def test_profile_traceability_validator_passes() -> None: + result = subprocess.run( + [sys.executable, "tools/validate_profile_traceability.py"], + cwd=ROOT, + text=True, + capture_output=True, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + assert "PROFILE_TRACEABILITY=PASS" in result.stdout diff --git a/tools/blackbox_release_audit.py b/tools/blackbox_release_audit.py new file mode 100755 index 0000000..1c2713b --- /dev/null +++ b/tools/blackbox_release_audit.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path, PurePosixPath +from typing import Any + +EXPECTED_ROOT = "aset-python-sqlite" +REQUIRED_DOCUMENTS = { + "AUTHORS.md", + "BACKGROUND_IP_NOTICE.md", + "CONTRIBUTING.md", + "LICENSE", + "MANIFEST.json", + "NOTICE", + "ORIGIN.json", + "README.md", + "README.pt-BR.md", + "README.ru.md", + "SECURITY.md", + "TRADEMARKS.md", + "canon.lock.json", + "profile/PROFILE.json", + "profile/verification-map.json", +} + + +def digest(data: bytes) -> str: + return "sha256:" + hashlib.sha256(data).hexdigest() + + +def safe_name(name: str) -> bool: + path = PurePosixPath(name) + return ( + not path.is_absolute() + and ".." not in path.parts + and bool(path.parts) + and path.parts[0] == EXPECTED_ROOT + and not name.endswith("/") + ) + + +def read_archive(path: Path) -> tuple[dict[str, bytes], list[str]]: + errors: list[str] = [] + files: dict[str, bytes] = {} + with zipfile.ZipFile(path) as archive: + infos = archive.infolist() + names = [info.filename for info in infos] + if len(names) != len(set(names)): + errors.append("duplicate archive member") + for info in infos: + if not safe_name(info.filename): + errors.append(f"unsafe archive member: {info.filename}") + continue + relative = PurePosixPath(info.filename).relative_to(EXPECTED_ROOT).as_posix() + files[relative] = archive.read(info) + return files, errors + + +def json_object(files: dict[str, bytes], relative: str, errors: list[str]) -> dict[str, Any]: + try: + value = json.loads(files[relative].decode("utf-8")) + except Exception as error: # audit boundary reports exact parse class + errors.append(f"invalid {relative}: {type(error).__name__}") + return {} + if not isinstance(value, dict): + errors.append(f"{relative} must contain an object") + return {} + return value + + +def archive_checks(files: dict[str, bytes], errors: list[str]) -> None: + missing = sorted(REQUIRED_DOCUMENTS - set(files)) + for relative in missing: + errors.append(f"missing required document: {relative}") + + if "MANIFEST.json" not in files: + return + manifest = json_object(files, "MANIFEST.json", errors) + entries = manifest.get("files") + if not isinstance(entries, list): + errors.append("manifest files must be an array") + return + expected: dict[str, dict[str, Any]] = {} + for entry in entries: + if not isinstance(entry, dict) or not isinstance(entry.get("path"), str): + errors.append("invalid manifest entry") + continue + expected[entry["path"]] = entry + actual_paths = set(files) - {"MANIFEST.json"} + if set(expected) != actual_paths: + errors.append( + "manifest scope mismatch: " + f"missing={sorted(actual_paths - set(expected))} " + f"extra={sorted(set(expected) - actual_paths)}" + ) + if manifest.get("files_count") != len(expected): + errors.append("manifest files_count mismatch") + for relative, entry in expected.items(): + data = files.get(relative) + if data is None: + continue + if entry.get("sha256") != digest(data): + errors.append(f"manifest digest mismatch: {relative}") + if entry.get("size_bytes") != len(data): + errors.append(f"manifest size mismatch: {relative}") + + +def documentation_checks(files: dict[str, bytes], errors: list[str]) -> None: + if "README.md" in files: + readme = files["README.md"].decode("utf-8") + for marker in ( + "NORMATIVE=false", + "PRODUCTION_READY=false", + "https://github.com/attractor-set/ASET", + "Implementation precedence: none", + ): + if marker not in readme: + errors.append(f"README marker missing: {marker}") + if "profile/PROFILE.json" in files: + profile = json_object(files, "profile/PROFILE.json", errors) + if profile.get("normative") is not False: + errors.append("profile normative must be false") + if profile.get("production_ready") is not False: + errors.append("profile production_ready must be false") + requirements = profile.get("profile_requirements") + if not isinstance(requirements, list) or not any( + isinstance(item, dict) and item.get("id") == "ASET-PYSQL-REQ-012" + for item in requirements + ): + errors.append("release-audit requirement missing") + if "ORIGIN.json" in files: + origin = json_object(files, "ORIGIN.json", errors) + paths = origin.get("derived_paths") + if not isinstance(paths, list) or not paths: + errors.append("ORIGIN derived_paths missing") + else: + for relative in paths: + if not isinstance(relative, str): + errors.append("ORIGIN derived path must be a string") + continue + if relative not in files and not any( + path.startswith(relative.rstrip("/") + "/") for path in files + ): + errors.append(f"ORIGIN derived path does not exist: {relative}") + + +def extract(files: dict[str, bytes], destination: Path) -> Path: + root = destination / EXPECTED_ROOT + for relative, data in files.items(): + target = root / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(data) + return root + + +def run( + command: list[str], + *, + cwd: Path, + input_text: str | None = None, + env: dict[str, str] | None = None, +) -> subprocess.CompletedProcess[str]: + return subprocess.run( + command, + cwd=cwd, + input=input_text, + text=True, + capture_output=True, + check=False, + env=env, + ) + + +def runtime_checks(files: dict[str, bytes], wheel: Path, errors: list[str]) -> None: + if not wheel.is_file(): + errors.append(f"wheel missing: {wheel}") + return + with tempfile.TemporaryDirectory(prefix="aset-pysql-blackbox-") as temp_name: + temp = Path(temp_name) + source = extract(files, temp / "source") + target = temp / "site" + install = run( + [ + sys.executable, + "-m", + "pip", + "install", + "--no-deps", + "--target", + str(target), + str(wheel.resolve()), + ], + cwd=temp, + ) + if install.returncode: + errors.append(f"wheel installation failed: {install.stderr.strip()}") + return + + environment = dict(os.environ) + environment["PYTHONPATH"] = str(target) + import_check = run( + [ + sys.executable, + "-c", + ( + "import pathlib, aset_python_sqlite; " + "print(aset_python_sqlite.__version__); " + "print(pathlib.Path(aset_python_sqlite.__file__).resolve())" + ), + ], + cwd=temp, + env=environment, + ) + import_lines = import_check.stdout.strip().splitlines() + if ( + import_check.returncode + or len(import_lines) != 2 + or import_lines[0] != "0.1.0" + or target.resolve() not in Path(import_lines[1]).parents + ): + errors.append("installed package import/version/path check failed") + + describe_request = json.dumps( + { + "protocol": "ASET-IMPLEMENTATION-CONFORMANCE-V1", + "operation": "describe", + } + ) + describe = run( + [sys.executable, "-m", "aset_python_sqlite.adapter"], + cwd=temp, + input_text=describe_request, + env=environment, + ) + if describe.returncode: + errors.append(f"installed adapter describe failed: {describe.stderr.strip()}") + else: + try: + response = json.loads(describe.stdout) + except json.JSONDecodeError: + errors.append("installed adapter describe returned invalid JSON") + else: + implementation = response.get("implementation", {}) + if implementation.get("normative") is not False: + errors.append("installed adapter claims normative status") + if "verdict" in response or "pass" in response: + errors.append("installed adapter self-declares conformance") + + fixture_path = source / "tests/fixtures/POS-001.json" + if not fixture_path.is_file(): + errors.append("positive fixture missing from source archive") + else: + case = json.loads(fixture_path.read_text(encoding="utf-8")) + execute_request = json.dumps( + { + "protocol": "ASET-IMPLEMENTATION-CONFORMANCE-V1", + "operation": "execute_case", + "case": case, + } + ) + execute = run( + [sys.executable, "-m", "aset_python_sqlite.adapter"], + cwd=temp, + input_text=execute_request, + env=environment, + ) + if execute.returncode: + errors.append(f"installed adapter execute failed: {execute.stderr.strip()}") + else: + try: + response = json.loads(execute.stdout) + except json.JSONDecodeError: + errors.append("installed adapter execute returned invalid JSON") + else: + if response.get("actual") != case.get("expected"): + errors.append("installed adapter positive observation mismatch") + if "verdict" in response or "pass" in response: + errors.append("installed adapter execute self-declares conformance") + + help_result = run( + [sys.executable, "-m", "aset_python_sqlite", "--help"], + cwd=temp, + env=environment, + ) + if help_result.returncode or "aset-python-sqlite" not in help_result.stdout: + errors.append("installed CLI help check failed") + + +def write_reports(output_json: Path, output_md: Path, errors: list[str], archive: Path, wheel: Path) -> None: + report = { + "document_type": "aset-python-sqlite-blackbox-release-audit", + "archive": archive.name, + "archive_sha256": digest(archive.read_bytes()), + "wheel": wheel.name, + "wheel_sha256": digest(wheel.read_bytes()) if wheel.is_file() else None, + "findings": errors, + "verdict": "PASS" if not errors else "FAIL", + } + output_json.parent.mkdir(parents=True, exist_ok=True) + output_json.write_text(json.dumps(report, sort_keys=True, indent=2) + "\n", encoding="utf-8") + lines = [ + "# aset-python-sqlite black-box release audit", + "", + f"- Archive: `{report['archive']}`", + f"- Archive SHA-256: `{report['archive_sha256']}`", + f"- Wheel: `{report['wheel']}`", + f"- Wheel SHA-256: `{report['wheel_sha256']}`", + f"- Verdict: **{report['verdict']}**", + "", + "## Findings", + "", + ] + lines.extend(f"- {finding}" for finding in errors) + if not errors: + lines.append("- None.") + output_md.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("archive", type=Path) + parser.add_argument("--wheel", type=Path, required=True) + parser.add_argument( + "--output-json", + type=Path, + default=Path("dist/blackbox-release-audit.json"), + ) + parser.add_argument( + "--output-md", + type=Path, + default=Path("dist/blackbox-release-audit.md"), + ) + args = parser.parse_args() + + errors: list[str] = [] + try: + files, archive_errors = read_archive(args.archive) + except (OSError, zipfile.BadZipFile) as error: + files = {} + archive_errors = [f"archive unreadable: {type(error).__name__}"] + errors.extend(archive_errors) + archive_checks(files, errors) + documentation_checks(files, errors) + runtime_checks(files, args.wheel, errors) + write_reports(args.output_json, args.output_md, errors, args.archive, args.wheel) + + print(f"BLACKBOX_RELEASE_FINDINGS={len(errors)}") + print("BLACKBOX_RELEASE_AUDIT=" + ("PASS" if not errors else "FAIL")) + return 0 if not errors else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/build_release.py b/tools/build_release.py new file mode 100755 index 0000000..c666444 --- /dev/null +++ b/tools/build_release.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import hashlib +import stat +import zipfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DIST = ROOT / "dist" +ARCHIVE = DIST / "aset-python-sqlite-source.zip" +FIXED_TIMESTAMP = (1980, 1, 1, 0, 0, 0) +EXCLUDED_PARTS = { + ".aset-spec", + ".git", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + ".tox", + ".venv", + "__pycache__", + "build", + "dist", +} + + +def included(relative: Path) -> bool: + if relative.as_posix() == ".coverage": + return False + if any(part.endswith(".egg-info") for part in relative.parts): + return False + return not any(part in EXCLUDED_PARTS for part in relative.parts) + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + while chunk := stream.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def main() -> int: + DIST.mkdir(parents=True, exist_ok=True) + files = [ + path + for path in ROOT.rglob("*") + if path.is_file() and included(path.relative_to(ROOT)) + ] + + with zipfile.ZipFile( + ARCHIVE, + "w", + compression=zipfile.ZIP_DEFLATED, + compresslevel=9, + ) as archive: + for path in sorted(files, key=lambda item: item.relative_to(ROOT).as_posix()): + relative = Path("aset-python-sqlite") / path.relative_to(ROOT) + info = zipfile.ZipInfo(relative.as_posix(), FIXED_TIMESTAMP) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = (stat.S_IFREG | 0o644) << 16 + archive.writestr(info, path.read_bytes()) + + digest = sha256_file(ARCHIVE) + checksum = ARCHIVE.with_suffix(ARCHIVE.suffix + ".sha256") + checksum.write_text( + f"{digest} {ARCHIVE.name}\n", + encoding="utf-8", + newline="\n", + ) + print(f"ARCHIVE={ARCHIVE}") + print(f"SHA256={digest}") + print("SOURCE_ARCHIVE_BUILD=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/profile_gate.py b/tools/profile_gate.py old mode 100644 new mode 100755 index f1eb719..ea5ae05 --- a/tools/profile_gate.py +++ b/tools/profile_gate.py @@ -1,18 +1,102 @@ #!/usr/bin/env python3 from __future__ import annotations -import argparse,json,subprocess,sys + +import argparse +import json +import shutil +import subprocess +import sys from pathlib import Path -ROOT=Path(__file__).resolve().parents[1] -def run(args): - r=subprocess.run([sys.executable,*args],cwd=ROOT,check=False) - if r.returncode: raise SystemExit(r.returncode) -def main(): - ap=argparse.ArgumentParser(); ap.add_argument('--canon-root',type=Path); a=ap.parse_args() - profile=json.loads((ROOT/'profile/PROFILE.json').read_text()) - assert profile['normative'] is False and profile['production_ready'] is False - run(['-m','pytest','-q']) - if a.canon_root: - run(['tools/verify_canon_lock.py','--canon-root',str(a.canon_root)]) - run([str(a.canon_root/'tools/run_external_conformance.py'),'--canon-root',str(a.canon_root),'--adapter',f'{sys.executable} {ROOT / "tools/adapter_entry.py"}','--adapter-cwd',str(ROOT),'--output','dist/conformance-results.json']) - print('PYTHON_SQLITE_PROFILE_GATE=PASS'); return 0 -if __name__=='__main__': raise SystemExit(main()) + +ROOT = Path(__file__).resolve().parents[1] +DIST = ROOT / "dist" + + +def run(arguments: list[str]) -> None: + result = subprocess.run( + [sys.executable, *arguments], + cwd=ROOT, + check=False, + ) + if result.returncode: + raise SystemExit(result.returncode) + + +def build_wheel() -> Path: + wheel_dir = DIST / "wheel" + if wheel_dir.exists(): + shutil.rmtree(wheel_dir) + wheel_dir.mkdir(parents=True) + result = subprocess.run( + [ + sys.executable, + "-m", + "pip", + "wheel", + "--no-deps", + "--no-build-isolation", + "--wheel-dir", + str(wheel_dir), + ".", + ], + cwd=ROOT, + check=False, + ) + if result.returncode: + raise SystemExit(result.returncode) + wheels = sorted(wheel_dir.glob("aset_python_sqlite-*.whl")) + if len(wheels) != 1: + print(f"WHEEL_COUNT={len(wheels)}") + raise SystemExit(1) + print(f"WHEEL={wheels[0]}") + print("WHEEL_BUILD=PASS") + return wheels[0] + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--canon-root", type=Path) + args = parser.parse_args() + + profile = json.loads((ROOT / "profile/PROFILE.json").read_text(encoding="utf-8")) + if profile["normative"] is not False or profile["production_ready"] is not False: + print("PROFILE_STATUS=INVALID") + return 1 + + run(["tools/rebuild_manifest.py", "--check"]) + run(["tools/validate_profile_traceability.py"]) + run(["-m", "pytest", "-q"]) + + if args.canon_root: + canon_root = args.canon_root.resolve() + run(["tools/verify_canon_lock.py", "--canon-root", str(canon_root)]) + run( + [ + str(canon_root / "tools/run_external_conformance.py"), + "--canon-root", + str(canon_root), + "--adapter", + f'{sys.executable} {ROOT / "tools/adapter_entry.py"}', + "--adapter-cwd", + str(ROOT), + "--output", + "dist/conformance-results.json", + ] + ) + + run(["tools/build_release.py"]) + wheel = build_wheel() + run( + [ + "tools/blackbox_release_audit.py", + "dist/aset-python-sqlite-source.zip", + "--wheel", + str(wheel), + ] + ) + print("PYTHON_SQLITE_PROFILE_GATE=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/rebuild_manifest.py b/tools/rebuild_manifest.py index b1f569d..eacd19b 100644 --- a/tools/rebuild_manifest.py +++ b/tools/rebuild_manifest.py @@ -5,9 +5,9 @@ import json from pathlib import Path ROOT=Path(__file__).resolve().parents[1] -EXCLUDED={'.git','.venv','__pycache__','.pytest_cache','dist','build'} +EXCLUDED={'.aset-spec','.git','.venv','__pycache__','.pytest_cache','.ruff_cache','.mypy_cache','.tox','dist','build'} def included(path:Path)->bool: - return not any(part in EXCLUDED or part.endswith('.egg-info') for part in path.parts) and path.name!='MANIFEST.json' + return (path.as_posix() != '.coverage' and not any(part in EXCLUDED or part.endswith('.egg-info') for part in path.parts) and path.name != 'MANIFEST.json') def expected(): files=[] for path in sorted(ROOT.rglob('*')): diff --git a/tools/validate_profile_traceability.py b/tools/validate_profile_traceability.py new file mode 100755 index 0000000..8c51531 --- /dev/null +++ b/tools/validate_profile_traceability.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +PROFILE = ROOT / "profile/PROFILE.json" +MAP = ROOT / "profile/verification-map.json" +ALLOWED_GATE_CHECKS = { + "blackbox-release-audit", + "canon-lock-verification", + "external-conformance", + "manifest-check", + "source-archive-build", + "wheel-build-install", +} + + +def load(path: Path) -> dict[str, object]: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"{path.relative_to(ROOT)} must contain an object") + return value + + +def python_tests(path: Path) -> set[str]: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + return { + node.name + for node in tree.body + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + and node.name.startswith("test_") + } + + +def validate_check(check: str) -> str | None: + if check in ALLOWED_GATE_CHECKS: + return None + prefix = "pytest::" + if not check.startswith(prefix): + return f"unknown check identifier: {check}" + payload = check[len(prefix) :] + if "::" not in payload: + return f"invalid pytest check: {check}" + relative, function = payload.rsplit("::", 1) + path = ROOT / relative + if not path.is_file(): + return f"missing pytest file: {relative}" + if function not in python_tests(path): + return f"missing pytest function: {relative}::{function}" + return None + + +def main() -> int: + profile = load(PROFILE) + mapping = load(MAP) + errors: list[str] = [] + + expected_requirements = { + item["id"] for item in profile["profile_requirements"] + } + expected_invariants = { + item["id"] for item in profile["profile_invariants"] + } + requirement_map = mapping.get("requirements") + invariant_map = mapping.get("invariants") + if not isinstance(requirement_map, dict): + errors.append("requirements mapping missing") + requirement_map = {} + if not isinstance(invariant_map, dict): + errors.append("invariants mapping missing") + invariant_map = {} + + actual_requirements = set(requirement_map) + actual_invariants = set(invariant_map) + if actual_requirements != expected_requirements: + errors.append( + "requirement mapping mismatch: " + f"missing={sorted(expected_requirements - actual_requirements)} " + f"extra={sorted(actual_requirements - expected_requirements)}" + ) + if actual_invariants != expected_invariants: + errors.append( + "invariant mapping mismatch: " + f"missing={sorted(expected_invariants - actual_invariants)} " + f"extra={sorted(actual_invariants - expected_invariants)}" + ) + + for category, entries in ( + ("requirement", requirement_map), + ("invariant", invariant_map), + ): + for identifier, checks in entries.items(): + if not isinstance(checks, list) or not checks: + errors.append(f"{category} {identifier} has no checks") + continue + for check in checks: + if not isinstance(check, str): + errors.append(f"{category} {identifier} contains non-string check") + continue + error = validate_check(check) + if error: + errors.append(f"{category} {identifier}: {error}") + + release_checks = set(requirement_map.get("ASET-PYSQL-REQ-012", [])) + required_release_checks = { + "blackbox-release-audit", + "manifest-check", + "source-archive-build", + "wheel-build-install", + } + if not required_release_checks.issubset(release_checks): + errors.append( + "ASET-PYSQL-REQ-012 does not bind the complete release gate: " + f"missing={sorted(required_release_checks - release_checks)}" + ) + + if errors: + for error in errors: + print(f"PROFILE_TRACEABILITY_ERROR={error}") + print("PROFILE_TRACEABILITY=FAIL") + return 1 + + print(f"PROFILE_REQUIREMENTS_TRACED={len(expected_requirements)}") + print(f"PROFILE_INVARIANTS_TRACED={len(expected_invariants)}") + print("PROFILE_TRACEABILITY=PASS") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())