From 54d624465386a13bb99eba3fa09a1340b0b5d4c6 Mon Sep 17 00:00:00 2001 From: THOTH Date: Tue, 20 Oct 2026 04:27:17 +0900 Subject: [PATCH] Close a stuck request on retry when its local owner process is gone A short command interrupted when THOTH exited stayed RUNNING forever; sending the same request again only replayed RUNNING. In local mode, repeating the same request now closes it when that is safe. Owner record - New table operation_owners (one migration, existing tables and rows untouched): worker id, process id, process marker and a host token, written in the same transaction as the claim. It is internal only and never appears in GET or RPC responses or events. - The host token is an HMAC of the OS tag, machine id and boot id; only the hash is stored. If any input cannot be read, no owner row is written. Hosted review mode reads nothing. Closing on retry - Only for the 67 commands measured to write nothing when repeated, an operation with an owner row, the same machine and boot, an owning worker confirmed dead (alive or unknown never closes), and while this process holds the workspace lock. - The operation is re-read and closed in one transaction with its binding and operation.failed event, after the access check. If the original handler completed first, its stored result is replayed. The answer is OPERATION_OWNER_GONE with remote_observation UNKNOWN and one Korean and one English line asking the user to check whether the change was saved before sending a new request. - No time-based expiry, automatic rerun or sweep. Research commands, commands outside the list, old rows and hosted review are unchanged. - When completion or failure loses the compare-and-set to another writer, the command bus now replays the stored winner instead of appending its own succeeded or failed events. - docs/KNOWN_LIMITATIONS describes the scope and the case where a saved change may miss its notification when the close wins. Verification: the full local check (Python 5345 tests, web tests, lint, typecheck, build, architecture and module budget, README smoke) passed with no failures, including real child process kill tests and the completion race tests. --- SOURCE_MANIFEST.json | 193 +- config/module-responsibility-budget.json | 2 +- docs/KNOWN_LIMITATIONS.ko.md | 26 +- docs/KNOWN_LIMITATIONS.md | 27 +- migrations/env.py | 1 + .../versions/4c17f5089ea3_operation_owners.py | 24 + src/thoth/adapters/operation_host_identity.py | 104 + .../storage/operation_owner_schema.py | 15 + .../adapters/storage/operation_owner_store.py | 30 + src/thoth/adapters/storage/operations.py | 9 +- src/thoth/adapters/storage/workspace_owner.py | 22 + src/thoth/apps/resource_scope_composition.py | 7 +- src/thoth/apps/runtime_entry.py | 21 + src/thoth/domain/operation.py | 10 +- src/thoth/protocol/bus.py | 51 +- src/thoth/protocol/operation_claim.py | 16 + src/thoth/protocol/operation_owner.py | 217 ++ tests/atomicity/harness.py | 62 + tests/fixtures/sealable_methods.json | 78 + tests/fixtures/storage_migration_ddl.json | 14 +- tests/fixtures/storage_schema_contract.json | 3443 ++++++++++++++++- tests/integration/migration_head_helpers.py | 4 +- tests/integration/schema_contract_probe.py | 2 + tests/integration/storage_coverage_helpers.py | 1 + .../test_action_step_add_step_id.py | 12 +- .../test_d1_object_followup_atomicity.py | 6 +- .../test_m3_action_authorization_atomicity.py | 26 +- .../test_m3_action_mutation_atomicity.py | 26 +- .../test_m3_execution_effect_atomicity.py | 26 +- .../test_m3_execution_revision_atomicity.py | 26 +- .../test_m3_object_mutation_atomicity.py | 20 +- .../test_m3_outcome_child_atomicity.py | 45 +- .../test_m3_outcome_revision_atomicity.py | 26 +- ...4_memory_control_multiwrite_observation.py | 32 +- .../test_operation_owner_atomicity.py | 98 + .../test_operation_owner_completion_race.py | 177 + .../test_operation_owner_migration.py | 118 + .../test_operation_owner_recovery.py | 169 + .../test_operation_owner_storage.py | 231 ++ ...st_resource_scope_transition_boundaries.py | 95 +- tests/integration/test_revision_full_rpc.py | 7 + .../test_stale_running_current_behavior.py | 99 +- tests/integration/test_workspace_owner_pin.py | 46 + tests/unit/test_operation_host_identity.py | 204 + tests/unit/test_operation_owner_allowances.py | 139 + tests/unit/test_operation_owner_policy.py | 180 + 46 files changed, 5979 insertions(+), 208 deletions(-) create mode 100644 migrations/versions/4c17f5089ea3_operation_owners.py create mode 100644 src/thoth/adapters/operation_host_identity.py create mode 100644 src/thoth/adapters/storage/operation_owner_schema.py create mode 100644 src/thoth/adapters/storage/operation_owner_store.py create mode 100644 src/thoth/protocol/operation_owner.py create mode 100644 tests/fixtures/sealable_methods.json create mode 100644 tests/integration/test_operation_owner_atomicity.py create mode 100644 tests/integration/test_operation_owner_completion_race.py create mode 100644 tests/integration/test_operation_owner_migration.py create mode 100644 tests/integration/test_operation_owner_recovery.py create mode 100644 tests/integration/test_operation_owner_storage.py create mode 100644 tests/integration/test_workspace_owner_pin.py create mode 100644 tests/unit/test_operation_host_identity.py create mode 100644 tests/unit/test_operation_owner_allowances.py create mode 100644 tests/unit/test_operation_owner_policy.py diff --git a/SOURCE_MANIFEST.json b/SOURCE_MANIFEST.json index ee37916..5c8f14a 100644 --- a/SOURCE_MANIFEST.json +++ b/SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "schema_version": "thoth-source-preview-manifest-v1", - "created_at": "2026-10-19T10:25:51.945157+00:00", + "created_at": "2026-10-19T13:28:27.492646+00:00", "license": "MIT", "git_history_included": false, "full_suite_verified": false, @@ -1878,7 +1878,7 @@ }, { "path": "config/module-responsibility-budget.json", - "sha256": "428bab7a87160af210a40f3d8c5a8762aa588981bdbbc07cf53c8509f83b7195", + "sha256": "2c8eee608d261d612e04010f1e3e18bd1df3549a91b91d323beee60316924f58", "bytes": 8580 }, { @@ -1948,13 +1948,13 @@ }, { "path": "docs/KNOWN_LIMITATIONS.ko.md", - "sha256": "d8b0bd6598ae913e90c5144b163d6938009845e06c179ca21e56f0cd24b4e33f", - "bytes": 8165 + "sha256": "777febcf50b13b2878658fe826e34c0230d3d9930efbf9cea72897facfcefe60", + "bytes": 8864 }, { "path": "docs/KNOWN_LIMITATIONS.md", - "sha256": "1d782ae99f440fdd981ae120ab33592b8f90d6146d4b47f07d2bb314fac3398e", - "bytes": 7100 + "sha256": "f294c9aaaefcca87c0d61f9704d0bbf3a9a97568bcf00570467eec74128ce113", + "bytes": 7626 }, { "path": "docs/KNOWN_TEST_FAILURES.json", @@ -2508,8 +2508,8 @@ }, { "path": "migrations/env.py", - "sha256": "feb20ec5fb10d3cb726e07d14b1b6c5ae413a18b1072f5de787c930ad3555c54", - "bytes": 1820 + "sha256": "34cf2a8e8da9eeecd7771517a2a8f4cb828404710abea66e502a491e065e4396", + "bytes": 1904 }, { "path": "migrations/script.py.mako", @@ -2561,6 +2561,11 @@ "sha256": "613e8a6fb5593ebb8fea445bd1717211f6c4fdc6d69176e8099c166190a4b9fd", "bytes": 3880 }, + { + "path": "migrations/versions/4c17f5089ea3_operation_owners.py", + "sha256": "11e8a4955e3a8515118e907bbda150856a27b71a8369a10874f1624b15c02781", + "bytes": 702 + }, { "path": "migrations/versions/4edb1326e7ba_control_records.py", "sha256": "24681b809904acd6ff0988ecf6697a44aa35eb73eb822a62833276e7cda97f99", @@ -3731,6 +3736,11 @@ "sha256": "3600657f686c6f21b351205969dfdfd6e5dca7bc5413d738b4d762c1d25479df", "bytes": 9429 }, + { + "path": "src/thoth/adapters/operation_host_identity.py", + "sha256": "35342a6370b5fd7cfaa22b0130bdab496fc5c2fe45f5f843d20706e168e827a9", + "bytes": 3277 + }, { "path": "src/thoth/adapters/parsers/__init__.py", "sha256": "afd09e7f809b052ac67df1bad9d16f4bee4fb3ecce7e6cfdf5b7447f2efda6a8", @@ -4111,10 +4121,20 @@ "sha256": "60ff045f20b5a50cb3e6bf826d1494272e80ea9c5e93a59954726b54917fb5fe", "bytes": 2463 }, + { + "path": "src/thoth/adapters/storage/operation_owner_schema.py", + "sha256": "b3ff285f2e632dd1c0c223b3292606bd531bc7b096d28f83927b141e178c5a7d", + "bytes": 530 + }, + { + "path": "src/thoth/adapters/storage/operation_owner_store.py", + "sha256": "4b6e37108fe02464410bf56a4efd7b2f056a440f9047745776fcd79225970050", + "bytes": 1024 + }, { "path": "src/thoth/adapters/storage/operations.py", - "sha256": "a69dc2b43fdab8bcdf3fb3962e95e9979205fa17e9e38aa6f1282e697b52ec8d", - "bytes": 17632 + "sha256": "88eadd4ad1e2ba972821f30b11bcbf014d3d131f92c57c73d07b97108e33a2e7", + "bytes": 18053 }, { "path": "src/thoth/adapters/storage/outcome.py", @@ -4218,8 +4238,8 @@ }, { "path": "src/thoth/adapters/storage/workspace_owner.py", - "sha256": "39e1eb1688ea5ed18d658d7f4cb3dde7e1950d130111e0e213eeef628d963e42", - "bytes": 6348 + "sha256": "ec0f5d99b5e71ba00fc16f988ace72779788cdaa0a9f1ac011646ebf5f213584", + "bytes": 6935 }, { "path": "src/thoth/adapters/storage/workspace_setup.py", @@ -6168,8 +6188,8 @@ }, { "path": "src/thoth/apps/resource_scope_composition.py", - "sha256": "c43ba984adac460a0add497ead45591c7cea145e8d9e4102d1e912aaaac8ad41", - "bytes": 6964 + "sha256": "f29ad10251d86251ac70f5c6330ef08db1fefa631b3ad66c7e8ed41d0bdfca5a", + "bytes": 7204 }, { "path": "src/thoth/apps/restore_profiles.py", @@ -6188,8 +6208,8 @@ }, { "path": "src/thoth/apps/runtime_entry.py", - "sha256": "981d5168692375e90e79e62cabe2c7f182ef4368791d32121d0b5541ee818d3a", - "bytes": 5011 + "sha256": "b9bc5d885152e0fe4bc084678b8f17810facb37313a4c46651f1ce481413b50d", + "bytes": 6005 }, { "path": "src/thoth/apps/runtime_registration.py", @@ -6638,8 +6658,8 @@ }, { "path": "src/thoth/domain/operation.py", - "sha256": "4cd49438c78a9ab1f7690a02d8a130d5a5d0de1a8fe31054dd80971a93194fb1", - "bytes": 1614 + "sha256": "3838538ce23dfb081da960e2444421f5114a20ec264e068baf972aeeea95ee8b", + "bytes": 1934 }, { "path": "src/thoth/domain/outcome.py", @@ -7203,8 +7223,8 @@ }, { "path": "src/thoth/protocol/bus.py", - "sha256": "96b88ef685f671415e29cdee5a6b5fdd864181da49856bf06f31e0eb42edf780", - "bytes": 31115 + "sha256": "5a6f0012d36f4e719080566385916ce7441977baaaaaa6127f970b8b2f2d06a7", + "bytes": 31488 }, { "path": "src/thoth/protocol/deferred.py", @@ -7233,8 +7253,13 @@ }, { "path": "src/thoth/protocol/operation_claim.py", - "sha256": "7d959c150a371bb2a0e9e2477c50fef1aecc4c5011975809e3531962ee987891", - "bytes": 583 + "sha256": "da6237d8d5619e85cc650b2944730221ac29cb7c15e44d3988658f9c91a37554", + "bytes": 1121 + }, + { + "path": "src/thoth/protocol/operation_owner.py", + "sha256": "8f6da0b375e71715fe8ab4b959b8c41b9be1f7b826075ff0bc937dad4f0b8c73", + "bytes": 8187 }, { "path": "src/thoth/protocol/read_queries.py", @@ -7513,8 +7538,8 @@ }, { "path": "tests/atomicity/harness.py", - "sha256": "2a1c0cdb5a6cc615a2992864e7e469881fa451a86d5172ddf924c275f03116e6", - "bytes": 6014 + "sha256": "77de7714764378821c70ae59a82bc5bdff882b9d7a1c0836efa91138aa3ea812", + "bytes": 8892 }, { "path": "tests/atomicity/outcome_helpers.py", @@ -7921,15 +7946,20 @@ "sha256": "18b4236f9f0d283985bbfc15b170d9c62bfaa328248ea0f7974bbc76b9bf39d3", "bytes": 112407 }, + { + "path": "tests/fixtures/sealable_methods.json", + "sha256": "feba3ef6fcde2e83ba7b5c9c376573a94b02dafaeafcf8da6ce4cefef14efb67", + "bytes": 2350 + }, { "path": "tests/fixtures/storage_migration_ddl.json", - "sha256": "aa74ddf13cc5b40816dc22386a4da0a44603e463a47de30a64c5bc76c22b5352", - "bytes": 109395 + "sha256": "7650d89b2e3d13106ab50fc4cebc691ddbfbc8c059d4871490b6057ef51d6b6d", + "bytes": 109872 }, { "path": "tests/fixtures/storage_schema_contract.json", - "sha256": "f9038cda6f0fce7eaf22022ff337afc5b1c4c71eeb66f268af6aab6c0e866144", - "bytes": 1396654 + "sha256": "7352456044287d3c14955aa29fb7bcb3141fa95a545a3eba9890e8d54623adfa", + "bytes": 1560992 }, { "path": "tests/fixtures/structured_source_fixture.py", @@ -8023,7 +8053,7 @@ }, { "path": "tests/integration/migration_head_helpers.py", - "sha256": "1a20359a8938c37ae8d7afe6f95068642d8af403195263dea9f32316230b7f56", + "sha256": "558317d21741346f81c115e2d2c60ea461d0831b60319593509756f470db7998", "bytes": 698 }, { @@ -8073,8 +8103,8 @@ }, { "path": "tests/integration/schema_contract_probe.py", - "sha256": "477fb8231fcd57aa15c4efed0ffb309e28c5ba284faf82596393561963cbf842", - "bytes": 5725 + "sha256": "c7dd94899f25b7fbdc4f793019ed87cd05a53acbc9bb06275caea145083ad987", + "bytes": 5808 }, { "path": "tests/integration/scoped_runtime.py", @@ -8093,8 +8123,8 @@ }, { "path": "tests/integration/storage_coverage_helpers.py", - "sha256": "1e7e413b5b5debdd04fa7a687e2acd17193c955c976391a060833a1888258644", - "bytes": 2665 + "sha256": "6a18b236a42439bcdecde9cb51de7e0bf079dc388b89a6e90683a87c3a6e31bb", + "bytes": 2689 }, { "path": "tests/integration/synthetic_field_baseline.py", @@ -8363,8 +8393,8 @@ }, { "path": "tests/integration/test_action_step_add_step_id.py", - "sha256": "61d48c1d80db7a43812858e311777764ccf58e038304d9c9bb2463fb8304d4cf", - "bytes": 7319 + "sha256": "0a3fe2fb46a20eca060b926abbe472eafcf4b41e1490e3ee92cc4ef38f6b447d", + "bytes": 7441 }, { "path": "tests/integration/test_active_behavior_consumed.py", @@ -8508,8 +8538,8 @@ }, { "path": "tests/integration/test_d1_object_followup_atomicity.py", - "sha256": "3dac6c905c39f9cd3d566e99af64f338312dfb99771ed3b7eaa9dbdd573b6866", - "bytes": 15698 + "sha256": "ee925b452b50cee46ec4ed634f3b839726e2f0fc4dabd102d02984c11273d995", + "bytes": 15839 }, { "path": "tests/integration/test_d2_counterevidence_stage.py", @@ -8768,13 +8798,13 @@ }, { "path": "tests/integration/test_m3_action_authorization_atomicity.py", - "sha256": "e6d1c1c7c57a80908435d3dba119077b79ceeb54263b66fcdaa92a308d4c0da3", - "bytes": 26211 + "sha256": "a41185e1c90a1e0b2dcd09baade22daff1dc62fd76b85722d52e35022c8e5a29", + "bytes": 26486 }, { "path": "tests/integration/test_m3_action_mutation_atomicity.py", - "sha256": "79f870a268a528ec5140dd868afdabbbeb706ec8066dcd698ce0005d7d937585", - "bytes": 32875 + "sha256": "0d224249ac77c15abb54002a4f19f4e072f1d223143120c514ed801893b74f88", + "bytes": 33150 }, { "path": "tests/integration/test_m3_direct_test_binding_contract.py", @@ -8783,13 +8813,13 @@ }, { "path": "tests/integration/test_m3_execution_effect_atomicity.py", - "sha256": "7838d6056ef61f727247543c5bf9ecf4d01d56e8125fafd3311ba92baa9bde62", - "bytes": 31334 + "sha256": "40242101a1f779344f66d69cae8bff5cc2dee03a462c3bb93bccbdceb628b72b", + "bytes": 31609 }, { "path": "tests/integration/test_m3_execution_revision_atomicity.py", - "sha256": "a30deaf69c7695cbf7233e48bc242a7f7fe4b5225e966c3cef16bd298b316143", - "bytes": 22981 + "sha256": "a897a144f056989f7a94a81b3cc5ae8007c58b1b47bece761185e4e0200c3092", + "bytes": 23256 }, { "path": "tests/integration/test_m3_hypothesis_auxiliary_atomicity.py", @@ -8823,18 +8853,18 @@ }, { "path": "tests/integration/test_m3_object_mutation_atomicity.py", - "sha256": "964b7d293ce48d4127555661f4dfbe0211f3dcdf30ed279fdbbb05f3a8562ca3", - "bytes": 27362 + "sha256": "fee25beeba736d033fcf43ce144cc3dae61cc2800acd17d57cc1ae3f00e39ee6", + "bytes": 27543 }, { "path": "tests/integration/test_m3_outcome_child_atomicity.py", - "sha256": "0f2f2054d3abcf7bb30969ee6fd5bebda42ccdf2cf1561fa7d0d3feaaf4afd2e", - "bytes": 30876 + "sha256": "d072e490208a9f5505179e003b4829e7a912fa1ac01e6d5ec587e292ebbf1538", + "bytes": 31695 }, { "path": "tests/integration/test_m3_outcome_revision_atomicity.py", - "sha256": "f6468d7624162fda66f31900f1f35b225925e456d29c7fab82255ba80a0b1a00", - "bytes": 25504 + "sha256": "8fc75182af37902a10b5541a117192daa8c22955c4a865d669eb2a6b6ef42e58", + "bytes": 25779 }, { "path": "tests/integration/test_m3_prediction_preparation_atomicity.py", @@ -8848,8 +8878,8 @@ }, { "path": "tests/integration/test_m4_memory_control_multiwrite_observation.py", - "sha256": "c3f2bb4eaf2836b958e038d244c3074e016f60f19eb5b352282a1eba96b0e12e", - "bytes": 17723 + "sha256": "49663656a8527b1f8c10311ef1edc731be6bee31e8af56293c4af46b649883ef", + "bytes": 18382 }, { "path": "tests/integration/test_m5_exposure_service_atomicity.py", @@ -9021,6 +9051,31 @@ "sha256": "7b269e5c1382f64823122be46004ea6e0679725d3ba6b6d9644523596e37a927", "bytes": 13419 }, + { + "path": "tests/integration/test_operation_owner_atomicity.py", + "sha256": "c9d5937379847011fd6381efb2b1f2bf5c2447d57df294f347550ac34e5a8a5e", + "bytes": 4492 + }, + { + "path": "tests/integration/test_operation_owner_completion_race.py", + "sha256": "b01b01b8ad62891167943f2893791137f8ea34cc83c5c5ce33ea76952914e1cb", + "bytes": 7531 + }, + { + "path": "tests/integration/test_operation_owner_migration.py", + "sha256": "9d92163feccecd485ebaed50553e9adaa451e26315fae803f9818eb2b219b76b", + "bytes": 4851 + }, + { + "path": "tests/integration/test_operation_owner_recovery.py", + "sha256": "5a8bf32436df6ebf36f7d5dbc25d548ee4087201dd1887592ddd640b02b9138f", + "bytes": 6789 + }, + { + "path": "tests/integration/test_operation_owner_storage.py", + "sha256": "b7ab6e89c25983737be61165c897fe9225313e586379612c118f56f1430843cf", + "bytes": 9563 + }, { "path": "tests/integration/test_operation_started_atomic.py", "sha256": "ec8a58d446540336d6a4e964fbbbbc908428b0bb0aacd0e8882af579c285f01e", @@ -9483,8 +9538,8 @@ }, { "path": "tests/integration/test_resource_scope_transition_boundaries.py", - "sha256": "cd12e11703e24014dc747d000be07fd08bc5a6e416ea9c3bf6b7eb2010949318", - "bytes": 16263 + "sha256": "eecfac5e09c950b271e121060dad638e5aca7526a37713d0c3a7d982444e23e7", + "bytes": 18949 }, { "path": "tests/integration/test_restore_access.py", @@ -9563,8 +9618,8 @@ }, { "path": "tests/integration/test_revision_full_rpc.py", - "sha256": "0ad36d95ea809f99c6e74db0cea6ce5a2560eb0bcb1fdcf056df3e4bbdfe45f5", - "bytes": 22002 + "sha256": "f27a1c5773020ad0d53a78ad63d4a1d9cd48813901cfaf59036c0b7731f67a2b", + "bytes": 22230 }, { "path": "tests/integration/test_revision_merge_late_write_observation.py", @@ -9688,8 +9743,8 @@ }, { "path": "tests/integration/test_stale_running_current_behavior.py", - "sha256": "b64169bb51afebab03c03497fa23b252049dff38954988e504cfe7e1a47d38ab", - "bytes": 13545 + "sha256": "dd6ab402e02900c2e42504d0bc20e28faa8e94b8bb4771cf2a5e3786ba24fa38", + "bytes": 15507 }, { "path": "tests/integration/test_state_transition_noop.py", @@ -9816,6 +9871,11 @@ "sha256": "7b5656a82f03b863c9e2bcce445385d79aac3450a3615c38ddb2e9d42094c194", "bytes": 2668 }, + { + "path": "tests/integration/test_workspace_owner_pin.py", + "sha256": "4b62b04db34039b1ee9c5958b54f6b127b57f3b54fcc02f8fca6646aeaed02f2", + "bytes": 1578 + }, { "path": "tests/integration/test_workspace_restart_storage.py", "sha256": "e6a5ef1002509cdaebae4236fa0df2a48470e6cc1996e0123c8331e583ca5c94", @@ -11146,6 +11206,21 @@ "sha256": "11edc80dabe3d3cb9a210613ea22428052e1be8cb0143c66e1914b355d8a6269", "bytes": 2321 }, + { + "path": "tests/unit/test_operation_host_identity.py", + "sha256": "7cb67880b5deaebf2906087489764f3a80bfc68d95bb8eb33b672ba931626252", + "bytes": 7012 + }, + { + "path": "tests/unit/test_operation_owner_allowances.py", + "sha256": "78165376eab556e94660124f2ad7ca1645176ea52e6610ceb6d138d99f8a6288", + "bytes": 5114 + }, + { + "path": "tests/unit/test_operation_owner_policy.py", + "sha256": "c89a0ce411e1b781e551790c492e44fa44e8c5d365f4eef323626714a8f8246e", + "bytes": 6335 + }, { "path": "tests/unit/test_operation_reentry_registry.py", "sha256": "019af79fa67e596211ce244e2429988d202c4ea7ff32b314e2069219548d7082", diff --git a/config/module-responsibility-budget.json b/config/module-responsibility-budget.json index 52a5a7c..4a6d579 100644 --- a/config/module-responsibility-budget.json +++ b/config/module-responsibility-budget.json @@ -227,7 +227,7 @@ "policy": "over the 600-line limit when it was introduced; do not grow it; lower the baseline when it is split" }, "src/thoth/protocol/bus.py": { - "baseline_lines": 741, + "baseline_lines": 740, "top_level_symbols": [ "DispatchTicket", "PreClaimAuthorization", diff --git a/docs/KNOWN_LIMITATIONS.ko.md b/docs/KNOWN_LIMITATIONS.ko.md index f1b2ea6..7c4f2c7 100644 --- a/docs/KNOWN_LIMITATIONS.ko.md +++ b/docs/KNOWN_LIMITATIONS.ko.md @@ -20,15 +20,23 @@ English: [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md) 함께 쓰는 폴더나 클라우드 동기화·네트워크 공유로 여러 컴퓨터가 보는 작업 공간은 보호하지 못합니다. 그런 방식으로 같은 작업 공간을 두 곳에서 열지 마세요. 심사용(hosted review) 모드는 이 잠금을 쓰지 않습니다. -## 종료로 중단된 요청이 "실행 중"으로 남을 수 있습니다 - -THOTH는 작업을 시작하기 전에 요청을 "실행 중"으로 기록합니다. 작업 도중 앱이 멈추면 그 기록이 -"실행 중"으로 남을 수 있습니다. 같은 요청을 같은 멱등 key로 다시 보내면 작업을 두 번 하지 않으려고 -다시 시작하지 않고 "실행 중"을 돌려줍니다. 이어서 하려면 새 질문으로 다시 하세요. 새 질문은 -모델 사용량을 다시 쓸 수 있으니, 먼저 이전 결과가 이미 저장됐는지 확인하세요. - -심사용(hosted review) 모드에서는 재시작 뒤 이런 요청이 "이전 호출의 종료를 확인하지 못했다"는 -안내와 함께 닫힐 수도 있습니다. 그 모드의 알려진 한계는 [VERIFICATION.md](VERIFICATION.md)에 있습니다. +## 종료로 중단된 요청의 복구 + +로컬 모드에서는 실측한 67개 명령에 한해, 같은 멱등 key를 다시 보내면 소유 worker의 종료가 +확인된 요청을 실패로 닫을 수 있습니다. 같은 호스트·부팅 식별과 이미 보유한 작업 공간 잠금도 +필요합니다. `OPERATION_OWNER_GONE`과 함께 저장된 내용을 확인하라는 안내를 반환하며, +handler를 다시 실행하거나 과거 요청을 자동으로 훑어 닫지는 않습니다. + +목록 밖의 명령(연구 시작·입력·조정 및 일시정지·재개·중지 포함), 소유 정보가 없는 옛 기록, +호스트 식별이 불명하거나 다른 경우, worker가 살아 있거나 생존 여부가 불명인 경우는 +실행 중으로 남을 수 있습니다. 시간 초과만으로 종료를 판단하지 않습니다. 심사용 모드의 복구 +정책은 그대로입니다. `improvement/evaluation/run`은 쓰기 없는 응답에 명시적인 unchanged 표시가 +없어, 별도 검토 전까지 허용 목록 밖의 후속 후보로 남습니다. + +봉인이 원래 완료와의 경합에서 이기면 도메인 변경은 이미 저장됐어도 일반 변경 알림은 빠질 수 +있습니다. 새 요청을 보내기 전에 저장된 결과를 확인하세요. 새 질문은 모델 사용량을 다시 쓸 수 +있고, 새로 읽은 개정 번호로 보내면 같은 변경이 한 번 더 적용될 수도 있습니다. 이 복구가 +외부 효과의 정확히 한 번 실행을 보장하는 것은 아닙니다. ## 끝난 요청의 후속 활동 기록이 빠질 수 있습니다 diff --git a/docs/KNOWN_LIMITATIONS.md b/docs/KNOWN_LIMITATIONS.md index c70e96a..5f4681f 100644 --- a/docs/KNOWN_LIMITATIONS.md +++ b/docs/KNOWN_LIMITATIONS.md @@ -22,16 +22,23 @@ on a local disk. It does not cover a workspace folder shared between Windows and cloud sync client or a network share exposes to several computers; do not open the same workspace from two places that way. The hosted review mode does not use this lock. -## A request interrupted by a shutdown can stay "running" - -THOTH records a request as running before it starts the work. If the application stops while the -work is in progress, the record can stay "running". Sending the same request again with the same -idempotency key returns "running" instead of starting the work a second time, so that nothing is -done twice. To continue, ask again as a new question. A new question can use model capacity again, -so check first whether the earlier result has already been saved. - -In the hosted review mode a restart can also close such a request with a message that the previous -call could not be confirmed. See [VERIFICATION.md](VERIFICATION.md) for that mode's known limit. +## Recovery of a request interrupted by shutdown + +In LOCAL mode, 67 measured commands can be closed as failed when the same idempotency key is +sent again and the original worker is confirmed gone. THOTH also requires matching host/boot +identity and an already-held workspace lock. It returns `OPERATION_OWNER_GONE` with guidance to +check what was saved; it does not rerun the handler or scan old requests automatically. + +Requests outside that list (including research start/input/steer and pause/resume/stop), old +records without ownership, an unavailable or different host identity, and a live or unknown +worker can remain running. A timeout alone is not proof of death. The hosted review recovery +policy is unchanged. `improvement/evaluation/run` remains outside the list pending separate +review because its no-effect response lacks an explicit unchanged marker. + +If sealing wins a race with the original completion, a domain change may already be saved while +its ordinary change notification is omitted. Check the saved result before sending a new request. +A new question may use model capacity again; a newly refreshed revision guard can also permit a +second change. This recovery is not a guarantee of exactly-once external effects. ## A finished request can miss some follow-up log entries diff --git a/migrations/env.py b/migrations/env.py index f197ebc..075242b 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -8,6 +8,7 @@ from thoth.adapters.storage import behavior_execution_schema as behavior_execution_schema from thoth.adapters.storage import evaluation_run_schema as evaluation_run_schema +from thoth.adapters.storage import operation_owner_schema as operation_owner_schema from thoth.adapters.storage import research_schema as research_schema from thoth.adapters.storage import resource_scope_schema as resource_scope_schema from thoth.adapters.storage import test_validity_schema as test_validity_schema diff --git a/migrations/versions/4c17f5089ea3_operation_owners.py b/migrations/versions/4c17f5089ea3_operation_owners.py new file mode 100644 index 0000000..8e34c1a --- /dev/null +++ b/migrations/versions/4c17f5089ea3_operation_owners.py @@ -0,0 +1,24 @@ +"""Record internal operation process ownership without changing existing rows.""" + +import sqlalchemy as sa +from alembic import op + +revision = "4c17f5089ea3" +down_revision = "3b06e4f87d92" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "operation_owners", + sa.Column("operation_id", sa.String(160), primary_key=True), + sa.Column("worker_id", sa.String(160), nullable=False), + sa.Column("process_id", sa.Integer(), nullable=False), + sa.Column("process_marker", sa.String(160), nullable=False), + sa.Column("host_token", sa.String(160), nullable=False), + ) + + +def downgrade() -> None: + op.drop_table("operation_owners") diff --git a/src/thoth/adapters/operation_host_identity.py b/src/thoth/adapters/operation_host_identity.py new file mode 100644 index 0000000..ea736bd --- /dev/null +++ b/src/thoth/adapters/operation_host_identity.py @@ -0,0 +1,104 @@ +"""Best-effort host/boot equality token; not an authentication or liveness proof.""" + +from __future__ import annotations + +import hashlib +import hmac +import os +import re +import sys +import threading +from pathlib import Path +from uuid import UUID + +from thoth.domain.deployment_mode import DeploymentMode + +_KEY = b"thoth.operation-owner.host.v1" +_cache: tuple[int, str | None] | None = None +_lock = threading.Lock() + + +def _guid(value: str) -> str: + if not re.fullmatch(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}", value): + raise ValueError("HOST_IDENTITY_FORMAT") + parsed = UUID(value) + if parsed.int == 0: + raise ValueError("HOST_IDENTITY_FORMAT") + return parsed.hex + + +def _windows_identity() -> tuple[str, str]: + import winreg + + access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY + boot_path = ( + r"SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters" + ) + + def boot() -> int: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, boot_path, 0, access) as key: + value, kind = winreg.QueryValueEx(key, "BootId") + if kind != winreg.REG_DWORD or type(value) is not int or not 0 <= value <= 2**32 - 1: + raise ValueError("HOST_IDENTITY_FORMAT") + return value + + before = boot() + with winreg.OpenKey( + winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography", 0, access + ) as key: + machine, kind = winreg.QueryValueEx(key, "MachineGuid") + after = boot() + if before != after or kind != winreg.REG_SZ or not isinstance(machine, str): + raise ValueError("HOST_IDENTITY_UNAVAILABLE") + return _guid(machine), "prefetch-bootid:" + str(before) + + +def _bounded_text(path: str) -> str: + with Path(path).open("rb") as source: + raw = source.read(129) + if len(raw) > 128: + raise ValueError("HOST_IDENTITY_FORMAT") + return raw.decode("ascii").strip() + + +def _linux_identity() -> tuple[str, str]: + machine = _bounded_text("/etc/machine-id") + if not re.fullmatch(r"[0-9a-fA-F]{32}", machine) or int(machine, 16) == 0: + raise ValueError("HOST_IDENTITY_FORMAT") + return machine.lower(), _guid(_bounded_text("/proc/sys/kernel/random/boot_id")) + + +def _read_token() -> str | None: + try: + if sys.platform == "win32": + machine, boot = _windows_identity() + elif sys.platform == "linux": + machine, boot = _linux_identity() + else: + return None + payload = (sys.platform + "\0" + machine + "\0" + boot).encode("utf-8") + return hmac.new(_KEY, payload, hashlib.sha256).hexdigest() + except (OSError, ValueError, TypeError, ImportError): + return None + + +def operation_host_token(mode: DeploymentMode) -> str | None: + """Read once per LOCAL process, caching failure too; HOSTED performs no lookup.""" + if mode is not DeploymentMode.LOCAL: + return None + global _cache + with _lock: + pid = os.getpid() + if _cache is None or _cache[0] != pid: + _cache = (pid, _read_token()) + return _cache[1] + + +def _after_fork() -> None: + global _cache, _lock + _cache = None + _lock = threading.Lock() + + +if hasattr(os, "register_at_fork"): + os.register_at_fork(after_in_child=_after_fork) diff --git a/src/thoth/adapters/storage/operation_owner_schema.py b/src/thoth/adapters/storage/operation_owner_schema.py new file mode 100644 index 0000000..8823c46 --- /dev/null +++ b/src/thoth/adapters/storage/operation_owner_schema.py @@ -0,0 +1,15 @@ +"""Internal process ownership, separate from the public operation columns.""" + +from sqlalchemy import Column, Integer, String, Table + +from thoth.adapters.storage.schema import metadata + +operation_owners = Table( + "operation_owners", + metadata, + Column("operation_id", String(160), primary_key=True), + Column("worker_id", String(160), nullable=False), + Column("process_id", Integer, nullable=False), + Column("process_marker", String(160), nullable=False), + Column("host_token", String(160), nullable=False), +) diff --git a/src/thoth/adapters/storage/operation_owner_store.py b/src/thoth/adapters/storage/operation_owner_store.py new file mode 100644 index 0000000..41b32fd --- /dev/null +++ b/src/thoth/adapters/storage/operation_owner_store.py @@ -0,0 +1,30 @@ +"""Owner persistence on the caller's existing operation transaction.""" + +from sqlalchemy import Connection, insert, select + +from thoth.adapters.storage.operation_owner_schema import operation_owners +from thoth.domain.operation import OperationOwnerRecord + + +def insert_owner( + connection: Connection, operation_id: str, owner: OperationOwnerRecord | None +) -> None: + if owner is not None and owner.host_token is not None: + connection.execute( + insert(operation_owners).values(operation_id=operation_id, **owner.model_dump()) + ) + + +def read_owner(connection: Connection, operation_id: str) -> OperationOwnerRecord | None: + row = ( + connection.execute( + select(operation_owners).where(operation_owners.c.operation_id == operation_id) + ) + .mappings() + .first() + ) + if row is None: + return None + return OperationOwnerRecord.model_validate( + {key: row[key] for key in ("worker_id", "process_id", "process_marker", "host_token")} + ) diff --git a/src/thoth/adapters/storage/operations.py b/src/thoth/adapters/storage/operations.py index b245d50..1fbd098 100644 --- a/src/thoth/adapters/storage/operations.py +++ b/src/thoth/adapters/storage/operations.py @@ -13,6 +13,7 @@ from sqlalchemy.exc import OperationalError from thoth.adapters.storage.events import insert_event_on_connection +from thoth.adapters.storage.operation_owner_store import insert_owner, read_owner from thoth.adapters.storage.resource_scope_schema import operation_resource_bindings from thoth.adapters.storage.schema import ( idempotency_keys, @@ -150,6 +151,8 @@ def _claim( *, started_event: Callable[[], EventRecord] | None = None, ) -> tuple[OperationRecord, bool]: + if candidate.owner is not None and candidate.owner.host_token is None: + candidate = candidate.model_copy(update={"owner": None}) # Logical operation bookkeeping owns its transaction; it must not borrow a # domain transaction that could later commit a partially failed claim. with self._engine.begin() as connection: @@ -203,6 +206,7 @@ def _claim( operation_id=candidate.operation_id, ) ) + insert_owner(connection, candidate.operation_id, candidate.owner) if started_event is not None: insert_event_on_connection(connection, started_event()) return candidate, False @@ -362,7 +366,10 @@ def _read_with_connection( ) if row is None: return None - return self._with_resource_binding(connection, self._deserialize(row)) + record = self._deserialize(row).model_copy( + update={"owner": read_owner(connection, operation_id)} + ) + return self._with_resource_binding(connection, record) @staticmethod def _with_resource_binding( diff --git a/src/thoth/adapters/storage/workspace_owner.py b/src/thoth/adapters/storage/workspace_owner.py index 70e0eba..5a38d08 100644 --- a/src/thoth/adapters/storage/workspace_owner.py +++ b/src/thoth/adapters/storage/workspace_owner.py @@ -14,6 +14,8 @@ import os import sys import threading +from collections.abc import Generator +from contextlib import contextmanager from dataclasses import dataclass from datetime import UTC, datetime from pathlib import Path @@ -183,6 +185,26 @@ def acquire_workspace_owner(workspace: Path, *, command: str | None = None) -> W return WorkspaceOwnership(key) +def holds_workspace(workspace: Path) -> bool: + with _GUARD: + return _key(workspace) in _OWNED + + +@contextmanager +def pin_workspace_owner(workspace: Path) -> Generator[bool]: + """Retain only an already-owned lock; never acquire or create a lock file.""" + key = _key(workspace) + with _GUARD: + entry = _OWNED.get(key) + if entry is not None: + entry.count += 1 + try: + yield entry is not None + finally: + if entry is not None: + _release(key) + + def _release(key: str) -> None: with _GUARD: entry = _OWNED.get(key) diff --git a/src/thoth/apps/resource_scope_composition.py b/src/thoth/apps/resource_scope_composition.py index 5671a0d..025b6b6 100644 --- a/src/thoth/apps/resource_scope_composition.py +++ b/src/thoth/apps/resource_scope_composition.py @@ -23,6 +23,7 @@ from thoth.ports.event_store import EventStorePort from thoth.ports.field_measurement import FieldMeasurementObserverPort from thoth.ports.governance import GovernanceStorePort +from thoth.ports.journal import JournalPort from thoth.ports.ledger import ManagedLedgerPort from thoth.ports.operation import OperationStorePort from thoth.ports.project import ProjectStorePort @@ -30,6 +31,7 @@ from thoth.ports.sandbox import SandboxPort from thoth.ports.store_bundle import StoreBundlePort from thoth.protocol.bus import CommandBus +from thoth.protocol.operation_owner import OperationOwner from thoth.protocol.registry import MethodRegistry _LOCAL_SOURCE_KINDS = frozenset({"LOCAL", "GIT"}) @@ -113,6 +115,8 @@ def scope_bus( seal_abandoned_running: bool = False, before_continue: Callable[[str], Awaitable[None]] | None = None, queued_admission: Callable[[OperationRecord], dict[str, JsonValue] | None] | None = None, + owner: OperationOwner | None = None, + journal: JournalPort | None = None, ) -> CommandBus: handlers = OperationCommandHandlers( operations, @@ -132,12 +136,13 @@ def scope_bus( operations, clock, ids, - journal=OperationJournal(events, clock, ids), + journal=journal if journal is not None else OperationJournal(events, clock, ids), measurement=measurement, resource_access=scopes, seal_abandoned_running=seal_abandoned_running, before_continue=before_continue, queued_admission=queued_admission, + owner=owner, ) diff --git a/src/thoth/apps/runtime_entry.py b/src/thoth/apps/runtime_entry.py index dde9f51..30c7601 100644 --- a/src/thoth/apps/runtime_entry.py +++ b/src/thoth/apps/runtime_entry.py @@ -2,13 +2,17 @@ from __future__ import annotations +from functools import partial from pathlib import Path +from thoth.adapters.operation_host_identity import operation_host_token +from thoth.adapters.storage.workspace_owner import holds_workspace, pin_workspace_owner from thoth.application.commands import ThreadCommandHandlers from thoth.application.commands.research_operations import register_research_operations from thoth.application.services import FieldMeasurementService from thoth.application.services.connector_service import ConnectorService from thoth.application.services.full_project_memory import FullProjectMemoryService +from thoth.application.services.operation_journal import OperationJournal from thoth.application.services.scoped_artifacts import ScopedArtifactLedger from thoth.apps.research_entry_composition import ResearchEntryComposition, create_research_entry from thoth.apps.research_runtime import ResearchComponents @@ -26,6 +30,7 @@ from thoth.ports.store_bundle import StoreBundlePort from thoth.ports.task_profile import TaskProfileCatalogPort from thoth.protocol.bus import CommandBus +from thoth.protocol.operation_owner import OperationOwner def assemble_runtime_entry( @@ -77,6 +82,20 @@ def assemble_runtime_entry( from thoth.apps.hosted_review_dispatch import wait_for_hosted_dispatch before_continue = wait_for_hosted_dispatch + journal = OperationJournal(stores.events, clock, ids) + owner = None + if mode is DeploymentMode.LOCAL: + owner = OperationOwner( + worker=research_component.worker, + host_token=operation_host_token(mode), + holds_workspace=partial(holds_workspace, workspace), + pin_workspace=partial(pin_workspace_owner, workspace), + operations=operations, + uow=stores.immediate_uow, + journal=journal, + clock=clock, + resource_access=artifact_ledger.scopes, + ) bus = scope_bus( registration.registry, ledger, @@ -89,6 +108,8 @@ def assemble_runtime_entry( seal_abandoned_running=mode is DeploymentMode.HOSTED_REVIEW, before_continue=before_continue, queued_admission=research_component.entry.queued_admission, + owner=owner, + journal=journal, ) registration.judgment_review.connect(bus, research_component.entry) from thoth.application.commands.research_queue_cancel import QueueAwareOperationCancel diff --git a/src/thoth/domain/operation.py b/src/thoth/domain/operation.py index fd5732f..f694fcc 100644 --- a/src/thoth/domain/operation.py +++ b/src/thoth/domain/operation.py @@ -1,6 +1,6 @@ from __future__ import annotations -from pydantic import AwareDatetime, JsonValue +from pydantic import AwareDatetime, Field, JsonValue from thoth.domain.base import DomainModel from thoth.domain.enums import OperationState @@ -35,6 +35,13 @@ class InternalFailureDiagnostic(DomainModel): post_state: InternalFailureStateSnapshot +class OperationOwnerRecord(DomainModel): + worker_id: str = Field(min_length=1, max_length=160) + process_id: int = Field(gt=0) + process_marker: str = Field(min_length=1, max_length=160) + host_token: Sha256 | None + + class OperationRecord(DomainModel): operation_id: OperationId project_id: ProjectId @@ -53,3 +60,4 @@ class OperationRecord(DomainModel): epoch: int = 0 schema_version: str = "1.0.0" resource_uses: tuple[ResourceUse, ...] | None = None + owner: OperationOwnerRecord | None = Field(default=None, exclude=True, repr=False) diff --git a/src/thoth/protocol/bus.py b/src/thoth/protocol/bus.py index f24c41a..2ebf574 100644 --- a/src/thoth/protocol/bus.py +++ b/src/thoth/protocol/bus.py @@ -12,7 +12,6 @@ from thoth.application.services.historical_access_verification import historical_query_verification from thoth.domain.auth import current_authenticated_actor from thoth.domain.canonical import canonical_payload, domain_digest -from thoth.domain.deployment_mode import STALE_RUNNING_MESSAGE, STALE_RUNNING_REASON from thoth.domain.enums import OperationState from thoth.domain.operation import ( RESEARCH_OPERATION_METHODS, @@ -52,7 +51,8 @@ RpcErrorCode, ) from thoth.protocol.notifications import notifications_for -from thoth.protocol.operation_claim import claim_operation +from thoth.protocol.operation_claim import claim_busy_response, claim_operation +from thoth.protocol.operation_owner import OperationOwner, stale_running_error from thoth.protocol.read_queries import READ_QUERY_METHODS from thoth.protocol.registry import CommandHandler, MethodRegistry @@ -83,6 +83,7 @@ def __init__( seal_abandoned_running: bool = False, before_continue: Callable[[str], Awaitable[None]] | None = None, queued_admission: Callable[[OperationRecord], dict[str, JsonValue] | None] | None = None, + owner: OperationOwner | None = None, ) -> None: self._registry = registry self._operations = operations @@ -94,6 +95,7 @@ def __init__( self._seal_abandoned_running = seal_abandoned_running self._before_continue = before_continue self._queued_admission = queued_admission + self._owner = owner self._operation_task_canceller: Callable[[str], None] | None = None self._failure_diagnostics = ( operations if isinstance(operations, InternalFailureDiagnosticPort) else None @@ -207,11 +209,16 @@ def claim(self, request: JsonRpcRequest) -> DispatchTicket | JsonRpcResponse: ), state=OperationState.RUNNING, created_at=self._clock.now(), + owner=None if self._owner is None else self._owner.record(), ) operation, replayed = claim_operation(self._operations, candidate, self._journal) if replayed: if operation.state == OperationState.RUNNING: self._registry.authorize_reentry(operation) + if self._owner is not None: + sealed = self._owner.seal(request, operation, self._measure) + if sealed is not None: + return self._replay(request.id, sealed) live_task = operation.operation_id in self._research_tasks if ( self._seal_abandoned_running @@ -259,18 +266,7 @@ def claim(self, request: JsonRpcRequest) -> DispatchTicket | JsonRpcResponse: except OperationReentryDenied as exc: return self._reentry_denied(request, exc) except OperationClaimBusy: - return JsonRpcResponse( - id=request.id, - error=JsonRpcError( - code=RpcErrorCode.DOMAIN_REJECTED, - message="operation storage is busy; retry the same request later", - data={ - "reason_code": "OPERATION_CLAIM_BUSY", - "operation_claimed": False, - "retryable": True, - }, - ), - ) + return claim_busy_response(request.id) except IdempotencyConflict as exc: return JsonRpcResponse( id=request.id, @@ -373,11 +369,13 @@ async def _execute(self, ticket: DispatchTicket) -> JsonRpcResponse: ) except (RpcApplicationError, ResourceScopeError) as exc: error = self._application_error(exc) - self._operations.fail( + failed = self._operations.fail( operation.operation_id, error.model_dump(mode="json"), completed_at=self._clock.now(), ) + if failed.error != error.model_dump(mode="json"): + return self._replay(request.id, failed) self._append_event( operation, "operation.failed", @@ -391,11 +389,13 @@ async def _execute(self, ticket: DispatchTicket) -> JsonRpcResponse: message="authenticated authority identity mismatch", data={"reason_code": str(exc), "pre_io": True}, ) - self._operations.fail( + failed = self._operations.fail( operation.operation_id, error.model_dump(mode="json"), completed_at=self._clock.now(), ) + if failed.error != error.model_dump(mode="json"): + return self._replay(request.id, failed) self._append_event( operation, "operation.failed", @@ -414,11 +414,13 @@ async def _execute(self, ticket: DispatchTicket) -> JsonRpcResponse: ) }, ) - self._operations.fail( + failed = self._operations.fail( operation.operation_id, error.model_dump(mode="json"), completed_at=self._clock.now(), ) + if failed.error != error.model_dump(mode="json"): + return self._replay(request.id, failed) self._append_event( operation, "operation.failed", @@ -438,11 +440,13 @@ async def _execute(self, ticket: DispatchTicket) -> JsonRpcResponse: message="internal command failure", data={"operation_id": operation.operation_id}, ) - self._operations.fail( + failed = self._operations.fail( operation.operation_id, error.model_dump(mode="json"), completed_at=self._clock.now(), ) + if failed.error != error.model_dump(mode="json"): + return self._replay(request.id, failed) self._append_event( operation, "operation.failed", @@ -455,6 +459,8 @@ async def _execute(self, ticket: DispatchTicket) -> JsonRpcResponse: durable_value, completed_at=self._clock.now(), ) + if completed.state is not OperationState.SUCCEEDED: + return self._replay(request.id, completed) result_digest = domain_digest("OPERATION_RESULT", "1.0.0", canonical_payload(durable_value)) if self._journal is not None: self._journal.checkpoint( @@ -584,14 +590,7 @@ def _scope_digest(self, request: JsonRpcRequest) -> str: def _fail_abandoned_running( self, request: JsonRpcRequest, operation: OperationRecord ) -> JsonRpcResponse: - error = JsonRpcError( - code=RpcErrorCode.DOMAIN_REJECTED, - message=STALE_RUNNING_MESSAGE, - data={ - "reason_code": STALE_RUNNING_REASON, - "remote_observation": "UNKNOWN", - }, - ) + error = stale_running_error() failed = self._operations.fail( operation.operation_id, error.model_dump(mode="json"), diff --git a/src/thoth/protocol/operation_claim.py b/src/thoth/protocol/operation_claim.py index 8180e30..010bca2 100644 --- a/src/thoth/protocol/operation_claim.py +++ b/src/thoth/protocol/operation_claim.py @@ -5,6 +5,7 @@ from thoth.domain.operation import OperationRecord from thoth.ports.journal import JournalPort from thoth.ports.operation import OperationStorePort +from thoth.protocol.jsonrpc import JsonRpcError, JsonRpcResponse, RpcErrorCode def claim_operation( @@ -13,3 +14,18 @@ def claim_operation( if journal is None: return operations.claim(candidate) return operations.claim(candidate, started_event=partial(journal.prepare_started, candidate)) + + +def claim_busy_response(request_id: str | int) -> JsonRpcResponse: + return JsonRpcResponse( + id=request_id, + error=JsonRpcError( + code=RpcErrorCode.DOMAIN_REJECTED, + message="operation storage is busy; retry the same request later", + data={ + "reason_code": "OPERATION_CLAIM_BUSY", + "operation_claimed": False, + "retryable": True, + }, + ), + ) diff --git a/src/thoth/protocol/operation_owner.py b/src/thoth/protocol/operation_owner.py new file mode 100644 index 0000000..4a05ccc --- /dev/null +++ b/src/thoth/protocol/operation_owner.py @@ -0,0 +1,217 @@ +"""LOCAL recovery for measured commands whose original worker is confirmed gone.""" + +from collections.abc import Callable +from contextlib import AbstractContextManager + +from thoth.domain.deployment_mode import STALE_RUNNING_MESSAGE, STALE_RUNNING_REASON +from thoth.domain.enums import OperationState +from thoth.domain.operation import OperationOwnerRecord, OperationRecord +from thoth.domain.resource_scope import current_resource_uses, resource_use_scope +from thoth.ports.journal import JournalPort +from thoth.ports.operation import OperationStorePort +from thoth.ports.resource_scope import ResourceAccessPort +from thoth.ports.runtime import AtomicUnitOfWorkPort, ClockPort +from thoth.ports.worker_identity import WorkerIdentityPort +from thoth.protocol.jsonrpc import JsonRpcError, JsonRpcRequest, RpcErrorCode + +# V-S2 approved evidence is pinned by tests/fixtures/sealable_methods.json. +SEALABLE_METHODS: frozenset[str] = frozenset( + { + "action/authorization/decide", + "action/plan/revise", + "action/revise", + "action/select", + "action/step/add", + "action/step/revise", + "closure/decide", + "closure/finalize", + "closure/reopen", + "criteria/field/correct", + "criteria/profile/apply", + "execution/cancel", + "execution/observation/link", + "execution/pause", + "execution/resume", + "execution/retry", + "execution/start", + "hypothesis/assumption/add", + "hypothesis/causal/update", + "hypothesis/intent/update", + "hypothesis/prediction/bind", + "hypothesis/relation/add", + "hypothesis/relation/remove", + "hypothesis/revise", + "improvement/evaluation/assess", + "improvement/exposure/complete", + "improvement/exposure/decide", + "improvement/exposure/rollback", + "improvement/exposure/start", + "improvement/promotion/decide", + "improvement/revise", + "investigation/pause", + "investigation/resume", + "investigation/stop", + "investigation/update", + "memory/candidate/classify", + "memory/settings/update", + "model/callSettings/update", + "model/settings/update", + "object/attention/acknowledge", + "object/facet/update", + "object/frame/revise", + "object/profile/apply", + "object/relation/add", + "object/relation/remove", + "outcome/assess", + "outcome/observation/link", + "outcome/reassess", + "project/activate", + "project/cutoff/update", + "project/metadata/update", + "project/overlay/update", + "project/policy/update", + "project/reference/import", + "project/role/assign", + "project/role/revoke", + "project/source/scope/assign", + "project/source/scope/grant", + "project/source/scope/revoke", + "project/source/scope/update", + "project/source/time/confirm", + "project/source/time/correct", + "revision/baseline/decide", + "revision/changeSet/validate", + "revision/merge/resolve", + "thread/metadata/update", + "trace/confirm", + } +) + + +def owner_gone_error() -> JsonRpcError: + return JsonRpcError( + code=RpcErrorCode.DOMAIN_REJECTED, + message=( + "이전에 보낸 같은 요청을 실행하던 THOTH가 끝나서 요청이 마무리되지 못했습니다. " + "변경이 저장됐는지 확인한 뒤 필요하면 새 요청으로 다시 보내세요. / " + "The THOTH process that was running this request ended before it finished. " + "Check whether the change was saved, then send it again as a new request " + "if you still need it." + ), + data={ + "reason_code": "OPERATION_OWNER_GONE", + "remote_observation": "UNKNOWN", + "new_request_required": True, + }, + ) + + +def stale_running_error() -> JsonRpcError: + return JsonRpcError( + code=RpcErrorCode.DOMAIN_REJECTED, + message=STALE_RUNNING_MESSAGE, + data={"reason_code": STALE_RUNNING_REASON, "remote_observation": "UNKNOWN"}, + ) + + +class OperationOwner: + """Injected LOCAL policy; OS identity/liveness is never read inside the UoW.""" + + def __init__( + self, + *, + worker: WorkerIdentityPort, + host_token: str | None, + holds_workspace: Callable[[], bool], + pin_workspace: Callable[[], AbstractContextManager[bool]], + operations: OperationStorePort, + uow: AtomicUnitOfWorkPort, + journal: JournalPort, + clock: ClockPort, + resource_access: ResourceAccessPort | None = None, + ) -> None: + self._worker = worker + self._host_token = host_token + self._holds_workspace = holds_workspace + self._pin_workspace = pin_workspace + self._operations = operations + self._uow = uow + self._journal = journal + self._clock = clock + self._resource_access = resource_access + self._record = ( + None + if host_token is None or worker.process_marker == "UNKNOWN" + else OperationOwnerRecord( + worker_id=worker.worker_id, + process_id=worker.process_id, + process_marker=worker.process_marker, + host_token=host_token, + ) + ) + + def record(self) -> OperationOwnerRecord | None: + return self._record + + def gone(self, operation: OperationRecord, method: str) -> bool: + owner = operation.owner + if ( + operation.state is not OperationState.RUNNING + or method != operation.method + or method not in SEALABLE_METHODS + or owner is None + or self._record is None + or owner.host_token is None + or owner.host_token != self._host_token + or owner.process_marker == "UNKNOWN" + or not self._holds_workspace() + ): + return False + return ( + self._worker.is_alive(owner.process_id, owner.process_marker, owner.worker_id) is False + ) + + def seal( + self, + request: JsonRpcRequest, + operation: OperationRecord, + measure: Callable[[JsonRpcRequest, str, str], None], + ) -> OperationRecord | None: + if request.params.meta.idempotency_key != operation.idempotency_key or not self.gone( + operation, request.method + ): + return None + with self._pin_workspace() as held: + if not held: + return None + with resource_use_scope(operation.project_id), self._uow.transaction(): + current = self._operations.read(operation.operation_id) + if current is None: + return None + if self._resource_access is not None: + self._resource_access.require_operation(current) + if current.state is not OperationState.RUNNING: + return current + if current.owner != operation.owner: + return current + error = owner_gone_error() + payload = error.model_dump(mode="json") + if self._resource_access is not None: + self._resource_access.require_operation( + current.model_copy( + update={"error": payload, "resource_uses": current_resource_uses()} + ) + ) + failed = self._operations.fail( + current.operation_id, payload, completed_at=self._clock.now() + ) + if failed.state is not OperationState.FAILED or failed.error != payload: + return failed + self._journal.append( + project_id=failed.project_id, + operation_id=failed.operation_id, + event_type="operation.failed", + payload={"code": error.code, "message": error.message}, + ) + measure(request, failed.project_id, "FAILED") + return failed diff --git a/tests/atomicity/harness.py b/tests/atomicity/harness.py index b5d591e..1f60458 100644 --- a/tests/atomicity/harness.py +++ b/tests/atomicity/harness.py @@ -12,6 +12,7 @@ from thoth.adapters.storage.resource_scope_schema import operation_resource_bindings from thoth.adapters.storage.schema import events, metadata, operations from thoth.domain.canonical import canonical_payload, domain_digest +from thoth.domain.operation import OperationOwnerRecord from thoth.domain.resource_scope import OperationResourceBinding from thoth.protocol.jsonrpc import JsonRpcRequest @@ -60,6 +61,7 @@ def matches(self, table: str, direction: str, row: dict[str, Any]) -> bool: def assert_phase_delta( before: Snapshot, after: Snapshot, allowed: tuple[AllowedRow, ...] = () ) -> None: + allowed = (*allowed, *operation_owner_allowances(before, after, allowed)) unexpected: list[tuple[str, str, str]] = [] for table in sorted(before.keys() | after.keys()): left, right = Counter(before.get(table, ())), Counter(after.get(table, ())) @@ -71,6 +73,66 @@ def assert_phase_delta( assert not unexpected, unexpected +def operation_owner_allowances( + before: Snapshot, after: Snapshot, allowed: tuple[AllowedRow, ...] +) -> tuple[AllowedRow, ...]: + """At most one owner added for each explicitly allowed, newly created operation.""" + old_ids = {json.loads(row)["operation_id"] for row in before.get("operations", ())} + old_owner_ids = {json.loads(row)["operation_id"] for row in before.get("operation_owners", ())} + new_operations = Counter(after.get("operations", ())) - Counter(before.get("operations", ())) + owners = [json.loads(row) for row in after.get("operation_owners", ())] + result: list[AllowedRow] = [] + for raw in new_operations: + operation = json.loads(raw) + identifier = operation["operation_id"] + if identifier in old_ids or identifier in old_owner_ids: + continue + if not any( + rule.identity.get("operation_id") == identifier + and rule.matches("operations", "added", operation) + for rule in allowed + ): + continue + rows = [row for row in owners if row["operation_id"] == identifier] + assert len(rows) <= 1, "duplicate owner rows for one operation" + if rows: + row = rows[0] + owner = OperationOwnerRecord.model_validate( + {key: value for key, value in row.items() if key != "operation_id"} + ) + assert owner.host_token is not None + result.append(AllowedRow("operation_owners", row, "added", {})) + return tuple(result) + + +def unchanged_or_operation_owner( + before: Snapshot, after: Snapshot, table: str, command: JsonRpcRequest +) -> bool: + """Keep domain equality exact; validate only this new request's companion owner.""" + if before.get(table, ()) == after.get(table, ()): + return True + if table != "operation_owners": + return False + candidates = [ + json.loads(row) + for row in after.get("operations", ()) + if json.loads(row)["method"] == command.method + and json.loads(row)["idempotency_key"] == command.params.meta.idempotency_key + and json.loads(row)["project_id"] == command.params.input["project_id"] + ] + assert len(candidates) == 1 + operation = candidates[0] + allowed = operation_owner_allowances( + before, after, (AllowedRow("operations", operation, "added", {}),) + ) + left, right = Counter(before.get(table, ())), Counter(after.get(table, ())) + assert not left - right, "owner removal or replacement is not bookkeeping" + added = list((right - left).elements()) + assert len(added) == 1 and len(allowed) == 1, "expected exactly this request's owner row" + assert allowed[0].matches(table, "added", json.loads(added[0])) + return True + + def failed_command_allowances( engine: Engine, command: JsonRpcRequest, *, expected_reads: tuple[str, ...] = () ) -> tuple[AllowedRow, ...]: diff --git a/tests/fixtures/sealable_methods.json b/tests/fixtures/sealable_methods.json new file mode 100644 index 0000000..24b8de3 --- /dev/null +++ b/tests/fixtures/sealable_methods.json @@ -0,0 +1,78 @@ +{ + "schema_version": "1.0.0", + "methods": [ + "action/authorization/decide", + "action/plan/revise", + "action/revise", + "action/select", + "action/step/add", + "action/step/revise", + "closure/decide", + "closure/finalize", + "closure/reopen", + "criteria/field/correct", + "criteria/profile/apply", + "execution/cancel", + "execution/observation/link", + "execution/pause", + "execution/resume", + "execution/retry", + "execution/start", + "hypothesis/assumption/add", + "hypothesis/causal/update", + "hypothesis/intent/update", + "hypothesis/prediction/bind", + "hypothesis/relation/add", + "hypothesis/relation/remove", + "hypothesis/revise", + "improvement/evaluation/assess", + "improvement/exposure/complete", + "improvement/exposure/decide", + "improvement/exposure/rollback", + "improvement/exposure/start", + "improvement/promotion/decide", + "improvement/revise", + "investigation/pause", + "investigation/resume", + "investigation/stop", + "investigation/update", + "memory/candidate/classify", + "memory/settings/update", + "model/callSettings/update", + "model/settings/update", + "object/attention/acknowledge", + "object/facet/update", + "object/frame/revise", + "object/profile/apply", + "object/relation/add", + "object/relation/remove", + "outcome/assess", + "outcome/observation/link", + "outcome/reassess", + "project/activate", + "project/cutoff/update", + "project/metadata/update", + "project/overlay/update", + "project/policy/update", + "project/reference/import", + "project/role/assign", + "project/role/revoke", + "project/source/scope/assign", + "project/source/scope/grant", + "project/source/scope/revoke", + "project/source/scope/update", + "project/source/time/confirm", + "project/source/time/correct", + "revision/baseline/decide", + "revision/changeSet/validate", + "revision/merge/resolve", + "thread/metadata/update", + "trace/confirm" + ], + "evidence": { + "source_head": "96a111a3294b73478aa3aed8233be5614183c191", + "path": "outputs/refactor-vs2-20261019/sealable-evidence.json", + "sha256": "02a6dd31f253b1c243d636821c372a72555231f09d20f5c4d1bd4f12a46f57a6", + "approved_methods_sha256": "83993c93cbe89314247caa791d292cbc806a73d0c955d424d8fcf48ebeb18da4" + } +} diff --git a/tests/fixtures/storage_migration_ddl.json b/tests/fixtures/storage_migration_ddl.json index 1f64ae6..b2cfd0c 100644 --- a/tests/fixtures/storage_migration_ddl.json +++ b/tests/fixtures/storage_migration_ddl.json @@ -1,7 +1,7 @@ { "contract": { "heads": [ - "3b06e4f87d92" + "4c17f5089ea3" ], "schema_rows": [ [ @@ -1726,6 +1726,12 @@ "object_relations", null ], + [ + "index", + "sqlite_autoindex_operation_owners_1", + "operation_owners", + null + ], [ "index", "sqlite_autoindex_operation_resource_bindings_1", @@ -2524,6 +2530,12 @@ "object_relations", "CREATE TABLE object_relations (\n\trelation_revision_id VARCHAR(160) NOT NULL, \n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_object_id VARCHAR(160) NOT NULL, \n\trevision_digest VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tcreated_at VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (relation_revision_id)\n)" ], + [ + "table", + "operation_owners", + "operation_owners", + "CREATE TABLE operation_owners (\n\toperation_id VARCHAR(160) NOT NULL, \n\tworker_id VARCHAR(160) NOT NULL, \n\tprocess_id INTEGER NOT NULL, \n\tprocess_marker VARCHAR(160) NOT NULL, \n\thost_token VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (operation_id)\n)" + ], [ "table", "operation_resource_bindings", diff --git a/tests/fixtures/storage_schema_contract.json b/tests/fixtures/storage_schema_contract.json index b6a2dc4..9909152 100644 --- a/tests/fixtures/storage_schema_contract.json +++ b/tests/fixtures/storage_schema_contract.json @@ -95,6 +95,8 @@ "thoth.adapters.storage.memory.memory_records": "memory_records", "thoth.adapters.storage.memory.memory_revision_ledger": "memory_revision_ledger", "thoth.adapters.storage.memory.memory_transition_receipts": "memory_transition_receipts", + "thoth.adapters.storage.operation_owner_schema.operation_owners": "operation_owners", + "thoth.adapters.storage.operation_owner_store.operation_owners": "operation_owners", "thoth.adapters.storage.operations.idempotency_keys": "idempotency_keys", "thoth.adapters.storage.operations.memory_records": "memory_records", "thoth.adapters.storage.operations.memory_revision_ledger": "memory_revision_ledger", @@ -329,6 +331,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -606,6 +609,7 @@ "object_candidates", "object_profiles", "object_relations", + "operation_owners", "operation_resource_bindings", "operations", "organizations", @@ -754,6 +758,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -2325,6 +2330,3260 @@ }, "sql": "\nCREATE TABLE object_relations (\n\trelation_revision_id VARCHAR(160) NOT NULL, \n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_object_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (relation_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" }, + "operation_owners": { + "columns": [ + "operation_id", + "worker_id", + "process_id", + "process_marker", + "host_token" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE operation_owners (\n\toperation_id VARCHAR(160) NOT NULL, \n\tworker_id VARCHAR(160) NOT NULL, \n\tprocess_id INTEGER NOT NULL, \n\tprocess_marker VARCHAR(160) NOT NULL, \n\thost_token VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (operation_id)\n)\n\n" + }, + "operation_resource_bindings": { + "columns": [ + "operation_id", + "project_id", + "binding_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE operation_resource_bindings (\n\toperation_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tbinding_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (operation_id)\n)\n\n" + }, + "operations": { + "columns": [ + "operation_id", + "project_id", + "method", + "idempotency_key", + "scope_digest", + "owner_actor_id", + "owner_session_id", + "owner_role_assignment_id", + "owner_data_scopes_json", + "state", + "result_json", + "error_json", + "created_at", + "completed_at", + "epoch" + ], + "constraint_names": [ + null, + "uq_operation_idempotency_scope" + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_operations_project_id": "CREATE INDEX ix_operations_project_id ON operations (project_id)" + }, + "sql": "\nCREATE TABLE operations (\n\toperation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tmethod VARCHAR(160) NOT NULL, \n\tidempotency_key VARCHAR(260) NOT NULL, \n\tscope_digest VARCHAR(64) NOT NULL, \n\towner_actor_id VARCHAR(160), \n\towner_session_id VARCHAR(160), \n\towner_role_assignment_id VARCHAR(160), \n\towner_data_scopes_json TEXT DEFAULT '[]' NOT NULL, \n\tstate VARCHAR(40) NOT NULL, \n\tresult_json TEXT, \n\terror_json TEXT, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tcompleted_at VARCHAR(32), \n\tepoch INTEGER DEFAULT '0' NOT NULL, \n\tPRIMARY KEY (operation_id), \n\tCONSTRAINT uq_operation_idempotency_scope UNIQUE (project_id, method, idempotency_key)\n)\n\n" + }, + "organizations": { + "columns": [ + "organization_id", + "name", + "kind", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE organizations (\n\torganization_id VARCHAR(160) NOT NULL, \n\tname TEXT NOT NULL, \n\tkind VARCHAR(80) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (organization_id)\n)\n\n" + }, + "outcome_assessments": { + "columns": [ + "assessment_revision_id", + "outcome_assessment_id", + "project_id", + "outcome_series_id", + "object_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_outcome_assessments_object_id": "CREATE INDEX ix_outcome_assessments_object_id ON outcome_assessments (object_id)", + "ix_outcome_assessments_outcome_assessment_id": "CREATE INDEX ix_outcome_assessments_outcome_assessment_id ON outcome_assessments (outcome_assessment_id)", + "ix_outcome_assessments_outcome_series_id": "CREATE INDEX ix_outcome_assessments_outcome_series_id ON outcome_assessments (outcome_series_id)", + "ix_outcome_assessments_project_id": "CREATE INDEX ix_outcome_assessments_project_id ON outcome_assessments (project_id)" + }, + "sql": "\nCREATE TABLE outcome_assessments (\n\tassessment_revision_id VARCHAR(160) NOT NULL, \n\toutcome_assessment_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\toutcome_series_id VARCHAR(160) NOT NULL, \n\tobject_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (assessment_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "outcome_attributions": { + "columns": [ + "attribution_assessment_id", + "project_id", + "outcome_assessment_id", + "content_json", + "attribution_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_outcome_attributions_outcome_assessment_id": "CREATE INDEX ix_outcome_attributions_outcome_assessment_id ON outcome_attributions (outcome_assessment_id)", + "ix_outcome_attributions_project_id": "CREATE INDEX ix_outcome_attributions_project_id ON outcome_attributions (project_id)" + }, + "sql": "\nCREATE TABLE outcome_attributions (\n\tattribution_assessment_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\toutcome_assessment_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tattribution_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (attribution_assessment_id), \n\tUNIQUE (attribution_digest)\n)\n\n" + }, + "outcome_audit": { + "columns": [ + "audit_id", + "project_id", + "subject_id", + "event_type", + "payload_json", + "event_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_outcome_audit_project_id": "CREATE INDEX ix_outcome_audit_project_id ON outcome_audit (project_id)", + "ix_outcome_audit_subject_id": "CREATE INDEX ix_outcome_audit_subject_id ON outcome_audit (subject_id)" + }, + "sql": "\nCREATE TABLE outcome_audit (\n\taudit_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsubject_id VARCHAR(160) NOT NULL, \n\tevent_type VARCHAR(120) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tevent_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (audit_id), \n\tUNIQUE (event_digest)\n)\n\n" + }, + "outcome_change_sets": { + "columns": [ + "outcome_change_set_id", + "project_id", + "outcome_assessment_id", + "content_json", + "change_set_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_outcome_change_sets_outcome_assessment_id": "CREATE INDEX ix_outcome_change_sets_outcome_assessment_id ON outcome_change_sets (outcome_assessment_id)", + "ix_outcome_change_sets_project_id": "CREATE INDEX ix_outcome_change_sets_project_id ON outcome_change_sets (project_id)" + }, + "sql": "\nCREATE TABLE outcome_change_sets (\n\toutcome_change_set_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\toutcome_assessment_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tchange_set_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (outcome_change_set_id), \n\tUNIQUE (change_set_digest)\n)\n\n" + }, + "outcome_impacts": { + "columns": [ + "impact_assessment_id", + "project_id", + "outcome_series_id", + "content_json", + "impact_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_outcome_impacts_outcome_series_id": "CREATE INDEX ix_outcome_impacts_outcome_series_id ON outcome_impacts (outcome_series_id)", + "ix_outcome_impacts_project_id": "CREATE INDEX ix_outcome_impacts_project_id ON outcome_impacts (project_id)" + }, + "sql": "\nCREATE TABLE outcome_impacts (\n\timpact_assessment_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\toutcome_series_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\timpact_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (impact_assessment_id), \n\tUNIQUE (impact_digest)\n)\n\n" + }, + "outcome_profiles": { + "columns": [ + "profile_ref", + "version", + "content_json", + "enabled", + "profile_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE outcome_profiles (\n\tprofile_ref VARCHAR(160) NOT NULL, \n\tversion INTEGER NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tenabled INTEGER NOT NULL, \n\tprofile_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (profile_ref, version), \n\tUNIQUE (profile_digest)\n)\n\n" + }, + "outcome_series": { + "columns": [ + "series_revision_id", + "outcome_series_id", + "project_id", + "object_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_outcome_series_object_id": "CREATE INDEX ix_outcome_series_object_id ON outcome_series (object_id)", + "ix_outcome_series_outcome_series_id": "CREATE INDEX ix_outcome_series_outcome_series_id ON outcome_series (outcome_series_id)", + "ix_outcome_series_project_id": "CREATE INDEX ix_outcome_series_project_id ON outcome_series (project_id)" + }, + "sql": "\nCREATE TABLE outcome_series (\n\tseries_revision_id VARCHAR(160) NOT NULL, \n\toutcome_series_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tobject_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (series_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "plan_executions": { + "columns": [ + "execution_revision_id", + "plan_execution_id", + "project_id", + "object_id", + "plan_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_plan_executions_object_id": "CREATE INDEX ix_plan_executions_object_id ON plan_executions (object_id)", + "ix_plan_executions_plan_execution_id": "CREATE INDEX ix_plan_executions_plan_execution_id ON plan_executions (plan_execution_id)", + "ix_plan_executions_plan_id": "CREATE INDEX ix_plan_executions_plan_id ON plan_executions (plan_id)", + "ix_plan_executions_project_id": "CREATE INDEX ix_plan_executions_project_id ON plan_executions (project_id)" + }, + "sql": "\nCREATE TABLE plan_executions (\n\texecution_revision_id VARCHAR(160) NOT NULL, \n\tplan_execution_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tobject_id VARCHAR(160) NOT NULL, \n\tplan_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (execution_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "project_head_sets": { + "columns": [ + "project_id", + "revision", + "head_set_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE project_head_sets (\n\tproject_id VARCHAR(160) NOT NULL, \n\trevision INTEGER NOT NULL, \n\thead_set_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (project_id)\n)\n\n" + }, + "project_policies": { + "columns": [ + "policy_id", + "project_id", + "version", + "payload_json", + "policy_digest", + "created_at" + ], + "constraint_names": [ + null, + "uq_project_policy_version" + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_project_policies_project_id": "CREATE INDEX ix_project_policies_project_id ON project_policies (project_id)" + }, + "sql": "\nCREATE TABLE project_policies (\n\tpolicy_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tversion INTEGER NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tpolicy_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (policy_id), \n\tCONSTRAINT uq_project_policy_version UNIQUE (project_id, version)\n)\n\n" + }, + "project_references": { + "columns": [ + "reference_id", + "project_id", + "origin_project_id", + "origin_revision", + "rights_status", + "scope", + "authority_status", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_project_references_project_id": "CREATE INDEX ix_project_references_project_id ON project_references (project_id)" + }, + "sql": "\nCREATE TABLE project_references (\n\treference_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\torigin_project_id VARCHAR(160) NOT NULL, \n\torigin_revision VARCHAR(160) NOT NULL, \n\trights_status VARCHAR(80) NOT NULL, \n\tscope VARCHAR(260) NOT NULL, \n\tauthority_status VARCHAR(80) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (reference_id)\n)\n\n" + }, + "project_roles": { + "columns": [ + "role_assignment_id", + "project_id", + "actor_id", + "organization_id", + "role", + "scope", + "authority_tags_json", + "state", + "created_at", + "revoked_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_project_roles_actor_id": "CREATE INDEX ix_project_roles_actor_id ON project_roles (actor_id)", + "ix_project_roles_project_id": "CREATE INDEX ix_project_roles_project_id ON project_roles (project_id)" + }, + "sql": "\nCREATE TABLE project_roles (\n\trole_assignment_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tactor_id VARCHAR(160) NOT NULL, \n\torganization_id VARCHAR(160), \n\trole VARCHAR(160) NOT NULL, \n\tscope VARCHAR(260) NOT NULL, \n\tauthority_tags_json TEXT NOT NULL, \n\tstate VARCHAR(40) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\trevoked_at VARCHAR(32), \n\tPRIMARY KEY (role_assignment_id)\n)\n\n" + }, + "projects": { + "columns": [ + "project_id", + "name", + "description", + "cutoff_at", + "lifecycle", + "overlay", + "policy_ref", + "created_at", + "revision" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE projects (\n\tproject_id VARCHAR(160) NOT NULL, \n\tname TEXT NOT NULL, \n\tdescription TEXT DEFAULT '' NOT NULL, \n\tcutoff_at VARCHAR(32) NOT NULL, \n\tlifecycle VARCHAR(40) NOT NULL, \n\toverlay VARCHAR(160) DEFAULT 'default' NOT NULL, \n\tpolicy_ref VARCHAR(160) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\trevision INTEGER DEFAULT '0' NOT NULL, \n\tPRIMARY KEY (project_id)\n)\n\n" + }, + "receipt_dag_bundles": { + "columns": [ + "bundle_id", + "project_id", + "content_json", + "bundle_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_receipt_dag_bundles_project_id": "CREATE INDEX ix_receipt_dag_bundles_project_id ON receipt_dag_bundles (project_id)" + }, + "sql": "\nCREATE TABLE receipt_dag_bundles (\n\tbundle_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tbundle_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (bundle_id), \n\tUNIQUE (bundle_digest)\n)\n\n" + }, + "receipt_dag_edges": { + "columns": [ + "edge_id", + "project_id", + "parent_node_id", + "child_node_id", + "content_json", + "edge_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_receipt_dag_edges_project_id": "CREATE INDEX ix_receipt_dag_edges_project_id ON receipt_dag_edges (project_id)" + }, + "sql": "\nCREATE TABLE receipt_dag_edges (\n\tedge_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tparent_node_id VARCHAR(200) NOT NULL, \n\tchild_node_id VARCHAR(200) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tedge_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (edge_id, project_id), \n\tUNIQUE (edge_digest)\n)\n\n" + }, + "receipt_dag_manifests": { + "columns": [ + "manifest_id", + "project_id", + "content_json", + "manifest_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE receipt_dag_manifests (\n\tmanifest_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tmanifest_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (manifest_id), \n\tUNIQUE (project_id)\n)\n\n" + }, + "receipt_dag_nodes": { + "columns": [ + "node_id", + "project_id", + "kind", + "content_json", + "node_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_receipt_dag_nodes_kind": "CREATE INDEX ix_receipt_dag_nodes_kind ON receipt_dag_nodes (kind)", + "ix_receipt_dag_nodes_project_id": "CREATE INDEX ix_receipt_dag_nodes_project_id ON receipt_dag_nodes (project_id)" + }, + "sql": "\nCREATE TABLE receipt_dag_nodes (\n\tnode_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tkind VARCHAR(40) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tnode_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (node_id, project_id), \n\tUNIQUE (node_digest)\n)\n\n" + }, + "receipt_dag_verifications": { + "columns": [ + "verification_id", + "project_id", + "subject_id", + "content_json", + "verification_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_receipt_dag_verifications_project_id": "CREATE INDEX ix_receipt_dag_verifications_project_id ON receipt_dag_verifications (project_id)", + "ix_receipt_dag_verifications_subject_id": "CREATE INDEX ix_receipt_dag_verifications_subject_id ON receipt_dag_verifications (subject_id)" + }, + "sql": "\nCREATE TABLE receipt_dag_verifications (\n\tverification_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsubject_id VARCHAR(200) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tverification_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (verification_id), \n\tUNIQUE (verification_digest)\n)\n\n" + }, + "receipts": { + "columns": [ + "receipt_id", + "project_id", + "receipt_type", + "payload_json", + "receipt_digest", + "previous_transition_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_receipts_project_id": "CREATE INDEX ix_receipts_project_id ON receipts (project_id)" + }, + "sql": "\nCREATE TABLE receipts (\n\treceipt_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\treceipt_type VARCHAR(40) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\treceipt_digest VARCHAR(64) NOT NULL, \n\tprevious_transition_digest VARCHAR(64), \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (receipt_id), \n\tUNIQUE (receipt_digest)\n)\n\n" + }, + "relations": { + "columns": [ + "relation_id", + "project_id", + "source_ref", + "relation_type", + "target_ref", + "payload_json", + "revision_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_relations_project_id": "CREATE INDEX ix_relations_project_id ON relations (project_id)", + "ix_relations_source_ref": "CREATE INDEX ix_relations_source_ref ON relations (source_ref)" + }, + "sql": "\nCREATE TABLE relations (\n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_ref VARCHAR(260) NOT NULL, \n\trelation_type VARCHAR(80) NOT NULL, \n\ttarget_ref VARCHAR(260) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (relation_id)\n)\n\n" + }, + "research_identities": { + "columns": [ + "project_id", + "owner_revision", + "entity_id", + "aggregate_kind", + "object_id", + "schema_family", + "snapshot_id", + "content_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_research_identity_entity": "CREATE INDEX ix_research_identity_entity ON research_identities (entity_id, aggregate_kind)" + }, + "sql": "\nCREATE TABLE research_identities (\n\tproject_id VARCHAR(160) NOT NULL, \n\towner_revision VARCHAR(64) NOT NULL, \n\tentity_id VARCHAR(200) NOT NULL, \n\taggregate_kind VARCHAR(40) NOT NULL, \n\tobject_id VARCHAR(200), \n\tschema_family VARCHAR(80) NOT NULL, \n\tsnapshot_id VARCHAR(200) NOT NULL, \n\tcontent_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (project_id, owner_revision)\n)\n\n" + }, + "research_test_assessments": { + "columns": [ + "project_id", + "assessment_id", + "prediction_id", + "attempt_ref", + "assessment_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "uq_research_test_prediction_attempt": "CREATE UNIQUE INDEX uq_research_test_prediction_attempt ON research_test_assessments (project_id, prediction_id, attempt_ref)" + }, + "sql": "\nCREATE TABLE research_test_assessments (\n\tproject_id VARCHAR(160) NOT NULL, \n\tassessment_id VARCHAR(200) NOT NULL, \n\tprediction_id VARCHAR(200) NOT NULL, \n\tattempt_ref VARCHAR(200) NOT NULL, \n\tassessment_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (project_id, assessment_id)\n)\n\n" + }, + "resource_scope_heads": { + "columns": [ + "project_id", + "resource_ref", + "revision", + "record_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE resource_scope_heads (\n\tproject_id VARCHAR(160) NOT NULL, \n\tresource_ref VARCHAR(260) NOT NULL, \n\trevision INTEGER NOT NULL, \n\trecord_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (project_id, resource_ref)\n)\n\n" + }, + "resource_scope_history": { + "columns": [ + "project_id", + "resource_ref", + "revision", + "record_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE resource_scope_history (\n\tproject_id VARCHAR(160) NOT NULL, \n\tresource_ref VARCHAR(260) NOT NULL, \n\trevision INTEGER NOT NULL, \n\trecord_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (project_id, resource_ref, revision)\n)\n\n" + }, + "resource_scope_receipts": { + "columns": [ + "project_id", + "receipt_id", + "resource_ref", + "receipt_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE resource_scope_receipts (\n\tproject_id VARCHAR(160) NOT NULL, \n\treceipt_id VARCHAR(200) NOT NULL, \n\tresource_ref VARCHAR(260) NOT NULL, \n\treceipt_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (project_id, receipt_id)\n)\n\n" + }, + "revision_parents": { + "columns": [ + "revision_id", + "ordinal", + "parent_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "ForeignKeyConstraint", + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE revision_parents (\n\trevision_id VARCHAR(160) NOT NULL, \n\tordinal INTEGER NOT NULL, \n\tparent_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (revision_id, ordinal), \n\tFOREIGN KEY(revision_id) REFERENCES semantic_revisions (revision_id)\n)\n\n" + }, + "semantic_revisions": { + "columns": [ + "revision_id", + "project_id", + "entity_type", + "entity_id", + "snapshot_id", + "actor_json", + "reason", + "evidence_refs_json", + "affected_refs_json", + "revision_digest", + "created_at", + "schema_version" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "ForeignKeyConstraint", + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_semantic_revisions_entity_id": "CREATE INDEX ix_semantic_revisions_entity_id ON semantic_revisions (entity_id)", + "ix_semantic_revisions_entity_type": "CREATE INDEX ix_semantic_revisions_entity_type ON semantic_revisions (entity_type)", + "ix_semantic_revisions_project_id": "CREATE INDEX ix_semantic_revisions_project_id ON semantic_revisions (project_id)" + }, + "sql": "\nCREATE TABLE semantic_revisions (\n\trevision_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tentity_type VARCHAR(60) NOT NULL, \n\tentity_id VARCHAR(160) NOT NULL, \n\tsnapshot_id VARCHAR(160) NOT NULL, \n\tactor_json TEXT NOT NULL, \n\treason TEXT NOT NULL, \n\tevidence_refs_json TEXT NOT NULL, \n\taffected_refs_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tschema_version VARCHAR(40) NOT NULL, \n\tPRIMARY KEY (revision_id), \n\tFOREIGN KEY(snapshot_id) REFERENCES entity_snapshots (snapshot_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "source_bindings": { + "columns": [ + "binding_id", + "project_id", + "artifact_id", + "capability", + "state", + "created_at", + "updated_at" + ], + "constraint_names": [ + null, + "uq_source_binding_artifact" + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_source_bindings_artifact_id": "CREATE INDEX ix_source_bindings_artifact_id ON source_bindings (artifact_id)", + "ix_source_bindings_project_id": "CREATE INDEX ix_source_bindings_project_id ON source_bindings (project_id)" + }, + "sql": "\nCREATE TABLE source_bindings (\n\tbinding_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tartifact_id VARCHAR(160) NOT NULL, \n\tcapability VARCHAR(40) NOT NULL, \n\tstate VARCHAR(40) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tupdated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (binding_id), \n\tCONSTRAINT uq_source_binding_artifact UNIQUE (project_id, artifact_id)\n)\n\n" + }, + "span_corrections": { + "columns": [ + "correction_id", + "project_id", + "span_id", + "corrected_text", + "reason", + "correction_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_span_corrections_project_id": "CREATE INDEX ix_span_corrections_project_id ON span_corrections (project_id)", + "ix_span_corrections_span_id": "CREATE INDEX ix_span_corrections_span_id ON span_corrections (span_id)" + }, + "sql": "\nCREATE TABLE span_corrections (\n\tcorrection_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tspan_id VARCHAR(160) NOT NULL, \n\tcorrected_text TEXT NOT NULL, \n\treason TEXT NOT NULL, \n\tcorrection_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (correction_id), \n\tUNIQUE (correction_digest)\n)\n\n" + }, + "step_execution_attempts": { + "columns": [ + "attempt_revision_id", + "attempt_id", + "project_id", + "plan_execution_id", + "step_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_step_execution_attempts_attempt_id": "CREATE INDEX ix_step_execution_attempts_attempt_id ON step_execution_attempts (attempt_id)", + "ix_step_execution_attempts_plan_execution_id": "CREATE INDEX ix_step_execution_attempts_plan_execution_id ON step_execution_attempts (plan_execution_id)", + "ix_step_execution_attempts_project_id": "CREATE INDEX ix_step_execution_attempts_project_id ON step_execution_attempts (project_id)", + "ix_step_execution_attempts_step_id": "CREATE INDEX ix_step_execution_attempts_step_id ON step_execution_attempts (step_id)" + }, + "sql": "\nCREATE TABLE step_execution_attempts (\n\tattempt_revision_id VARCHAR(160) NOT NULL, \n\tattempt_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tplan_execution_id VARCHAR(160) NOT NULL, \n\tstep_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (attempt_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "structural_nodes": { + "columns": [ + "node_id", + "project_id", + "artifact_id", + "parent_id", + "kind", + "ordinal", + "text", + "locator_json", + "source_version_id", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_structural_nodes_artifact_id": "CREATE INDEX ix_structural_nodes_artifact_id ON structural_nodes (artifact_id)", + "ix_structural_nodes_project_id": "CREATE INDEX ix_structural_nodes_project_id ON structural_nodes (project_id)", + "ix_structural_nodes_source_version_id": "CREATE INDEX ix_structural_nodes_source_version_id ON structural_nodes (source_version_id)" + }, + "sql": "\nCREATE TABLE structural_nodes (\n\tnode_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tartifact_id VARCHAR(160) NOT NULL, \n\tparent_id VARCHAR(160), \n\tkind VARCHAR(60), \n\tordinal INTEGER, \n\ttext TEXT, \n\tlocator_json TEXT NOT NULL, \n\tsource_version_id VARCHAR(160), \n\tcontent_json TEXT, \n\tPRIMARY KEY (node_id)\n)\n\n" + }, + "thread_activities": { + "columns": [ + "activity_id", + "project_id", + "thread_id", + "cycle_id", + "event_type", + "payload_json", + "actor_id", + "activity_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_thread_activities_project_id": "CREATE INDEX ix_thread_activities_project_id ON thread_activities (project_id)", + "ix_thread_activities_thread_id": "CREATE INDEX ix_thread_activities_thread_id ON thread_activities (thread_id)" + }, + "sql": "\nCREATE TABLE thread_activities (\n\tactivity_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tthread_id VARCHAR(160) NOT NULL, \n\tcycle_id VARCHAR(160) NOT NULL, \n\tevent_type VARCHAR(120) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tactor_id VARCHAR(160) NOT NULL, \n\tactivity_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (activity_id), \n\tUNIQUE (activity_digest)\n)\n\n" + }, + "thread_checkpoints": { + "columns": [ + "checkpoint_id", + "project_id", + "thread_id", + "cycle_id", + "head_set_digest", + "payload_json", + "checkpoint_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_thread_checkpoints_project_id": "CREATE INDEX ix_thread_checkpoints_project_id ON thread_checkpoints (project_id)", + "ix_thread_checkpoints_thread_id": "CREATE INDEX ix_thread_checkpoints_thread_id ON thread_checkpoints (thread_id)" + }, + "sql": "\nCREATE TABLE thread_checkpoints (\n\tcheckpoint_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tthread_id VARCHAR(160) NOT NULL, \n\tcycle_id VARCHAR(160) NOT NULL, \n\thead_set_digest VARCHAR(64) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tcheckpoint_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (checkpoint_id), \n\tUNIQUE (checkpoint_digest)\n)\n\n" + }, + "thread_inputs": { + "columns": [ + "input_id", + "project_id", + "thread_id", + "kind", + "text", + "state", + "ordinal", + "created_at" + ], + "constraint_names": [ + null, + "uq_thread_input_ordinal" + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_thread_inputs_project_id": "CREATE INDEX ix_thread_inputs_project_id ON thread_inputs (project_id)", + "ix_thread_inputs_thread_id": "CREATE INDEX ix_thread_inputs_thread_id ON thread_inputs (thread_id)" + }, + "sql": "\nCREATE TABLE thread_inputs (\n\tinput_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tthread_id VARCHAR(160) NOT NULL, \n\tkind VARCHAR(40) NOT NULL, \n\ttext TEXT NOT NULL, \n\tstate VARCHAR(40) NOT NULL, \n\tordinal INTEGER NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (input_id), \n\tCONSTRAINT uq_thread_input_ordinal UNIQUE (thread_id, ordinal)\n)\n\n" + }, + "threads": { + "columns": [ + "thread_id", + "project_id", + "cycle_id", + "problem", + "display_name", + "scope_json", + "parent_thread_id", + "fork_origin", + "lifecycle", + "execution_state", + "current_object_ids_json", + "working_head_digest", + "revision", + "created_at", + "updated_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_threads_project_id": "CREATE INDEX ix_threads_project_id ON threads (project_id)" + }, + "sql": "\nCREATE TABLE threads (\n\tthread_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tcycle_id VARCHAR(160) NOT NULL, \n\tproblem TEXT NOT NULL, \n\tdisplay_name TEXT DEFAULT '' NOT NULL, \n\tscope_json TEXT DEFAULT '{}' NOT NULL, \n\tparent_thread_id VARCHAR(160), \n\tfork_origin VARCHAR(160), \n\tlifecycle VARCHAR(60) NOT NULL, \n\texecution_state VARCHAR(60) NOT NULL, \n\tcurrent_object_ids_json TEXT NOT NULL, \n\tworking_head_digest VARCHAR(64) NOT NULL, \n\trevision INTEGER DEFAULT '0' NOT NULL, \n\tcreated_at VARCHAR(32) DEFAULT '1970-01-01T00:00:00Z' NOT NULL, \n\tupdated_at VARCHAR(32) DEFAULT '1970-01-01T00:00:00Z' NOT NULL, \n\tPRIMARY KEY (thread_id)\n)\n\n" + }, + "tui_sessions": { + "columns": [ + "session_id", + "revision", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE tui_sessions (\n\tsession_id VARCHAR(200) NOT NULL, \n\trevision INTEGER NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (session_id)\n)\n\n" + }, + "working_heads": { + "columns": [ + "project_id", + "aggregate_key", + "revision_digest", + "updated_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE working_heads (\n\tproject_id VARCHAR(160) NOT NULL, \n\taggregate_key VARCHAR(260) NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tupdated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (project_id, aggregate_key)\n)\n\n" + }, + "workstreams": { + "columns": [ + "workstream_id", + "project_id", + "name", + "parent_workstream_id", + "owner_role_assignment_id", + "state", + "revision" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_workstreams_project_id": "CREATE INDEX ix_workstreams_project_id ON workstreams (project_id)" + }, + "sql": "\nCREATE TABLE workstreams (\n\tworkstream_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tname TEXT NOT NULL, \n\tparent_workstream_id VARCHAR(160), \n\towner_role_assignment_id VARCHAR(160), \n\tstate VARCHAR(40) NOT NULL, \n\trevision INTEGER DEFAULT '0' NOT NULL, \n\tPRIMARY KEY (workstream_id)\n)\n\n" + } + } + }, + "thoth.adapters.storage.evaluation_run_schema": { + "consumer_aliases": { + "thoth.adapters.storage.acquisition.artifact_versions": "artifact_versions", + "thoth.adapters.storage.acquisition.artifacts": "artifacts", + "thoth.adapters.storage.acquisition.control_records": "control_records", + "thoth.adapters.storage.acquisition.evidence_audit": "evidence_audit", + "thoth.adapters.storage.acquisition.evidence_sources": "evidence_sources", + "thoth.adapters.storage.acquisition.evidence_spans": "evidence_spans", + "thoth.adapters.storage.acquisition.project_policies": "project_policies", + "thoth.adapters.storage.acquisition.projects": "projects", + "thoth.adapters.storage.acquisition.source_bindings": "source_bindings", + "thoth.adapters.storage.acquisition.structural_nodes": "structural_nodes", + "thoth.adapters.storage.acquisition_trace.acquisition_search_intents": "acquisition_search_intents", + "thoth.adapters.storage.acquisition_trace.evidence_audit": "evidence_audit", + "thoth.adapters.storage.acquisition_trace.evidence_leads": "evidence_leads", + "thoth.adapters.storage.acquisition_trace.evidence_links": "evidence_links", + "thoth.adapters.storage.acquisition_trace.evidence_observations": "evidence_observations", + "thoth.adapters.storage.action.action_audit": "action_audit", + "thoth.adapters.storage.action.action_authorizations": "action_authorizations", + "thoth.adapters.storage.action.action_plans": "action_plans", + "thoth.adapters.storage.action.action_portfolios": "action_portfolios", + "thoth.adapters.storage.action.action_records": "action_records", + "thoth.adapters.storage.artifacts.artifact_versions": "artifact_versions", + "thoth.adapters.storage.artifacts.artifacts": "artifacts", + "thoth.adapters.storage.artifacts.evidence_spans": "evidence_spans", + "thoth.adapters.storage.artifacts.structural_nodes": "structural_nodes", + "thoth.adapters.storage.auth.auth_sessions": "auth_sessions", + "thoth.adapters.storage.baseline.baseline_candidates": "baseline_candidates", + "thoth.adapters.storage.baseline.baseline_decisions": "baseline_decisions", + "thoth.adapters.storage.baseline.baseline_sets": "baseline_sets", + "thoth.adapters.storage.baseline.project_head_sets": "project_head_sets", + "thoth.adapters.storage.behavior_artifact.behavior_artifacts": "behavior_artifacts", + "thoth.adapters.storage.behavior_artifact.behavior_registry": "behavior_registry", + "thoth.adapters.storage.behavior_execution_schema.behavior_baseline_history": "behavior_baseline_history", + "thoth.adapters.storage.behavior_execution_schema.behavior_exposure_history": "behavior_exposure_history", + "thoth.adapters.storage.control_record.control_records": "control_records", + "thoth.adapters.storage.conversation.tui_sessions": "tui_sessions", + "thoth.adapters.storage.criterion_contract.criterion_audit": "criterion_audit", + "thoth.adapters.storage.criterion_contract.criterion_conflicts": "criterion_conflicts", + "thoth.adapters.storage.criterion_contract.criterion_contracts": "criterion_contracts", + "thoth.adapters.storage.criterion_contract.criterion_profiles": "criterion_profiles", + "thoth.adapters.storage.criterion_contract.criterion_references": "criterion_references", + "thoth.adapters.storage.decision_object.decision_objects": "decision_objects", + "thoth.adapters.storage.decision_object.object_attention": "object_attention", + "thoth.adapters.storage.decision_object.object_audit": "object_audit", + "thoth.adapters.storage.decision_object.object_candidates": "object_candidates", + "thoth.adapters.storage.decision_object.object_profiles": "object_profiles", + "thoth.adapters.storage.decision_object.object_relations": "object_relations", + "thoth.adapters.storage.dependency.dependency_states": "dependency_states", + "thoth.adapters.storage.dependency.relations": "relations", + "thoth.adapters.storage.evaluation_run_schema.evaluation_runs": "evaluation_runs", + "thoth.adapters.storage.events.checkpoints": "checkpoints", + "thoth.adapters.storage.events.events": "events", + "thoth.adapters.storage.evidence_graph.evidence_audit": "evidence_audit", + "thoth.adapters.storage.evidence_graph.evidence_conflicts": "evidence_conflicts", + "thoth.adapters.storage.evidence_graph.evidence_links": "evidence_links", + "thoth.adapters.storage.evidence_graph.evidence_observations": "evidence_observations", + "thoth.adapters.storage.evidence_graph.evidence_sources": "evidence_sources", + "thoth.adapters.storage.evidence_graph.span_corrections": "span_corrections", + "thoth.adapters.storage.execution.execution_audit": "execution_audit", + "thoth.adapters.storage.execution.execution_effects": "execution_effects", + "thoth.adapters.storage.execution.execution_reconciliations": "execution_reconciliations", + "thoth.adapters.storage.execution.plan_executions": "plan_executions", + "thoth.adapters.storage.execution.step_execution_attempts": "step_execution_attempts", + "thoth.adapters.storage.field_measurement.field_events": "field_events", + "thoth.adapters.storage.field_measurement.field_exports": "field_exports", + "thoth.adapters.storage.field_measurement.field_protocol_seals": "field_protocol_seals", + "thoth.adapters.storage.field_measurement.field_scores": "field_scores", + "thoth.adapters.storage.field_measurement.field_session_metrics": "field_session_metrics", + "thoth.adapters.storage.field_measurement.field_sessions": "field_sessions", + "thoth.adapters.storage.governance.organizations": "organizations", + "thoth.adapters.storage.governance.project_policies": "project_policies", + "thoth.adapters.storage.governance.project_references": "project_references", + "thoth.adapters.storage.governance.project_roles": "project_roles", + "thoth.adapters.storage.governance.source_bindings": "source_bindings", + "thoth.adapters.storage.governance.workstreams": "workstreams", + "thoth.adapters.storage.governance_history.heads": "governance_heads", + "thoth.adapters.storage.governance_history.revisions": "governance_revisions", + "thoth.adapters.storage.governance_history_schema.governance_heads": "governance_heads", + "thoth.adapters.storage.governance_history_schema.governance_revisions": "governance_revisions", + "thoth.adapters.storage.hypothesis.hypothesis_appraisals": "hypothesis_appraisals", + "thoth.adapters.storage.hypothesis.hypothesis_assumptions": "hypothesis_assumptions", + "thoth.adapters.storage.hypothesis.hypothesis_audit": "hypothesis_audit", + "thoth.adapters.storage.hypothesis.hypothesis_portfolios": "hypothesis_portfolios", + "thoth.adapters.storage.hypothesis.hypothesis_predictions": "hypothesis_predictions", + "thoth.adapters.storage.hypothesis.hypothesis_records": "hypothesis_records", + "thoth.adapters.storage.hypothesis.hypothesis_relations": "hypothesis_relations", + "thoth.adapters.storage.hypothesis.hypothesis_test_bindings": "hypothesis_test_bindings", + "thoth.adapters.storage.investigation.investigation_audit": "investigation_audit", + "thoth.adapters.storage.investigation.investigations": "investigations", + "thoth.adapters.storage.memory.memory_context_packs": "memory_context_packs", + "thoth.adapters.storage.memory.memory_projections": "memory_projections", + "thoth.adapters.storage.memory.memory_records": "memory_records", + "thoth.adapters.storage.memory.memory_revision_ledger": "memory_revision_ledger", + "thoth.adapters.storage.memory.memory_transition_receipts": "memory_transition_receipts", + "thoth.adapters.storage.operation_owner_schema.operation_owners": "operation_owners", + "thoth.adapters.storage.operation_owner_store.operation_owners": "operation_owners", + "thoth.adapters.storage.operations.idempotency_keys": "idempotency_keys", + "thoth.adapters.storage.operations.memory_records": "memory_records", + "thoth.adapters.storage.operations.memory_revision_ledger": "memory_revision_ledger", + "thoth.adapters.storage.operations.memory_transition_receipts": "memory_transition_receipts", + "thoth.adapters.storage.operations.operation_resource_bindings": "operation_resource_bindings", + "thoth.adapters.storage.operations.operations": "operations", + "thoth.adapters.storage.operations.receipts": "receipts", + "thoth.adapters.storage.operations.semantic_revisions": "semantic_revisions", + "thoth.adapters.storage.operations.thread_inputs": "thread_inputs", + "thoth.adapters.storage.operations.working_heads": "working_heads", + "thoth.adapters.storage.outcome.outcome_assessments": "outcome_assessments", + "thoth.adapters.storage.outcome.outcome_attributions": "outcome_attributions", + "thoth.adapters.storage.outcome.outcome_audit": "outcome_audit", + "thoth.adapters.storage.outcome.outcome_change_sets": "outcome_change_sets", + "thoth.adapters.storage.outcome.outcome_impacts": "outcome_impacts", + "thoth.adapters.storage.outcome.outcome_profiles": "outcome_profiles", + "thoth.adapters.storage.outcome.outcome_series": "outcome_series", + "thoth.adapters.storage.projects.projects": "projects", + "thoth.adapters.storage.receipt_dag.receipt_dag_bundles": "receipt_dag_bundles", + "thoth.adapters.storage.receipt_dag.receipt_dag_edges": "receipt_dag_edges", + "thoth.adapters.storage.receipt_dag.receipt_dag_manifests": "receipt_dag_manifests", + "thoth.adapters.storage.receipt_dag.receipt_dag_nodes": "receipt_dag_nodes", + "thoth.adapters.storage.receipt_dag.receipt_dag_verifications": "receipt_dag_verifications", + "thoth.adapters.storage.research_identity.research_identities": "research_identities", + "thoth.adapters.storage.research_identity.semantic_revisions": "semantic_revisions", + "thoth.adapters.storage.research_schema.research_identities": "research_identities", + "thoth.adapters.storage.resource_scope_schema.operation_resource_bindings": "operation_resource_bindings", + "thoth.adapters.storage.resource_scope_schema.resource_scope_heads": "resource_scope_heads", + "thoth.adapters.storage.resource_scope_schema.resource_scope_history": "resource_scope_history", + "thoth.adapters.storage.resource_scope_schema.resource_scope_receipts": "resource_scope_receipts", + "thoth.adapters.storage.schema.acquisition_search_intents": "acquisition_search_intents", + "thoth.adapters.storage.schema.action_audit": "action_audit", + "thoth.adapters.storage.schema.action_authorizations": "action_authorizations", + "thoth.adapters.storage.schema.action_plans": "action_plans", + "thoth.adapters.storage.schema.action_portfolios": "action_portfolios", + "thoth.adapters.storage.schema.action_records": "action_records", + "thoth.adapters.storage.schema.artifact_versions": "artifact_versions", + "thoth.adapters.storage.schema.artifacts": "artifacts", + "thoth.adapters.storage.schema.auth_sessions": "auth_sessions", + "thoth.adapters.storage.schema.baseline_candidates": "baseline_candidates", + "thoth.adapters.storage.schema.baseline_decisions": "baseline_decisions", + "thoth.adapters.storage.schema.baseline_sets": "baseline_sets", + "thoth.adapters.storage.schema.behavior_artifacts": "behavior_artifacts", + "thoth.adapters.storage.schema.behavior_registry": "behavior_registry", + "thoth.adapters.storage.schema.checkpoints": "checkpoints", + "thoth.adapters.storage.schema.control_records": "control_records", + "thoth.adapters.storage.schema.criterion_audit": "criterion_audit", + "thoth.adapters.storage.schema.criterion_conflicts": "criterion_conflicts", + "thoth.adapters.storage.schema.criterion_contracts": "criterion_contracts", + "thoth.adapters.storage.schema.criterion_profiles": "criterion_profiles", + "thoth.adapters.storage.schema.criterion_references": "criterion_references", + "thoth.adapters.storage.schema.decision_objects": "decision_objects", + "thoth.adapters.storage.schema.dependency_states": "dependency_states", + "thoth.adapters.storage.schema.entity_snapshots": "entity_snapshots", + "thoth.adapters.storage.schema.events": "events", + "thoth.adapters.storage.schema.evidence_audit": "evidence_audit", + "thoth.adapters.storage.schema.evidence_conflicts": "evidence_conflicts", + "thoth.adapters.storage.schema.evidence_leads": "evidence_leads", + "thoth.adapters.storage.schema.evidence_links": "evidence_links", + "thoth.adapters.storage.schema.evidence_observations": "evidence_observations", + "thoth.adapters.storage.schema.evidence_sources": "evidence_sources", + "thoth.adapters.storage.schema.evidence_spans": "evidence_spans", + "thoth.adapters.storage.schema.execution_audit": "execution_audit", + "thoth.adapters.storage.schema.execution_effects": "execution_effects", + "thoth.adapters.storage.schema.execution_reconciliations": "execution_reconciliations", + "thoth.adapters.storage.schema.field_events": "field_events", + "thoth.adapters.storage.schema.field_exports": "field_exports", + "thoth.adapters.storage.schema.field_protocol_seals": "field_protocol_seals", + "thoth.adapters.storage.schema.field_scores": "field_scores", + "thoth.adapters.storage.schema.field_session_metrics": "field_session_metrics", + "thoth.adapters.storage.schema.field_sessions": "field_sessions", + "thoth.adapters.storage.schema.hypothesis_appraisals": "hypothesis_appraisals", + "thoth.adapters.storage.schema.hypothesis_assumptions": "hypothesis_assumptions", + "thoth.adapters.storage.schema.hypothesis_audit": "hypothesis_audit", + "thoth.adapters.storage.schema.hypothesis_portfolios": "hypothesis_portfolios", + "thoth.adapters.storage.schema.hypothesis_predictions": "hypothesis_predictions", + "thoth.adapters.storage.schema.hypothesis_records": "hypothesis_records", + "thoth.adapters.storage.schema.hypothesis_relations": "hypothesis_relations", + "thoth.adapters.storage.schema.hypothesis_test_bindings": "hypothesis_test_bindings", + "thoth.adapters.storage.schema.idempotency_keys": "idempotency_keys", + "thoth.adapters.storage.schema.investigation_audit": "investigation_audit", + "thoth.adapters.storage.schema.investigations": "investigations", + "thoth.adapters.storage.schema.memory_context_packs": "memory_context_packs", + "thoth.adapters.storage.schema.memory_projections": "memory_projections", + "thoth.adapters.storage.schema.memory_records": "memory_records", + "thoth.adapters.storage.schema.memory_revision_ledger": "memory_revision_ledger", + "thoth.adapters.storage.schema.memory_transition_receipts": "memory_transition_receipts", + "thoth.adapters.storage.schema.object_attention": "object_attention", + "thoth.adapters.storage.schema.object_audit": "object_audit", + "thoth.adapters.storage.schema.object_candidates": "object_candidates", + "thoth.adapters.storage.schema.object_profiles": "object_profiles", + "thoth.adapters.storage.schema.object_relations": "object_relations", + "thoth.adapters.storage.schema.operations": "operations", + "thoth.adapters.storage.schema.organizations": "organizations", + "thoth.adapters.storage.schema.outcome_assessments": "outcome_assessments", + "thoth.adapters.storage.schema.outcome_attributions": "outcome_attributions", + "thoth.adapters.storage.schema.outcome_audit": "outcome_audit", + "thoth.adapters.storage.schema.outcome_change_sets": "outcome_change_sets", + "thoth.adapters.storage.schema.outcome_impacts": "outcome_impacts", + "thoth.adapters.storage.schema.outcome_profiles": "outcome_profiles", + "thoth.adapters.storage.schema.outcome_series": "outcome_series", + "thoth.adapters.storage.schema.plan_executions": "plan_executions", + "thoth.adapters.storage.schema.project_head_sets": "project_head_sets", + "thoth.adapters.storage.schema.project_policies": "project_policies", + "thoth.adapters.storage.schema.project_references": "project_references", + "thoth.adapters.storage.schema.project_roles": "project_roles", + "thoth.adapters.storage.schema.projects": "projects", + "thoth.adapters.storage.schema.receipt_dag_bundles": "receipt_dag_bundles", + "thoth.adapters.storage.schema.receipt_dag_edges": "receipt_dag_edges", + "thoth.adapters.storage.schema.receipt_dag_manifests": "receipt_dag_manifests", + "thoth.adapters.storage.schema.receipt_dag_nodes": "receipt_dag_nodes", + "thoth.adapters.storage.schema.receipt_dag_verifications": "receipt_dag_verifications", + "thoth.adapters.storage.schema.receipts": "receipts", + "thoth.adapters.storage.schema.relations": "relations", + "thoth.adapters.storage.schema.revision_parents": "revision_parents", + "thoth.adapters.storage.schema.semantic_revisions": "semantic_revisions", + "thoth.adapters.storage.schema.source_bindings": "source_bindings", + "thoth.adapters.storage.schema.span_corrections": "span_corrections", + "thoth.adapters.storage.schema.step_execution_attempts": "step_execution_attempts", + "thoth.adapters.storage.schema.structural_nodes": "structural_nodes", + "thoth.adapters.storage.schema.thread_activities": "thread_activities", + "thoth.adapters.storage.schema.thread_checkpoints": "thread_checkpoints", + "thoth.adapters.storage.schema.thread_inputs": "thread_inputs", + "thoth.adapters.storage.schema.threads": "threads", + "thoth.adapters.storage.schema.tui_sessions": "tui_sessions", + "thoth.adapters.storage.schema.working_heads": "working_heads", + "thoth.adapters.storage.schema.workstreams": "workstreams", + "thoth.adapters.storage.sqlite.dependency_states": "dependency_states", + "thoth.adapters.storage.sqlite.entity_snapshots": "entity_snapshots", + "thoth.adapters.storage.sqlite.receipts": "receipts", + "thoth.adapters.storage.sqlite.revision_parents": "revision_parents", + "thoth.adapters.storage.sqlite.semantic_revisions": "semantic_revisions", + "thoth.adapters.storage.sqlite.structural_nodes": "structural_nodes", + "thoth.adapters.storage.sqlite.working_heads": "working_heads", + "thoth.adapters.storage.test_validity_schema.research_test_assessments": "research_test_assessments", + "thoth.adapters.storage.thread_runtime.thread_activities": "thread_activities", + "thoth.adapters.storage.thread_runtime.thread_checkpoints": "thread_checkpoints", + "thoth.adapters.storage.thread_runtime.thread_inputs": "thread_inputs", + "thoth.adapters.storage.threads.threads": "threads" + }, + "create_table_order": [ + "projects", + "organizations", + "project_roles", + "auth_sessions", + "field_protocol_seals", + "field_sessions", + "field_events", + "field_session_metrics", + "field_scores", + "field_exports", + "project_head_sets", + "baseline_candidates", + "baseline_sets", + "baseline_decisions", + "behavior_artifacts", + "behavior_registry", + "tui_sessions", + "workstreams", + "project_policies", + "project_references", + "source_bindings", + "threads", + "thread_activities", + "thread_checkpoints", + "thread_inputs", + "investigations", + "investigation_audit", + "acquisition_search_intents", + "evidence_leads", + "evidence_sources", + "evidence_observations", + "evidence_links", + "evidence_conflicts", + "evidence_audit", + "span_corrections", + "criterion_profiles", + "criterion_contracts", + "criterion_references", + "criterion_conflicts", + "criterion_audit", + "object_profiles", + "object_candidates", + "decision_objects", + "object_relations", + "object_attention", + "object_audit", + "hypothesis_records", + "hypothesis_portfolios", + "hypothesis_relations", + "hypothesis_assumptions", + "hypothesis_predictions", + "hypothesis_test_bindings", + "hypothesis_appraisals", + "hypothesis_audit", + "action_records", + "action_portfolios", + "action_plans", + "action_authorizations", + "action_audit", + "plan_executions", + "step_execution_attempts", + "execution_effects", + "execution_reconciliations", + "execution_audit", + "outcome_profiles", + "outcome_series", + "outcome_assessments", + "outcome_attributions", + "outcome_change_sets", + "outcome_impacts", + "outcome_audit", + "control_records", + "entity_snapshots", + "working_heads", + "receipts", + "receipt_dag_nodes", + "receipt_dag_edges", + "receipt_dag_manifests", + "receipt_dag_bundles", + "receipt_dag_verifications", + "structural_nodes", + "artifacts", + "operations", + "relations", + "dependency_states", + "memory_records", + "memory_revision_ledger", + "memory_transition_receipts", + "memory_context_packs", + "memory_projections", + "governance_revisions", + "governance_heads", + "research_identities", + "operation_owners", + "resource_scope_history", + "resource_scope_heads", + "resource_scope_receipts", + "operation_resource_bindings", + "evaluation_runs", + "behavior_exposure_history", + "behavior_baseline_history", + "research_test_assessments", + "semantic_revisions", + "artifact_versions", + "events", + "checkpoints", + "idempotency_keys", + "revision_parents", + "evidence_spans" + ], + "root_export_order": [ + "projects", + "organizations", + "project_roles", + "auth_sessions", + "field_protocol_seals", + "field_sessions", + "field_events", + "field_session_metrics", + "field_scores", + "field_exports", + "project_head_sets", + "baseline_candidates", + "baseline_sets", + "baseline_decisions", + "behavior_artifacts", + "behavior_registry", + "tui_sessions", + "workstreams", + "project_policies", + "project_references", + "source_bindings", + "threads", + "thread_activities", + "thread_checkpoints", + "thread_inputs", + "investigations", + "investigation_audit", + "acquisition_search_intents", + "evidence_leads", + "evidence_sources", + "evidence_observations", + "evidence_links", + "evidence_conflicts", + "evidence_audit", + "span_corrections", + "criterion_profiles", + "criterion_contracts", + "criterion_references", + "criterion_conflicts", + "criterion_audit", + "object_profiles", + "object_candidates", + "decision_objects", + "object_relations", + "object_attention", + "object_audit", + "hypothesis_records", + "hypothesis_portfolios", + "hypothesis_relations", + "hypothesis_assumptions", + "hypothesis_predictions", + "hypothesis_test_bindings", + "hypothesis_appraisals", + "hypothesis_audit", + "action_records", + "action_portfolios", + "action_plans", + "action_authorizations", + "action_audit", + "plan_executions", + "step_execution_attempts", + "execution_effects", + "execution_reconciliations", + "execution_audit", + "outcome_profiles", + "outcome_series", + "outcome_assessments", + "outcome_attributions", + "outcome_change_sets", + "outcome_impacts", + "outcome_audit", + "control_records", + "entity_snapshots", + "semantic_revisions", + "revision_parents", + "working_heads", + "receipts", + "receipt_dag_nodes", + "receipt_dag_edges", + "receipt_dag_manifests", + "receipt_dag_bundles", + "receipt_dag_verifications", + "structural_nodes", + "artifacts", + "artifact_versions", + "evidence_spans", + "operations", + "events", + "checkpoints", + "idempotency_keys", + "relations", + "dependency_states", + "memory_records", + "memory_revision_ledger", + "memory_transition_receipts", + "memory_context_packs", + "memory_projections" + ], + "root_exports": { + "acquisition_search_intents": "acquisition_search_intents", + "action_audit": "action_audit", + "action_authorizations": "action_authorizations", + "action_plans": "action_plans", + "action_portfolios": "action_portfolios", + "action_records": "action_records", + "artifact_versions": "artifact_versions", + "artifacts": "artifacts", + "auth_sessions": "auth_sessions", + "baseline_candidates": "baseline_candidates", + "baseline_decisions": "baseline_decisions", + "baseline_sets": "baseline_sets", + "behavior_artifacts": "behavior_artifacts", + "behavior_registry": "behavior_registry", + "checkpoints": "checkpoints", + "control_records": "control_records", + "criterion_audit": "criterion_audit", + "criterion_conflicts": "criterion_conflicts", + "criterion_contracts": "criterion_contracts", + "criterion_profiles": "criterion_profiles", + "criterion_references": "criterion_references", + "decision_objects": "decision_objects", + "dependency_states": "dependency_states", + "entity_snapshots": "entity_snapshots", + "events": "events", + "evidence_audit": "evidence_audit", + "evidence_conflicts": "evidence_conflicts", + "evidence_leads": "evidence_leads", + "evidence_links": "evidence_links", + "evidence_observations": "evidence_observations", + "evidence_sources": "evidence_sources", + "evidence_spans": "evidence_spans", + "execution_audit": "execution_audit", + "execution_effects": "execution_effects", + "execution_reconciliations": "execution_reconciliations", + "field_events": "field_events", + "field_exports": "field_exports", + "field_protocol_seals": "field_protocol_seals", + "field_scores": "field_scores", + "field_session_metrics": "field_session_metrics", + "field_sessions": "field_sessions", + "hypothesis_appraisals": "hypothesis_appraisals", + "hypothesis_assumptions": "hypothesis_assumptions", + "hypothesis_audit": "hypothesis_audit", + "hypothesis_portfolios": "hypothesis_portfolios", + "hypothesis_predictions": "hypothesis_predictions", + "hypothesis_records": "hypothesis_records", + "hypothesis_relations": "hypothesis_relations", + "hypothesis_test_bindings": "hypothesis_test_bindings", + "idempotency_keys": "idempotency_keys", + "investigation_audit": "investigation_audit", + "investigations": "investigations", + "memory_context_packs": "memory_context_packs", + "memory_projections": "memory_projections", + "memory_records": "memory_records", + "memory_revision_ledger": "memory_revision_ledger", + "memory_transition_receipts": "memory_transition_receipts", + "object_attention": "object_attention", + "object_audit": "object_audit", + "object_candidates": "object_candidates", + "object_profiles": "object_profiles", + "object_relations": "object_relations", + "operations": "operations", + "organizations": "organizations", + "outcome_assessments": "outcome_assessments", + "outcome_attributions": "outcome_attributions", + "outcome_audit": "outcome_audit", + "outcome_change_sets": "outcome_change_sets", + "outcome_impacts": "outcome_impacts", + "outcome_profiles": "outcome_profiles", + "outcome_series": "outcome_series", + "plan_executions": "plan_executions", + "project_head_sets": "project_head_sets", + "project_policies": "project_policies", + "project_references": "project_references", + "project_roles": "project_roles", + "projects": "projects", + "receipt_dag_bundles": "receipt_dag_bundles", + "receipt_dag_edges": "receipt_dag_edges", + "receipt_dag_manifests": "receipt_dag_manifests", + "receipt_dag_nodes": "receipt_dag_nodes", + "receipt_dag_verifications": "receipt_dag_verifications", + "receipts": "receipts", + "relations": "relations", + "revision_parents": "revision_parents", + "semantic_revisions": "semantic_revisions", + "source_bindings": "source_bindings", + "span_corrections": "span_corrections", + "step_execution_attempts": "step_execution_attempts", + "structural_nodes": "structural_nodes", + "thread_activities": "thread_activities", + "thread_checkpoints": "thread_checkpoints", + "thread_inputs": "thread_inputs", + "threads": "threads", + "tui_sessions": "tui_sessions", + "working_heads": "working_heads", + "workstreams": "workstreams" + }, + "sorted_tables": [ + "acquisition_search_intents", + "action_audit", + "action_authorizations", + "action_plans", + "action_portfolios", + "action_records", + "artifacts", + "auth_sessions", + "baseline_candidates", + "baseline_decisions", + "baseline_sets", + "behavior_artifacts", + "behavior_baseline_history", + "behavior_exposure_history", + "behavior_registry", + "control_records", + "criterion_audit", + "criterion_conflicts", + "criterion_contracts", + "criterion_profiles", + "criterion_references", + "decision_objects", + "dependency_states", + "entity_snapshots", + "evaluation_runs", + "evidence_audit", + "evidence_conflicts", + "evidence_leads", + "evidence_links", + "evidence_observations", + "evidence_sources", + "execution_audit", + "execution_effects", + "execution_reconciliations", + "field_events", + "field_exports", + "field_protocol_seals", + "field_scores", + "field_session_metrics", + "field_sessions", + "governance_heads", + "governance_revisions", + "hypothesis_appraisals", + "hypothesis_assumptions", + "hypothesis_audit", + "hypothesis_portfolios", + "hypothesis_predictions", + "hypothesis_records", + "hypothesis_relations", + "hypothesis_test_bindings", + "investigation_audit", + "investigations", + "memory_context_packs", + "memory_projections", + "memory_records", + "memory_revision_ledger", + "memory_transition_receipts", + "object_attention", + "object_audit", + "object_candidates", + "object_profiles", + "object_relations", + "operation_owners", + "operation_resource_bindings", + "operations", + "organizations", + "outcome_assessments", + "outcome_attributions", + "outcome_audit", + "outcome_change_sets", + "outcome_impacts", + "outcome_profiles", + "outcome_series", + "plan_executions", + "project_head_sets", + "project_policies", + "project_references", + "project_roles", + "projects", + "receipt_dag_bundles", + "receipt_dag_edges", + "receipt_dag_manifests", + "receipt_dag_nodes", + "receipt_dag_verifications", + "receipts", + "relations", + "research_identities", + "research_test_assessments", + "resource_scope_heads", + "resource_scope_history", + "resource_scope_receipts", + "source_bindings", + "span_corrections", + "step_execution_attempts", + "structural_nodes", + "thread_activities", + "thread_checkpoints", + "thread_inputs", + "threads", + "tui_sessions", + "working_heads", + "workstreams", + "artifact_versions", + "checkpoints", + "events", + "idempotency_keys", + "semantic_revisions", + "evidence_spans", + "revision_parents" + ], + "table_registration_order": [ + "projects", + "organizations", + "project_roles", + "auth_sessions", + "field_protocol_seals", + "field_sessions", + "field_events", + "field_session_metrics", + "field_scores", + "field_exports", + "project_head_sets", + "baseline_candidates", + "baseline_sets", + "baseline_decisions", + "behavior_artifacts", + "behavior_registry", + "tui_sessions", + "workstreams", + "project_policies", + "project_references", + "source_bindings", + "threads", + "thread_activities", + "thread_checkpoints", + "thread_inputs", + "investigations", + "investigation_audit", + "acquisition_search_intents", + "evidence_leads", + "evidence_sources", + "evidence_observations", + "evidence_links", + "evidence_conflicts", + "evidence_audit", + "span_corrections", + "criterion_profiles", + "criterion_contracts", + "criterion_references", + "criterion_conflicts", + "criterion_audit", + "object_profiles", + "object_candidates", + "decision_objects", + "object_relations", + "object_attention", + "object_audit", + "hypothesis_records", + "hypothesis_portfolios", + "hypothesis_relations", + "hypothesis_assumptions", + "hypothesis_predictions", + "hypothesis_test_bindings", + "hypothesis_appraisals", + "hypothesis_audit", + "action_records", + "action_portfolios", + "action_plans", + "action_authorizations", + "action_audit", + "plan_executions", + "step_execution_attempts", + "execution_effects", + "execution_reconciliations", + "execution_audit", + "outcome_profiles", + "outcome_series", + "outcome_assessments", + "outcome_attributions", + "outcome_change_sets", + "outcome_impacts", + "outcome_audit", + "control_records", + "entity_snapshots", + "semantic_revisions", + "revision_parents", + "working_heads", + "receipts", + "receipt_dag_nodes", + "receipt_dag_edges", + "receipt_dag_manifests", + "receipt_dag_bundles", + "receipt_dag_verifications", + "structural_nodes", + "artifacts", + "artifact_versions", + "evidence_spans", + "operations", + "events", + "checkpoints", + "idempotency_keys", + "relations", + "dependency_states", + "memory_records", + "memory_revision_ledger", + "memory_transition_receipts", + "memory_context_packs", + "memory_projections", + "governance_revisions", + "governance_heads", + "research_identities", + "operation_owners", + "resource_scope_history", + "resource_scope_heads", + "resource_scope_receipts", + "operation_resource_bindings", + "evaluation_runs", + "behavior_exposure_history", + "behavior_baseline_history", + "research_test_assessments" + ], + "tables": { + "acquisition_search_intents": { + "columns": [ + "search_intent_id", + "project_id", + "investigation_id", + "content_json", + "intent_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_acquisition_search_intents_investigation_id": "CREATE INDEX ix_acquisition_search_intents_investigation_id ON acquisition_search_intents (investigation_id)", + "ix_acquisition_search_intents_project_id": "CREATE INDEX ix_acquisition_search_intents_project_id ON acquisition_search_intents (project_id)" + }, + "sql": "\nCREATE TABLE acquisition_search_intents (\n\tsearch_intent_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tinvestigation_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tintent_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (search_intent_id), \n\tUNIQUE (intent_digest)\n)\n\n" + }, + "action_audit": { + "columns": [ + "audit_id", + "project_id", + "subject_id", + "event_type", + "payload_json", + "event_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_action_audit_project_id": "CREATE INDEX ix_action_audit_project_id ON action_audit (project_id)", + "ix_action_audit_subject_id": "CREATE INDEX ix_action_audit_subject_id ON action_audit (subject_id)" + }, + "sql": "\nCREATE TABLE action_audit (\n\taudit_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsubject_id VARCHAR(160) NOT NULL, \n\tevent_type VARCHAR(120) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tevent_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (audit_id), \n\tUNIQUE (event_digest)\n)\n\n" + }, + "action_authorizations": { + "columns": [ + "authorization_revision_id", + "authorization_id", + "project_id", + "plan_id", + "step_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_action_authorizations_authorization_id": "CREATE INDEX ix_action_authorizations_authorization_id ON action_authorizations (authorization_id)", + "ix_action_authorizations_plan_id": "CREATE INDEX ix_action_authorizations_plan_id ON action_authorizations (plan_id)", + "ix_action_authorizations_project_id": "CREATE INDEX ix_action_authorizations_project_id ON action_authorizations (project_id)", + "ix_action_authorizations_step_id": "CREATE INDEX ix_action_authorizations_step_id ON action_authorizations (step_id)" + }, + "sql": "\nCREATE TABLE action_authorizations (\n\tauthorization_revision_id VARCHAR(160) NOT NULL, \n\tauthorization_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tplan_id VARCHAR(160) NOT NULL, \n\tstep_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (authorization_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "action_plans": { + "columns": [ + "plan_revision_id", + "plan_id", + "project_id", + "object_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_action_plans_object_id": "CREATE INDEX ix_action_plans_object_id ON action_plans (object_id)", + "ix_action_plans_plan_id": "CREATE INDEX ix_action_plans_plan_id ON action_plans (plan_id)", + "ix_action_plans_project_id": "CREATE INDEX ix_action_plans_project_id ON action_plans (project_id)" + }, + "sql": "\nCREATE TABLE action_plans (\n\tplan_revision_id VARCHAR(160) NOT NULL, \n\tplan_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tobject_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (plan_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "action_portfolios": { + "columns": [ + "portfolio_revision_id", + "portfolio_id", + "project_id", + "object_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_action_portfolios_object_id": "CREATE INDEX ix_action_portfolios_object_id ON action_portfolios (object_id)", + "ix_action_portfolios_portfolio_id": "CREATE INDEX ix_action_portfolios_portfolio_id ON action_portfolios (portfolio_id)", + "ix_action_portfolios_project_id": "CREATE INDEX ix_action_portfolios_project_id ON action_portfolios (project_id)" + }, + "sql": "\nCREATE TABLE action_portfolios (\n\tportfolio_revision_id VARCHAR(160) NOT NULL, \n\tportfolio_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tobject_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (portfolio_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "action_records": { + "columns": [ + "action_revision_id", + "action_id", + "project_id", + "object_id", + "portfolio_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_action_records_action_id": "CREATE INDEX ix_action_records_action_id ON action_records (action_id)", + "ix_action_records_object_id": "CREATE INDEX ix_action_records_object_id ON action_records (object_id)", + "ix_action_records_portfolio_id": "CREATE INDEX ix_action_records_portfolio_id ON action_records (portfolio_id)", + "ix_action_records_project_id": "CREATE INDEX ix_action_records_project_id ON action_records (project_id)" + }, + "sql": "\nCREATE TABLE action_records (\n\taction_revision_id VARCHAR(160) NOT NULL, \n\taction_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tobject_id VARCHAR(160) NOT NULL, \n\tportfolio_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (action_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "artifact_versions": { + "columns": [ + "source_version_id", + "artifact_id", + "version_label", + "byte_sha256", + "retrieved_at", + "structure_metadata_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "ForeignKeyConstraint", + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE artifact_versions (\n\tsource_version_id VARCHAR(160) NOT NULL, \n\tartifact_id VARCHAR(160) NOT NULL, \n\tversion_label VARCHAR(260), \n\tbyte_sha256 VARCHAR(64) NOT NULL, \n\tretrieved_at VARCHAR(32) NOT NULL, \n\tstructure_metadata_json TEXT, \n\tPRIMARY KEY (source_version_id), \n\tFOREIGN KEY(artifact_id) REFERENCES artifacts (artifact_id)\n)\n\n" + }, + "artifacts": { + "columns": [ + "artifact_id", + "project_id", + "source_uri", + "media_type", + "byte_sha256", + "authority", + "cutoff_state", + "security_class", + "parser_name", + "parser_version", + "retrieved_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_artifacts_project_id": "CREATE INDEX ix_artifacts_project_id ON artifacts (project_id)" + }, + "sql": "\nCREATE TABLE artifacts (\n\tartifact_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_uri TEXT NOT NULL, \n\tmedia_type VARCHAR(260) NOT NULL, \n\tbyte_sha256 VARCHAR(64) NOT NULL, \n\tauthority VARCHAR(60) NOT NULL, \n\tcutoff_state VARCHAR(60) NOT NULL, \n\tsecurity_class VARCHAR(60) NOT NULL, \n\tparser_name VARCHAR(160) NOT NULL, \n\tparser_version VARCHAR(80) NOT NULL, \n\tretrieved_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (artifact_id)\n)\n\n" + }, + "auth_sessions": { + "columns": [ + "session_id", + "actor_id", + "project_id", + "role_assignment_id", + "token_digest", + "state", + "content_json", + "expires_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_auth_sessions_actor_id": "CREATE INDEX ix_auth_sessions_actor_id ON auth_sessions (actor_id)", + "ix_auth_sessions_project_id": "CREATE INDEX ix_auth_sessions_project_id ON auth_sessions (project_id)" + }, + "sql": "\nCREATE TABLE auth_sessions (\n\tsession_id VARCHAR(200) NOT NULL, \n\tactor_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\trole_assignment_id VARCHAR(160) NOT NULL, \n\ttoken_digest VARCHAR(64) NOT NULL, \n\tstate VARCHAR(40) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\texpires_at VARCHAR(40) NOT NULL, \n\tPRIMARY KEY (session_id), \n\tUNIQUE (token_digest)\n)\n\n" + }, + "baseline_candidates": { + "columns": [ + "candidate_id", + "project_id", + "scope", + "state", + "candidate_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_baseline_candidates_project_id": "CREATE INDEX ix_baseline_candidates_project_id ON baseline_candidates (project_id)" + }, + "sql": "\nCREATE TABLE baseline_candidates (\n\tcandidate_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tscope VARCHAR(80) NOT NULL, \n\tstate VARCHAR(60) NOT NULL, \n\tcandidate_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (candidate_id), \n\tUNIQUE (candidate_digest)\n)\n\n" + }, + "baseline_decisions": { + "columns": [ + "decision_id", + "project_id", + "candidate_id", + "decision", + "decision_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_baseline_decisions_project_id": "CREATE INDEX ix_baseline_decisions_project_id ON baseline_decisions (project_id)" + }, + "sql": "\nCREATE TABLE baseline_decisions (\n\tdecision_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tcandidate_id VARCHAR(200) NOT NULL, \n\tdecision VARCHAR(20) NOT NULL, \n\tdecision_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (decision_id), \n\tUNIQUE (decision_digest)\n)\n\n" + }, + "baseline_sets": { + "columns": [ + "baseline_set_id", + "project_id", + "scope", + "lifecycle", + "baseline_set_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_baseline_sets_project_id": "CREATE INDEX ix_baseline_sets_project_id ON baseline_sets (project_id)" + }, + "sql": "\nCREATE TABLE baseline_sets (\n\tbaseline_set_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tscope VARCHAR(80) NOT NULL, \n\tlifecycle VARCHAR(40) NOT NULL, \n\tbaseline_set_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (baseline_set_id), \n\tUNIQUE (baseline_set_digest)\n)\n\n" + }, + "behavior_artifacts": { + "columns": [ + "artifact_id", + "project_id", + "kind", + "state", + "content_digest", + "content_json" + ], + "constraint_names": [ + null, + "uq_behavior_artifact_content" + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_behavior_artifacts_project_id": "CREATE INDEX ix_behavior_artifacts_project_id ON behavior_artifacts (project_id)" + }, + "sql": "\nCREATE TABLE behavior_artifacts (\n\tartifact_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tkind VARCHAR(80) NOT NULL, \n\tstate VARCHAR(40) NOT NULL, \n\tcontent_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (artifact_id), \n\tCONSTRAINT uq_behavior_artifact_content UNIQUE (project_id, kind, content_digest)\n)\n\n" + }, + "behavior_baseline_history": { + "columns": [ + "project_id", + "scope_digest", + "component", + "environment", + "revision", + "content_digest", + "receipt_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE behavior_baseline_history (\n\tproject_id VARCHAR(160) NOT NULL, \n\tscope_digest VARCHAR(64) NOT NULL, \n\tcomponent VARCHAR(64) NOT NULL, \n\tenvironment VARCHAR(160) NOT NULL, \n\trevision INTEGER NOT NULL, \n\tcontent_digest VARCHAR(64) NOT NULL, \n\treceipt_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (project_id, scope_digest, component, environment, revision)\n)\n\n" + }, + "behavior_exposure_history": { + "columns": [ + "project_id", + "exposure_id", + "revision", + "scope_digest", + "stage", + "state", + "record_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE behavior_exposure_history (\n\tproject_id VARCHAR(160) NOT NULL, \n\texposure_id VARCHAR(200) NOT NULL, \n\trevision INTEGER NOT NULL, \n\tscope_digest VARCHAR(64) NOT NULL, \n\tstage VARCHAR(32) NOT NULL, \n\tstate VARCHAR(32) NOT NULL, \n\trecord_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (project_id, exposure_id, revision)\n)\n\n" + }, + "behavior_registry": { + "columns": [ + "project_id", + "kind", + "active_digest", + "revision", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE behavior_registry (\n\tproject_id VARCHAR(160) NOT NULL, \n\tkind VARCHAR(80) NOT NULL, \n\tactive_digest VARCHAR(64) NOT NULL, \n\trevision INTEGER NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (project_id, kind)\n)\n\n" + }, + "checkpoints": { + "columns": [ + "checkpoint_id", + "operation_id", + "payload_json", + "checkpoint_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "ForeignKeyConstraint", + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE checkpoints (\n\tcheckpoint_id VARCHAR(160) NOT NULL, \n\toperation_id VARCHAR(160) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tcheckpoint_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (checkpoint_id), \n\tFOREIGN KEY(operation_id) REFERENCES operations (operation_id), \n\tUNIQUE (checkpoint_digest)\n)\n\n" + }, + "control_records": { + "columns": [ + "control_revision_id", + "project_id", + "namespace", + "record_type", + "record_id", + "version", + "state", + "content_json", + "record_digest", + "supersedes_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_control_records_namespace": "CREATE INDEX ix_control_records_namespace ON control_records (namespace)", + "ix_control_records_project_id": "CREATE INDEX ix_control_records_project_id ON control_records (project_id)", + "ix_control_records_record_id": "CREATE INDEX ix_control_records_record_id ON control_records (record_id)", + "ix_control_records_record_type": "CREATE INDEX ix_control_records_record_type ON control_records (record_type)", + "ix_control_records_state": "CREATE INDEX ix_control_records_state ON control_records (state)" + }, + "sql": "\nCREATE TABLE control_records (\n\tcontrol_revision_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tnamespace VARCHAR(80) NOT NULL, \n\trecord_type VARCHAR(120) NOT NULL, \n\trecord_id VARCHAR(160) NOT NULL, \n\tversion INTEGER NOT NULL, \n\tstate VARCHAR(80) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trecord_digest VARCHAR(64) NOT NULL, \n\tsupersedes_digest VARCHAR(64), \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (control_revision_id), \n\tUNIQUE (record_digest)\n)\n\n" + }, + "criterion_audit": { + "columns": [ + "audit_id", + "project_id", + "criterion_id", + "event_type", + "payload_json", + "event_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_criterion_audit_criterion_id": "CREATE INDEX ix_criterion_audit_criterion_id ON criterion_audit (criterion_id)", + "ix_criterion_audit_project_id": "CREATE INDEX ix_criterion_audit_project_id ON criterion_audit (project_id)" + }, + "sql": "\nCREATE TABLE criterion_audit (\n\taudit_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tcriterion_id VARCHAR(160) NOT NULL, \n\tevent_type VARCHAR(120) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tevent_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (audit_id), \n\tUNIQUE (event_digest)\n)\n\n" + }, + "criterion_conflicts": { + "columns": [ + "conflict_id", + "project_id", + "criterion_id", + "field_path", + "candidate_values_json", + "evidence_refs_json", + "status", + "impact_json", + "conflict_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_criterion_conflicts_criterion_id": "CREATE INDEX ix_criterion_conflicts_criterion_id ON criterion_conflicts (criterion_id)", + "ix_criterion_conflicts_project_id": "CREATE INDEX ix_criterion_conflicts_project_id ON criterion_conflicts (project_id)" + }, + "sql": "\nCREATE TABLE criterion_conflicts (\n\tconflict_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tcriterion_id VARCHAR(160) NOT NULL, \n\tfield_path VARCHAR(260) NOT NULL, \n\tcandidate_values_json TEXT NOT NULL, \n\tevidence_refs_json TEXT NOT NULL, \n\tstatus VARCHAR(40) NOT NULL, \n\timpact_json TEXT NOT NULL, \n\tconflict_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (conflict_id), \n\tUNIQUE (conflict_digest)\n)\n\n" + }, + "criterion_contracts": { + "columns": [ + "criterion_revision_id", + "criterion_id", + "project_id", + "thread_id", + "content_json", + "revision_digest", + "supersedes_revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_criterion_contracts_criterion_id": "CREATE INDEX ix_criterion_contracts_criterion_id ON criterion_contracts (criterion_id)", + "ix_criterion_contracts_project_id": "CREATE INDEX ix_criterion_contracts_project_id ON criterion_contracts (project_id)" + }, + "sql": "\nCREATE TABLE criterion_contracts (\n\tcriterion_revision_id VARCHAR(160) NOT NULL, \n\tcriterion_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tthread_id VARCHAR(160), \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tsupersedes_revision_digest VARCHAR(64), \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (criterion_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "criterion_profiles": { + "columns": [ + "profile_ref", + "version", + "name", + "domain_hint", + "required_fields_json", + "conditional_fields_json", + "allowed_computation_types_json", + "authority_policy_json", + "enabled", + "profile_digest", + "profile_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE criterion_profiles (\n\tprofile_ref VARCHAR(160) NOT NULL, \n\tversion INTEGER NOT NULL, \n\tname TEXT NOT NULL, \n\tdomain_hint VARCHAR(160) NOT NULL, \n\trequired_fields_json TEXT NOT NULL, \n\tconditional_fields_json TEXT NOT NULL, \n\tallowed_computation_types_json TEXT NOT NULL, \n\tauthority_policy_json TEXT NOT NULL, \n\tenabled INTEGER NOT NULL, \n\tprofile_digest VARCHAR(64) NOT NULL, \n\tprofile_json TEXT, \n\tPRIMARY KEY (profile_ref, version), \n\tUNIQUE (profile_digest)\n)\n\n" + }, + "criterion_references": { + "columns": [ + "reference_candidate_id", + "project_id", + "criterion_id", + "content_json", + "reference_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_criterion_references_criterion_id": "CREATE INDEX ix_criterion_references_criterion_id ON criterion_references (criterion_id)", + "ix_criterion_references_project_id": "CREATE INDEX ix_criterion_references_project_id ON criterion_references (project_id)" + }, + "sql": "\nCREATE TABLE criterion_references (\n\treference_candidate_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tcriterion_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\treference_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (reference_candidate_id), \n\tUNIQUE (reference_digest)\n)\n\n" + }, + "decision_objects": { + "columns": [ + "object_revision_id", + "object_id", + "project_id", + "thread_id", + "content_json", + "revision_digest", + "supersedes_revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_decision_objects_object_id": "CREATE INDEX ix_decision_objects_object_id ON decision_objects (object_id)", + "ix_decision_objects_project_id": "CREATE INDEX ix_decision_objects_project_id ON decision_objects (project_id)", + "ix_decision_objects_thread_id": "CREATE INDEX ix_decision_objects_thread_id ON decision_objects (thread_id)" + }, + "sql": "\nCREATE TABLE decision_objects (\n\tobject_revision_id VARCHAR(160) NOT NULL, \n\tobject_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tthread_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tsupersedes_revision_digest VARCHAR(64), \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (object_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "dependency_states": { + "columns": [ + "project_id", + "entity_ref", + "status", + "caused_by_revision", + "updated_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE dependency_states (\n\tproject_id VARCHAR(160) NOT NULL, \n\tentity_ref VARCHAR(260) NOT NULL, \n\tstatus VARCHAR(60) NOT NULL, \n\tcaused_by_revision VARCHAR(64) NOT NULL, \n\tupdated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (project_id, entity_ref)\n)\n\n" + }, + "entity_snapshots": { + "columns": [ + "snapshot_id", + "project_id", + "entity_type", + "entity_id", + "schema_version", + "content_json", + "content_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_entity_snapshots_entity_id": "CREATE INDEX ix_entity_snapshots_entity_id ON entity_snapshots (entity_id)", + "ix_entity_snapshots_entity_type": "CREATE INDEX ix_entity_snapshots_entity_type ON entity_snapshots (entity_type)", + "ix_entity_snapshots_project_id": "CREATE INDEX ix_entity_snapshots_project_id ON entity_snapshots (project_id)" + }, + "sql": "\nCREATE TABLE entity_snapshots (\n\tsnapshot_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tentity_type VARCHAR(60) NOT NULL, \n\tentity_id VARCHAR(160) NOT NULL, \n\tschema_version VARCHAR(40) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tcontent_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (snapshot_id)\n)\n\n" + }, + "evaluation_runs": { + "columns": [ + "project_id", + "pair_id", + "dedupe_digest", + "plan_id", + "plan_digest", + "fixture_digest", + "scorer_digest", + "state", + "revision", + "record_digest", + "content_json" + ], + "constraint_names": [ + null, + "uq_evaluation_request" + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE evaluation_runs (\n\tproject_id VARCHAR(160) NOT NULL, \n\tpair_id VARCHAR(200) NOT NULL, \n\tdedupe_digest VARCHAR(64) NOT NULL, \n\tplan_id VARCHAR(200) NOT NULL, \n\tplan_digest VARCHAR(64) NOT NULL, \n\tfixture_digest VARCHAR(64) NOT NULL, \n\tscorer_digest VARCHAR(64) NOT NULL, \n\tstate VARCHAR(32) NOT NULL, \n\trevision INTEGER NOT NULL, \n\trecord_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (project_id, pair_id), \n\tCONSTRAINT uq_evaluation_request UNIQUE (project_id, dedupe_digest)\n)\n\n" + }, + "events": { + "columns": [ + "event_id", + "project_id", + "operation_id", + "event_type", + "payload_json", + "previous_event_digest", + "event_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "ForeignKeyConstraint", + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_events_project_id": "CREATE INDEX ix_events_project_id ON events (project_id)" + }, + "sql": "\nCREATE TABLE events (\n\tevent_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\toperation_id VARCHAR(160) NOT NULL, \n\tevent_type VARCHAR(160) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tprevious_event_digest VARCHAR(64), \n\tevent_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (event_id), \n\tFOREIGN KEY(operation_id) REFERENCES operations (operation_id), \n\tUNIQUE (event_digest)\n)\n\n" + }, + "evidence_audit": { + "columns": [ + "audit_id", + "project_id", + "evidence_ref", + "event_type", + "payload_json", + "event_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_evidence_audit_evidence_ref": "CREATE INDEX ix_evidence_audit_evidence_ref ON evidence_audit (evidence_ref)", + "ix_evidence_audit_project_id": "CREATE INDEX ix_evidence_audit_project_id ON evidence_audit (project_id)" + }, + "sql": "\nCREATE TABLE evidence_audit (\n\taudit_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tevidence_ref VARCHAR(160) NOT NULL, \n\tevent_type VARCHAR(120) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tevent_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (audit_id), \n\tUNIQUE (event_digest)\n)\n\n" + }, + "evidence_conflicts": { + "columns": [ + "conflict_id", + "project_id", + "evidence_ids_json", + "field", + "status", + "reason", + "resolution_evidence_ids_json", + "conflict_digest", + "created_at", + "updated_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_evidence_conflicts_project_id": "CREATE INDEX ix_evidence_conflicts_project_id ON evidence_conflicts (project_id)" + }, + "sql": "\nCREATE TABLE evidence_conflicts (\n\tconflict_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tevidence_ids_json TEXT NOT NULL, \n\tfield VARCHAR(160) NOT NULL, \n\tstatus VARCHAR(40) NOT NULL, \n\treason TEXT NOT NULL, \n\tresolution_evidence_ids_json TEXT NOT NULL, \n\tconflict_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tupdated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (conflict_id), \n\tUNIQUE (conflict_digest)\n)\n\n" + }, + "evidence_leads": { + "columns": [ + "lead_id", + "project_id", + "investigation_id", + "search_intent_id", + "source_id", + "span_id", + "state", + "content_json", + "lead_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_evidence_leads_investigation_id": "CREATE INDEX ix_evidence_leads_investigation_id ON evidence_leads (investigation_id)", + "ix_evidence_leads_project_id": "CREATE INDEX ix_evidence_leads_project_id ON evidence_leads (project_id)", + "ix_evidence_leads_search_intent_id": "CREATE INDEX ix_evidence_leads_search_intent_id ON evidence_leads (search_intent_id)", + "ix_evidence_leads_source_id": "CREATE INDEX ix_evidence_leads_source_id ON evidence_leads (source_id)", + "ix_evidence_leads_span_id": "CREATE INDEX ix_evidence_leads_span_id ON evidence_leads (span_id)", + "ix_evidence_leads_state": "CREATE INDEX ix_evidence_leads_state ON evidence_leads (state)" + }, + "sql": "\nCREATE TABLE evidence_leads (\n\tlead_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tinvestigation_id VARCHAR(160) NOT NULL, \n\tsearch_intent_id VARCHAR(160) NOT NULL, \n\tsource_id VARCHAR(160) NOT NULL, \n\tspan_id VARCHAR(160) NOT NULL, \n\tstate VARCHAR(60) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tlead_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (lead_id), \n\tUNIQUE (lead_digest)\n)\n\n" + }, + "evidence_links": { + "columns": [ + "evidence_id", + "project_id", + "thread_id", + "target_type", + "target_id", + "relation", + "source_ids_json", + "span_ids_json", + "observation_ids_json", + "conditions_json", + "applicability", + "independence_group", + "support_status", + "authority_status", + "verification_status", + "cutoff_eligibility", + "content_trust", + "conflict_ids_json", + "supersedes_evidence_id", + "revision", + "evidence_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_evidence_links_project_id": "CREATE INDEX ix_evidence_links_project_id ON evidence_links (project_id)", + "ix_evidence_links_target_id": "CREATE INDEX ix_evidence_links_target_id ON evidence_links (target_id)" + }, + "sql": "\nCREATE TABLE evidence_links (\n\tevidence_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tthread_id VARCHAR(160), \n\ttarget_type VARCHAR(80) NOT NULL, \n\ttarget_id VARCHAR(160) NOT NULL, \n\trelation VARCHAR(40) NOT NULL, \n\tsource_ids_json TEXT NOT NULL, \n\tspan_ids_json TEXT NOT NULL, \n\tobservation_ids_json TEXT NOT NULL, \n\tconditions_json TEXT NOT NULL, \n\tapplicability TEXT NOT NULL, \n\tindependence_group VARCHAR(160) NOT NULL, \n\tsupport_status VARCHAR(60) NOT NULL, \n\tauthority_status VARCHAR(60) NOT NULL, \n\tverification_status VARCHAR(60) NOT NULL, \n\tcutoff_eligibility VARCHAR(60) NOT NULL, \n\tcontent_trust VARCHAR(60) NOT NULL, \n\tconflict_ids_json TEXT NOT NULL, \n\tsupersedes_evidence_id VARCHAR(160), \n\trevision INTEGER NOT NULL, \n\tevidence_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (evidence_id), \n\tUNIQUE (evidence_digest)\n)\n\n" + }, + "evidence_observations": { + "columns": [ + "observation_id", + "project_id", + "span_ids_json", + "observed_statement", + "observed_time", + "valid_time", + "observer_group", + "independence_basis", + "provenance_class", + "contamination_note", + "supersedes_observation_id", + "observation_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_evidence_observations_project_id": "CREATE INDEX ix_evidence_observations_project_id ON evidence_observations (project_id)" + }, + "sql": "\nCREATE TABLE evidence_observations (\n\tobservation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tspan_ids_json TEXT NOT NULL, \n\tobserved_statement TEXT NOT NULL, \n\tobserved_time VARCHAR(32), \n\tvalid_time VARCHAR(32), \n\tobserver_group VARCHAR(160) NOT NULL, \n\tindependence_basis TEXT NOT NULL, \n\tprovenance_class VARCHAR(80) NOT NULL, \n\tcontamination_note TEXT, \n\tsupersedes_observation_id VARCHAR(160), \n\tobservation_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (observation_id), \n\tUNIQUE (observation_digest)\n)\n\n" + }, + "evidence_sources": { + "columns": [ + "source_id", + "project_id", + "artifact_id", + "connector_ref", + "uri", + "artifact_type", + "version", + "sha256", + "authority_status", + "security_class", + "official_copy", + "rights", + "retention", + "valid_time", + "snapshot_time", + "cutoff_eligibility", + "lineage_root_id", + "parent_source_ids_json", + "supersedes_source_id", + "source_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_evidence_sources_artifact_id": "CREATE INDEX ix_evidence_sources_artifact_id ON evidence_sources (artifact_id)", + "ix_evidence_sources_project_id": "CREATE INDEX ix_evidence_sources_project_id ON evidence_sources (project_id)" + }, + "sql": "\nCREATE TABLE evidence_sources (\n\tsource_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tartifact_id VARCHAR(160) NOT NULL, \n\tconnector_ref VARCHAR(160) NOT NULL, \n\turi TEXT NOT NULL, \n\tartifact_type VARCHAR(160) NOT NULL, \n\tversion VARCHAR(260), \n\tsha256 VARCHAR(64) NOT NULL, \n\tauthority_status VARCHAR(60) NOT NULL, \n\tsecurity_class VARCHAR(60) NOT NULL, \n\tofficial_copy INTEGER NOT NULL, \n\trights VARCHAR(160) NOT NULL, \n\tretention VARCHAR(160) NOT NULL, \n\tvalid_time VARCHAR(32), \n\tsnapshot_time VARCHAR(32), \n\tcutoff_eligibility VARCHAR(60) NOT NULL, \n\tlineage_root_id VARCHAR(160) NOT NULL, \n\tparent_source_ids_json TEXT NOT NULL, \n\tsupersedes_source_id VARCHAR(160), \n\tsource_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (source_id), \n\tUNIQUE (source_digest)\n)\n\n" + }, + "evidence_spans": { + "columns": [ + "span_id", + "project_id", + "artifact_id", + "source_version_id", + "locator_json", + "exact_text", + "text_sha256", + "extraction_method", + "support_state", + "authority_state", + "verification_state", + "cutoff_state" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "ForeignKeyConstraint", + "ForeignKeyConstraint", + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_evidence_spans_project_id": "CREATE INDEX ix_evidence_spans_project_id ON evidence_spans (project_id)" + }, + "sql": "\nCREATE TABLE evidence_spans (\n\tspan_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tartifact_id VARCHAR(160) NOT NULL, \n\tsource_version_id VARCHAR(160) NOT NULL, \n\tlocator_json TEXT NOT NULL, \n\texact_text TEXT NOT NULL, \n\ttext_sha256 VARCHAR(64) NOT NULL, \n\textraction_method VARCHAR(160) NOT NULL, \n\tsupport_state VARCHAR(60) NOT NULL, \n\tauthority_state VARCHAR(60) NOT NULL, \n\tverification_state VARCHAR(60) NOT NULL, \n\tcutoff_state VARCHAR(60) NOT NULL, \n\tPRIMARY KEY (span_id), \n\tFOREIGN KEY(artifact_id) REFERENCES artifacts (artifact_id), \n\tFOREIGN KEY(source_version_id) REFERENCES artifact_versions (source_version_id)\n)\n\n" + }, + "execution_audit": { + "columns": [ + "audit_id", + "project_id", + "plan_execution_id", + "event_type", + "payload_json", + "event_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_execution_audit_plan_execution_id": "CREATE INDEX ix_execution_audit_plan_execution_id ON execution_audit (plan_execution_id)", + "ix_execution_audit_project_id": "CREATE INDEX ix_execution_audit_project_id ON execution_audit (project_id)" + }, + "sql": "\nCREATE TABLE execution_audit (\n\taudit_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tplan_execution_id VARCHAR(160) NOT NULL, \n\tevent_type VARCHAR(120) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tevent_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (audit_id), \n\tUNIQUE (event_digest)\n)\n\n" + }, + "execution_effects": { + "columns": [ + "effect_id", + "project_id", + "attempt_id", + "content_json", + "effect_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_execution_effects_attempt_id": "CREATE INDEX ix_execution_effects_attempt_id ON execution_effects (attempt_id)", + "ix_execution_effects_project_id": "CREATE INDEX ix_execution_effects_project_id ON execution_effects (project_id)" + }, + "sql": "\nCREATE TABLE execution_effects (\n\teffect_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tattempt_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\teffect_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (effect_id), \n\tUNIQUE (effect_digest)\n)\n\n" + }, + "execution_reconciliations": { + "columns": [ + "reconciliation_id", + "project_id", + "plan_execution_id", + "attempt_id", + "content_json", + "reconciliation_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_execution_reconciliations_attempt_id": "CREATE INDEX ix_execution_reconciliations_attempt_id ON execution_reconciliations (attempt_id)", + "ix_execution_reconciliations_plan_execution_id": "CREATE INDEX ix_execution_reconciliations_plan_execution_id ON execution_reconciliations (plan_execution_id)", + "ix_execution_reconciliations_project_id": "CREATE INDEX ix_execution_reconciliations_project_id ON execution_reconciliations (project_id)" + }, + "sql": "\nCREATE TABLE execution_reconciliations (\n\treconciliation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tplan_execution_id VARCHAR(160) NOT NULL, \n\tattempt_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\treconciliation_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (reconciliation_id), \n\tUNIQUE (reconciliation_digest)\n)\n\n" + }, + "field_events": { + "columns": [ + "event_id", + "project_id", + "session_id", + "event_type", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_field_events_project_id": "CREATE INDEX ix_field_events_project_id ON field_events (project_id)", + "ix_field_events_session_id": "CREATE INDEX ix_field_events_session_id ON field_events (session_id)" + }, + "sql": "\nCREATE TABLE field_events (\n\tevent_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsession_id VARCHAR(200) NOT NULL, \n\tevent_type VARCHAR(80) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (event_id)\n)\n\n" + }, + "field_exports": { + "columns": [ + "export_id", + "project_id", + "protocol_digest", + "bundle_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_field_exports_project_id": "CREATE INDEX ix_field_exports_project_id ON field_exports (project_id)" + }, + "sql": "\nCREATE TABLE field_exports (\n\texport_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tprotocol_digest VARCHAR(64) NOT NULL, \n\tbundle_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (export_id), \n\tUNIQUE (bundle_digest)\n)\n\n" + }, + "field_protocol_seals": { + "columns": [ + "protocol_id", + "project_id", + "protocol_digest", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_field_protocol_seals_project_id": "CREATE INDEX ix_field_protocol_seals_project_id ON field_protocol_seals (project_id)" + }, + "sql": "\nCREATE TABLE field_protocol_seals (\n\tprotocol_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tprotocol_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (protocol_id), \n\tUNIQUE (protocol_digest)\n)\n\n" + }, + "field_scores": { + "columns": [ + "score_id", + "project_id", + "session_id", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_field_scores_project_id": "CREATE INDEX ix_field_scores_project_id ON field_scores (project_id)", + "ix_field_scores_session_id": "CREATE INDEX ix_field_scores_session_id ON field_scores (session_id)" + }, + "sql": "\nCREATE TABLE field_scores (\n\tscore_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsession_id VARCHAR(200) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (score_id)\n)\n\n" + }, + "field_session_metrics": { + "columns": [ + "session_id", + "project_id", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_field_session_metrics_project_id": "CREATE INDEX ix_field_session_metrics_project_id ON field_session_metrics (project_id)" + }, + "sql": "\nCREATE TABLE field_session_metrics (\n\tsession_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (session_id)\n)\n\n" + }, + "field_sessions": { + "columns": [ + "session_id", + "project_id", + "protocol_digest", + "state", + "content_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_field_sessions_project_id": "CREATE INDEX ix_field_sessions_project_id ON field_sessions (project_id)" + }, + "sql": "\nCREATE TABLE field_sessions (\n\tsession_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tprotocol_digest VARCHAR(64) NOT NULL, \n\tstate VARCHAR(40) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tPRIMARY KEY (session_id)\n)\n\n" + }, + "governance_heads": { + "columns": [ + "project_id", + "record_kind", + "record_id", + "revision", + "record_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE governance_heads (\n\tproject_id VARCHAR(160) NOT NULL, \n\trecord_kind VARCHAR(32) NOT NULL, \n\trecord_id VARCHAR(200) NOT NULL, \n\trevision INTEGER NOT NULL, \n\trecord_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (project_id, record_kind, record_id)\n)\n\n" + }, + "governance_revisions": { + "columns": [ + "project_id", + "record_kind", + "record_id", + "revision", + "record_digest", + "content_json", + "receipt_digest", + "receipt_json" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint", + "UniqueConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE governance_revisions (\n\tproject_id VARCHAR(160) NOT NULL, \n\trecord_kind VARCHAR(32) NOT NULL, \n\trecord_id VARCHAR(200) NOT NULL, \n\trevision INTEGER NOT NULL, \n\trecord_digest VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\treceipt_digest VARCHAR(64) NOT NULL, \n\treceipt_json TEXT NOT NULL, \n\tPRIMARY KEY (project_id, record_kind, record_id, revision), \n\tUNIQUE (record_digest), \n\tUNIQUE (receipt_digest)\n)\n\n" + }, + "hypothesis_appraisals": { + "columns": [ + "appraisal_id", + "project_id", + "hypothesis_id", + "content_json", + "appraisal_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_hypothesis_appraisals_hypothesis_id": "CREATE INDEX ix_hypothesis_appraisals_hypothesis_id ON hypothesis_appraisals (hypothesis_id)", + "ix_hypothesis_appraisals_project_id": "CREATE INDEX ix_hypothesis_appraisals_project_id ON hypothesis_appraisals (project_id)" + }, + "sql": "\nCREATE TABLE hypothesis_appraisals (\n\tappraisal_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\thypothesis_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tappraisal_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (appraisal_id), \n\tUNIQUE (appraisal_digest)\n)\n\n" + }, + "hypothesis_assumptions": { + "columns": [ + "assumption_id", + "project_id", + "hypothesis_id", + "content_json", + "assumption_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_hypothesis_assumptions_hypothesis_id": "CREATE INDEX ix_hypothesis_assumptions_hypothesis_id ON hypothesis_assumptions (hypothesis_id)", + "ix_hypothesis_assumptions_project_id": "CREATE INDEX ix_hypothesis_assumptions_project_id ON hypothesis_assumptions (project_id)" + }, + "sql": "\nCREATE TABLE hypothesis_assumptions (\n\tassumption_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\thypothesis_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tassumption_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (assumption_id), \n\tUNIQUE (assumption_digest)\n)\n\n" + }, + "hypothesis_audit": { + "columns": [ + "audit_id", + "project_id", + "hypothesis_id", + "event_type", + "payload_json", + "event_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_hypothesis_audit_hypothesis_id": "CREATE INDEX ix_hypothesis_audit_hypothesis_id ON hypothesis_audit (hypothesis_id)", + "ix_hypothesis_audit_project_id": "CREATE INDEX ix_hypothesis_audit_project_id ON hypothesis_audit (project_id)" + }, + "sql": "\nCREATE TABLE hypothesis_audit (\n\taudit_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\thypothesis_id VARCHAR(160) NOT NULL, \n\tevent_type VARCHAR(120) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tevent_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (audit_id), \n\tUNIQUE (event_digest)\n)\n\n" + }, + "hypothesis_portfolios": { + "columns": [ + "portfolio_revision_id", + "portfolio_id", + "project_id", + "object_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_hypothesis_portfolios_object_id": "CREATE INDEX ix_hypothesis_portfolios_object_id ON hypothesis_portfolios (object_id)", + "ix_hypothesis_portfolios_portfolio_id": "CREATE INDEX ix_hypothesis_portfolios_portfolio_id ON hypothesis_portfolios (portfolio_id)", + "ix_hypothesis_portfolios_project_id": "CREATE INDEX ix_hypothesis_portfolios_project_id ON hypothesis_portfolios (project_id)" + }, + "sql": "\nCREATE TABLE hypothesis_portfolios (\n\tportfolio_revision_id VARCHAR(160) NOT NULL, \n\tportfolio_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tobject_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (portfolio_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "hypothesis_predictions": { + "columns": [ + "prediction_id", + "project_id", + "hypothesis_id", + "content_json", + "prediction_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_hypothesis_predictions_hypothesis_id": "CREATE INDEX ix_hypothesis_predictions_hypothesis_id ON hypothesis_predictions (hypothesis_id)", + "ix_hypothesis_predictions_project_id": "CREATE INDEX ix_hypothesis_predictions_project_id ON hypothesis_predictions (project_id)" + }, + "sql": "\nCREATE TABLE hypothesis_predictions (\n\tprediction_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\thypothesis_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tprediction_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (prediction_id), \n\tUNIQUE (prediction_digest)\n)\n\n" + }, + "hypothesis_records": { + "columns": [ + "hypothesis_revision_id", + "hypothesis_id", + "project_id", + "object_id", + "portfolio_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_hypothesis_records_hypothesis_id": "CREATE INDEX ix_hypothesis_records_hypothesis_id ON hypothesis_records (hypothesis_id)", + "ix_hypothesis_records_object_id": "CREATE INDEX ix_hypothesis_records_object_id ON hypothesis_records (object_id)", + "ix_hypothesis_records_portfolio_id": "CREATE INDEX ix_hypothesis_records_portfolio_id ON hypothesis_records (portfolio_id)", + "ix_hypothesis_records_project_id": "CREATE INDEX ix_hypothesis_records_project_id ON hypothesis_records (project_id)" + }, + "sql": "\nCREATE TABLE hypothesis_records (\n\thypothesis_revision_id VARCHAR(160) NOT NULL, \n\thypothesis_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tobject_id VARCHAR(160) NOT NULL, \n\tportfolio_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (hypothesis_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "hypothesis_relations": { + "columns": [ + "relation_revision_id", + "relation_id", + "project_id", + "portfolio_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_hypothesis_relations_portfolio_id": "CREATE INDEX ix_hypothesis_relations_portfolio_id ON hypothesis_relations (portfolio_id)", + "ix_hypothesis_relations_project_id": "CREATE INDEX ix_hypothesis_relations_project_id ON hypothesis_relations (project_id)", + "ix_hypothesis_relations_relation_id": "CREATE INDEX ix_hypothesis_relations_relation_id ON hypothesis_relations (relation_id)" + }, + "sql": "\nCREATE TABLE hypothesis_relations (\n\trelation_revision_id VARCHAR(160) NOT NULL, \n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tportfolio_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (relation_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "hypothesis_test_bindings": { + "columns": [ + "test_binding_id", + "project_id", + "prediction_id", + "content_json", + "binding_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_hypothesis_test_bindings_prediction_id": "CREATE INDEX ix_hypothesis_test_bindings_prediction_id ON hypothesis_test_bindings (prediction_id)", + "ix_hypothesis_test_bindings_project_id": "CREATE INDEX ix_hypothesis_test_bindings_project_id ON hypothesis_test_bindings (project_id)" + }, + "sql": "\nCREATE TABLE hypothesis_test_bindings (\n\ttest_binding_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tprediction_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tbinding_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (test_binding_id), \n\tUNIQUE (binding_digest)\n)\n\n" + }, + "idempotency_keys": { + "columns": [ + "project_id", + "method", + "idempotency_key", + "scope_digest", + "operation_id" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "ForeignKeyConstraint", + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE idempotency_keys (\n\tproject_id VARCHAR(160) NOT NULL, \n\tmethod VARCHAR(160) NOT NULL, \n\tidempotency_key VARCHAR(260) NOT NULL, \n\tscope_digest VARCHAR(64) NOT NULL, \n\toperation_id VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (project_id, method, idempotency_key), \n\tFOREIGN KEY(operation_id) REFERENCES operations (operation_id)\n)\n\n" + }, + "investigation_audit": { + "columns": [ + "audit_id", + "project_id", + "investigation_id", + "event_type", + "payload_json", + "event_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_investigation_audit_investigation_id": "CREATE INDEX ix_investigation_audit_investigation_id ON investigation_audit (investigation_id)", + "ix_investigation_audit_project_id": "CREATE INDEX ix_investigation_audit_project_id ON investigation_audit (project_id)" + }, + "sql": "\nCREATE TABLE investigation_audit (\n\taudit_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tinvestigation_id VARCHAR(160) NOT NULL, \n\tevent_type VARCHAR(120) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tevent_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (audit_id), \n\tUNIQUE (event_digest)\n)\n\n" + }, + "investigations": { + "columns": [ + "investigation_id", + "project_id", + "thread_id", + "cycle_id", + "parent_investigation_id", + "trigger", + "question", + "target_object_id", + "target_hypothesis_id", + "mode", + "scope_json", + "required_evidence_groups_json", + "query_families_json", + "counter_search_policy", + "budget", + "budget_usage", + "stop_conditions_json", + "domain_state", + "execution_state", + "current_wave", + "observation_count", + "open_lead_count", + "claim_candidate_count", + "gap_count", + "sufficiency_json", + "checkpoint_digest", + "result_json", + "plan_revision", + "investigation_digest", + "created_at", + "updated_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_investigations_project_id": "CREATE INDEX ix_investigations_project_id ON investigations (project_id)", + "ix_investigations_thread_id": "CREATE INDEX ix_investigations_thread_id ON investigations (thread_id)" + }, + "sql": "\nCREATE TABLE investigations (\n\tinvestigation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tthread_id VARCHAR(160) NOT NULL, \n\tcycle_id VARCHAR(160) NOT NULL, \n\tparent_investigation_id VARCHAR(160), \n\t\"trigger\" VARCHAR(80) NOT NULL, \n\tquestion TEXT NOT NULL, \n\ttarget_object_id VARCHAR(160), \n\ttarget_hypothesis_id VARCHAR(160), \n\tmode VARCHAR(40) NOT NULL, \n\tscope_json TEXT NOT NULL, \n\trequired_evidence_groups_json TEXT NOT NULL, \n\tquery_families_json TEXT NOT NULL, \n\tcounter_search_policy VARCHAR(160) NOT NULL, \n\tbudget INTEGER NOT NULL, \n\tbudget_usage INTEGER NOT NULL, \n\tstop_conditions_json TEXT NOT NULL, \n\tdomain_state VARCHAR(40) NOT NULL, \n\texecution_state VARCHAR(40) NOT NULL, \n\tcurrent_wave INTEGER NOT NULL, \n\tobservation_count INTEGER NOT NULL, \n\topen_lead_count INTEGER NOT NULL, \n\tclaim_candidate_count INTEGER NOT NULL, \n\tgap_count INTEGER NOT NULL, \n\tsufficiency_json TEXT NOT NULL, \n\tcheckpoint_digest VARCHAR(64), \n\tresult_json TEXT, \n\tplan_revision INTEGER NOT NULL, \n\tinvestigation_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tupdated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (investigation_id), \n\tUNIQUE (investigation_digest)\n)\n\n" + }, + "memory_context_packs": { + "columns": [ + "context_pack_id", + "project_id", + "thread_id", + "content_json", + "query_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_memory_context_packs_project_id": "CREATE INDEX ix_memory_context_packs_project_id ON memory_context_packs (project_id)" + }, + "sql": "\nCREATE TABLE memory_context_packs (\n\tcontext_pack_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tthread_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tquery_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (context_pack_id)\n)\n\n" + }, + "memory_projections": { + "columns": [ + "projection_id", + "project_id", + "projection_type", + "content_json", + "rebuild_checkpoint" + ], + "constraint_names": [ + null, + "uq_memory_projection_type" + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_memory_projections_project_id": "CREATE INDEX ix_memory_projections_project_id ON memory_projections (project_id)" + }, + "sql": "\nCREATE TABLE memory_projections (\n\tprojection_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tprojection_type VARCHAR(40) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trebuild_checkpoint VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (projection_id), \n\tCONSTRAINT uq_memory_projection_type UNIQUE (project_id, projection_type)\n)\n\n" + }, + "memory_records": { + "columns": [ + "memory_id", + "project_id", + "payload_mode", + "kind", + "owner_revision_ref", + "source_ref", + "assertion", + "recall_eligibility", + "lifecycle", + "revision_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": { + "ix_memory_records_project_id": "CREATE INDEX ix_memory_records_project_id ON memory_records (project_id)" + }, + "sql": "\nCREATE TABLE memory_records (\n\tmemory_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tpayload_mode VARCHAR(60) NOT NULL, \n\tkind VARCHAR(60) NOT NULL, \n\towner_revision_ref VARCHAR(160) NOT NULL, \n\tsource_ref VARCHAR(260), \n\tassertion TEXT, \n\trecall_eligibility VARCHAR(60) NOT NULL, \n\tlifecycle VARCHAR(60) NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (memory_id)\n)\n\n" + }, + "memory_revision_ledger": { + "columns": [ + "memory_revision_id", + "memory_id", + "project_id", + "transition", + "owner_revision_ref", + "content_json", + "revision_digest" + ], + "constraint_names": [ + null, + "uq_memory_revision_source_memory" + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_memory_revision_ledger_project_id": "CREATE INDEX ix_memory_revision_ledger_project_id ON memory_revision_ledger (project_id)" + }, + "sql": "\nCREATE TABLE memory_revision_ledger (\n\tmemory_revision_id VARCHAR(200) NOT NULL, \n\tmemory_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\ttransition VARCHAR(40) NOT NULL, \n\towner_revision_ref VARCHAR(64) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (memory_revision_id), \n\tCONSTRAINT uq_memory_revision_source_memory UNIQUE (project_id, memory_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "memory_transition_receipts": { + "columns": [ + "receipt_id", + "project_id", + "memory_revision_id", + "content_json", + "receipt_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_memory_transition_receipts_project_id": "CREATE INDEX ix_memory_transition_receipts_project_id ON memory_transition_receipts (project_id)" + }, + "sql": "\nCREATE TABLE memory_transition_receipts (\n\treceipt_id VARCHAR(200) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tmemory_revision_id VARCHAR(200) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\treceipt_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (receipt_id), \n\tUNIQUE (memory_revision_id), \n\tUNIQUE (receipt_digest)\n)\n\n" + }, + "object_attention": { + "columns": [ + "attention_revision_id", + "attention_id", + "project_id", + "object_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_object_attention_attention_id": "CREATE INDEX ix_object_attention_attention_id ON object_attention (attention_id)", + "ix_object_attention_object_id": "CREATE INDEX ix_object_attention_object_id ON object_attention (object_id)", + "ix_object_attention_project_id": "CREATE INDEX ix_object_attention_project_id ON object_attention (project_id)" + }, + "sql": "\nCREATE TABLE object_attention (\n\tattention_revision_id VARCHAR(160) NOT NULL, \n\tattention_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tobject_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (attention_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "object_audit": { + "columns": [ + "audit_id", + "project_id", + "object_id", + "event_type", + "payload_json", + "event_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_object_audit_object_id": "CREATE INDEX ix_object_audit_object_id ON object_audit (object_id)", + "ix_object_audit_project_id": "CREATE INDEX ix_object_audit_project_id ON object_audit (project_id)" + }, + "sql": "\nCREATE TABLE object_audit (\n\taudit_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tobject_id VARCHAR(160) NOT NULL, \n\tevent_type VARCHAR(120) NOT NULL, \n\tpayload_json TEXT NOT NULL, \n\tevent_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (audit_id), \n\tUNIQUE (event_digest)\n)\n\n" + }, + "object_candidates": { + "columns": [ + "candidate_id", + "project_id", + "thread_id", + "content_json", + "candidate_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_object_candidates_project_id": "CREATE INDEX ix_object_candidates_project_id ON object_candidates (project_id)", + "ix_object_candidates_thread_id": "CREATE INDEX ix_object_candidates_thread_id ON object_candidates (thread_id)" + }, + "sql": "\nCREATE TABLE object_candidates (\n\tcandidate_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tthread_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tcandidate_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (candidate_id), \n\tUNIQUE (candidate_digest)\n)\n\n" + }, + "object_profiles": { + "columns": [ + "profile_ref", + "version", + "content_json", + "enabled", + "profile_digest" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE object_profiles (\n\tprofile_ref VARCHAR(160) NOT NULL, \n\tversion INTEGER NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\tenabled INTEGER NOT NULL, \n\tprofile_digest VARCHAR(64) NOT NULL, \n\tPRIMARY KEY (profile_ref, version), \n\tUNIQUE (profile_digest)\n)\n\n" + }, + "object_relations": { + "columns": [ + "relation_revision_id", + "relation_id", + "project_id", + "source_object_id", + "content_json", + "revision_digest", + "created_at" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint", + "UniqueConstraint" + ], + "indexes": { + "ix_object_relations_project_id": "CREATE INDEX ix_object_relations_project_id ON object_relations (project_id)", + "ix_object_relations_relation_id": "CREATE INDEX ix_object_relations_relation_id ON object_relations (relation_id)", + "ix_object_relations_source_object_id": "CREATE INDEX ix_object_relations_source_object_id ON object_relations (source_object_id)" + }, + "sql": "\nCREATE TABLE object_relations (\n\trelation_revision_id VARCHAR(160) NOT NULL, \n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_object_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (relation_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" + }, + "operation_owners": { + "columns": [ + "operation_id", + "worker_id", + "process_id", + "process_marker", + "host_token" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE operation_owners (\n\toperation_id VARCHAR(160) NOT NULL, \n\tworker_id VARCHAR(160) NOT NULL, \n\tprocess_id INTEGER NOT NULL, \n\tprocess_marker VARCHAR(160) NOT NULL, \n\thost_token VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (operation_id)\n)\n\n" + }, "operation_resource_bindings": { "columns": [ "operation_id", @@ -3215,7 +6474,7 @@ } } }, - "thoth.adapters.storage.evaluation_run_schema": { + "thoth.adapters.storage.governance_history_schema": { "consumer_aliases": { "thoth.adapters.storage.acquisition.artifact_versions": "artifact_versions", "thoth.adapters.storage.acquisition.artifacts": "artifacts", @@ -3310,6 +6569,8 @@ "thoth.adapters.storage.memory.memory_records": "memory_records", "thoth.adapters.storage.memory.memory_revision_ledger": "memory_revision_ledger", "thoth.adapters.storage.memory.memory_transition_receipts": "memory_transition_receipts", + "thoth.adapters.storage.operation_owner_schema.operation_owners": "operation_owners", + "thoth.adapters.storage.operation_owner_store.operation_owners": "operation_owners", "thoth.adapters.storage.operations.idempotency_keys": "idempotency_keys", "thoth.adapters.storage.operations.memory_records": "memory_records", "thoth.adapters.storage.operations.memory_revision_ledger": "memory_revision_ledger", @@ -3544,13 +6805,14 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", "operation_resource_bindings", - "evaluation_runs", "behavior_exposure_history", "behavior_baseline_history", + "evaluation_runs", "research_test_assessments", "semantic_revisions", "artifact_versions", @@ -3821,6 +7083,7 @@ "object_candidates", "object_profiles", "object_relations", + "operation_owners", "operation_resource_bindings", "operations", "organizations", @@ -3969,13 +7232,14 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", "operation_resource_bindings", - "evaluation_runs", "behavior_exposure_history", "behavior_baseline_history", + "evaluation_runs", "research_test_assessments" ], "tables": { @@ -5540,6 +8804,23 @@ }, "sql": "\nCREATE TABLE object_relations (\n\trelation_revision_id VARCHAR(160) NOT NULL, \n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_object_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (relation_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" }, + "operation_owners": { + "columns": [ + "operation_id", + "worker_id", + "process_id", + "process_marker", + "host_token" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE operation_owners (\n\toperation_id VARCHAR(160) NOT NULL, \n\tworker_id VARCHAR(160) NOT NULL, \n\tprocess_id INTEGER NOT NULL, \n\tprocess_marker VARCHAR(160) NOT NULL, \n\thost_token VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (operation_id)\n)\n\n" + }, "operation_resource_bindings": { "columns": [ "operation_id", @@ -6430,7 +9711,7 @@ } } }, - "thoth.adapters.storage.governance_history_schema": { + "thoth.adapters.storage.operation_owner_schema": { "consumer_aliases": { "thoth.adapters.storage.acquisition.artifact_versions": "artifact_versions", "thoth.adapters.storage.acquisition.artifacts": "artifacts", @@ -6525,6 +9806,8 @@ "thoth.adapters.storage.memory.memory_records": "memory_records", "thoth.adapters.storage.memory.memory_revision_ledger": "memory_revision_ledger", "thoth.adapters.storage.memory.memory_transition_receipts": "memory_transition_receipts", + "thoth.adapters.storage.operation_owner_schema.operation_owners": "operation_owners", + "thoth.adapters.storage.operation_owner_store.operation_owners": "operation_owners", "thoth.adapters.storage.operations.idempotency_keys": "idempotency_keys", "thoth.adapters.storage.operations.memory_records": "memory_records", "thoth.adapters.storage.operations.memory_revision_ledger": "memory_revision_ledger", @@ -6759,6 +10042,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -7036,6 +10320,7 @@ "object_candidates", "object_profiles", "object_relations", + "operation_owners", "operation_resource_bindings", "operations", "organizations", @@ -7184,6 +10469,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -8755,6 +12041,23 @@ }, "sql": "\nCREATE TABLE object_relations (\n\trelation_revision_id VARCHAR(160) NOT NULL, \n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_object_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (relation_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" }, + "operation_owners": { + "columns": [ + "operation_id", + "worker_id", + "process_id", + "process_marker", + "host_token" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE operation_owners (\n\toperation_id VARCHAR(160) NOT NULL, \n\tworker_id VARCHAR(160) NOT NULL, \n\tprocess_id INTEGER NOT NULL, \n\tprocess_marker VARCHAR(160) NOT NULL, \n\thost_token VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (operation_id)\n)\n\n" + }, "operation_resource_bindings": { "columns": [ "operation_id", @@ -9740,6 +13043,8 @@ "thoth.adapters.storage.memory.memory_records": "memory_records", "thoth.adapters.storage.memory.memory_revision_ledger": "memory_revision_ledger", "thoth.adapters.storage.memory.memory_transition_receipts": "memory_transition_receipts", + "thoth.adapters.storage.operation_owner_schema.operation_owners": "operation_owners", + "thoth.adapters.storage.operation_owner_store.operation_owners": "operation_owners", "thoth.adapters.storage.operations.idempotency_keys": "idempotency_keys", "thoth.adapters.storage.operations.memory_records": "memory_records", "thoth.adapters.storage.operations.memory_revision_ledger": "memory_revision_ledger", @@ -9974,6 +13279,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -10251,6 +13557,7 @@ "object_candidates", "object_profiles", "object_relations", + "operation_owners", "operation_resource_bindings", "operations", "organizations", @@ -10399,6 +13706,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -11970,6 +15278,23 @@ }, "sql": "\nCREATE TABLE object_relations (\n\trelation_revision_id VARCHAR(160) NOT NULL, \n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_object_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (relation_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" }, + "operation_owners": { + "columns": [ + "operation_id", + "worker_id", + "process_id", + "process_marker", + "host_token" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE operation_owners (\n\toperation_id VARCHAR(160) NOT NULL, \n\tworker_id VARCHAR(160) NOT NULL, \n\tprocess_id INTEGER NOT NULL, \n\tprocess_marker VARCHAR(160) NOT NULL, \n\thost_token VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (operation_id)\n)\n\n" + }, "operation_resource_bindings": { "columns": [ "operation_id", @@ -12955,6 +16280,8 @@ "thoth.adapters.storage.memory.memory_records": "memory_records", "thoth.adapters.storage.memory.memory_revision_ledger": "memory_revision_ledger", "thoth.adapters.storage.memory.memory_transition_receipts": "memory_transition_receipts", + "thoth.adapters.storage.operation_owner_schema.operation_owners": "operation_owners", + "thoth.adapters.storage.operation_owner_store.operation_owners": "operation_owners", "thoth.adapters.storage.operations.idempotency_keys": "idempotency_keys", "thoth.adapters.storage.operations.memory_records": "memory_records", "thoth.adapters.storage.operations.memory_revision_ledger": "memory_revision_ledger", @@ -13189,6 +16516,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -13466,6 +16794,7 @@ "object_candidates", "object_profiles", "object_relations", + "operation_owners", "operation_resource_bindings", "operations", "organizations", @@ -13614,6 +16943,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -15185,6 +18515,23 @@ }, "sql": "\nCREATE TABLE object_relations (\n\trelation_revision_id VARCHAR(160) NOT NULL, \n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_object_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (relation_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" }, + "operation_owners": { + "columns": [ + "operation_id", + "worker_id", + "process_id", + "process_marker", + "host_token" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE operation_owners (\n\toperation_id VARCHAR(160) NOT NULL, \n\tworker_id VARCHAR(160) NOT NULL, \n\tprocess_id INTEGER NOT NULL, \n\tprocess_marker VARCHAR(160) NOT NULL, \n\thost_token VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (operation_id)\n)\n\n" + }, "operation_resource_bindings": { "columns": [ "operation_id", @@ -16170,6 +19517,8 @@ "thoth.adapters.storage.memory.memory_records": "memory_records", "thoth.adapters.storage.memory.memory_revision_ledger": "memory_revision_ledger", "thoth.adapters.storage.memory.memory_transition_receipts": "memory_transition_receipts", + "thoth.adapters.storage.operation_owner_schema.operation_owners": "operation_owners", + "thoth.adapters.storage.operation_owner_store.operation_owners": "operation_owners", "thoth.adapters.storage.operations.idempotency_keys": "idempotency_keys", "thoth.adapters.storage.operations.memory_records": "memory_records", "thoth.adapters.storage.operations.memory_revision_ledger": "memory_revision_ledger", @@ -16404,6 +19753,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -16681,6 +20031,7 @@ "object_candidates", "object_profiles", "object_relations", + "operation_owners", "operation_resource_bindings", "operations", "organizations", @@ -16829,6 +20180,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -18400,6 +21752,23 @@ }, "sql": "\nCREATE TABLE object_relations (\n\trelation_revision_id VARCHAR(160) NOT NULL, \n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_object_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (relation_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" }, + "operation_owners": { + "columns": [ + "operation_id", + "worker_id", + "process_id", + "process_marker", + "host_token" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE operation_owners (\n\toperation_id VARCHAR(160) NOT NULL, \n\tworker_id VARCHAR(160) NOT NULL, \n\tprocess_id INTEGER NOT NULL, \n\tprocess_marker VARCHAR(160) NOT NULL, \n\thost_token VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (operation_id)\n)\n\n" + }, "operation_resource_bindings": { "columns": [ "operation_id", @@ -19385,6 +22754,8 @@ "thoth.adapters.storage.memory.memory_records": "memory_records", "thoth.adapters.storage.memory.memory_revision_ledger": "memory_revision_ledger", "thoth.adapters.storage.memory.memory_transition_receipts": "memory_transition_receipts", + "thoth.adapters.storage.operation_owner_schema.operation_owners": "operation_owners", + "thoth.adapters.storage.operation_owner_store.operation_owners": "operation_owners", "thoth.adapters.storage.operations.idempotency_keys": "idempotency_keys", "thoth.adapters.storage.operations.memory_records": "memory_records", "thoth.adapters.storage.operations.memory_revision_ledger": "memory_revision_ledger", @@ -19619,6 +22990,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -19896,6 +23268,7 @@ "object_candidates", "object_profiles", "object_relations", + "operation_owners", "operation_resource_bindings", "operations", "organizations", @@ -20044,6 +23417,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -21615,6 +24989,23 @@ }, "sql": "\nCREATE TABLE object_relations (\n\trelation_revision_id VARCHAR(160) NOT NULL, \n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_object_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (relation_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" }, + "operation_owners": { + "columns": [ + "operation_id", + "worker_id", + "process_id", + "process_marker", + "host_token" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE operation_owners (\n\toperation_id VARCHAR(160) NOT NULL, \n\tworker_id VARCHAR(160) NOT NULL, \n\tprocess_id INTEGER NOT NULL, \n\tprocess_marker VARCHAR(160) NOT NULL, \n\thost_token VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (operation_id)\n)\n\n" + }, "operation_resource_bindings": { "columns": [ "operation_id", @@ -22600,6 +25991,8 @@ "thoth.adapters.storage.memory.memory_records": "memory_records", "thoth.adapters.storage.memory.memory_revision_ledger": "memory_revision_ledger", "thoth.adapters.storage.memory.memory_transition_receipts": "memory_transition_receipts", + "thoth.adapters.storage.operation_owner_schema.operation_owners": "operation_owners", + "thoth.adapters.storage.operation_owner_store.operation_owners": "operation_owners", "thoth.adapters.storage.operations.idempotency_keys": "idempotency_keys", "thoth.adapters.storage.operations.memory_records": "memory_records", "thoth.adapters.storage.operations.memory_revision_ledger": "memory_revision_ledger", @@ -22834,6 +26227,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -23111,6 +26505,7 @@ "object_candidates", "object_profiles", "object_relations", + "operation_owners", "operation_resource_bindings", "operations", "organizations", @@ -23259,6 +26654,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -24830,6 +28226,23 @@ }, "sql": "\nCREATE TABLE object_relations (\n\trelation_revision_id VARCHAR(160) NOT NULL, \n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_object_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (relation_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" }, + "operation_owners": { + "columns": [ + "operation_id", + "worker_id", + "process_id", + "process_marker", + "host_token" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE operation_owners (\n\toperation_id VARCHAR(160) NOT NULL, \n\tworker_id VARCHAR(160) NOT NULL, \n\tprocess_id INTEGER NOT NULL, \n\tprocess_marker VARCHAR(160) NOT NULL, \n\thost_token VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (operation_id)\n)\n\n" + }, "operation_resource_bindings": { "columns": [ "operation_id", @@ -25818,6 +29231,8 @@ "thoth.adapters.storage.memory.memory_records": "memory_records", "thoth.adapters.storage.memory.memory_revision_ledger": "memory_revision_ledger", "thoth.adapters.storage.memory.memory_transition_receipts": "memory_transition_receipts", + "thoth.adapters.storage.operation_owner_schema.operation_owners": "operation_owners", + "thoth.adapters.storage.operation_owner_store.operation_owners": "operation_owners", "thoth.adapters.storage.operations.idempotency_keys": "idempotency_keys", "thoth.adapters.storage.operations.memory_records": "memory_records", "thoth.adapters.storage.operations.memory_revision_ledger": "memory_revision_ledger", @@ -26063,6 +29478,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -26340,6 +29756,7 @@ "object_candidates", "object_profiles", "object_relations", + "operation_owners", "operation_resource_bindings", "operations", "organizations", @@ -26488,6 +29905,7 @@ "governance_revisions", "governance_heads", "research_identities", + "operation_owners", "resource_scope_history", "resource_scope_heads", "resource_scope_receipts", @@ -28059,6 +31477,23 @@ }, "sql": "\nCREATE TABLE object_relations (\n\trelation_revision_id VARCHAR(160) NOT NULL, \n\trelation_id VARCHAR(160) NOT NULL, \n\tproject_id VARCHAR(160) NOT NULL, \n\tsource_object_id VARCHAR(160) NOT NULL, \n\tcontent_json TEXT NOT NULL, \n\trevision_digest VARCHAR(64) NOT NULL, \n\tcreated_at VARCHAR(32) NOT NULL, \n\tPRIMARY KEY (relation_revision_id), \n\tUNIQUE (revision_digest)\n)\n\n" }, + "operation_owners": { + "columns": [ + "operation_id", + "worker_id", + "process_id", + "process_marker", + "host_token" + ], + "constraint_names": [ + null + ], + "constraint_types": [ + "PrimaryKeyConstraint" + ], + "indexes": {}, + "sql": "\nCREATE TABLE operation_owners (\n\toperation_id VARCHAR(160) NOT NULL, \n\tworker_id VARCHAR(160) NOT NULL, \n\tprocess_id INTEGER NOT NULL, \n\tprocess_marker VARCHAR(160) NOT NULL, \n\thost_token VARCHAR(160) NOT NULL, \n\tPRIMARY KEY (operation_id)\n)\n\n" + }, "operation_resource_bindings": { "columns": [ "operation_id", diff --git a/tests/integration/migration_head_helpers.py b/tests/integration/migration_head_helpers.py index afbefad..b2e2c08 100644 --- a/tests/integration/migration_head_helpers.py +++ b/tests/integration/migration_head_helpers.py @@ -6,8 +6,8 @@ from alembic.script import ScriptDirectory ROOT = Path(__file__).resolve().parents[2] -CURRENT_HEAD = "3b06e4f87d92" -PREVIOUS_HEAD = "2af5d3e76c81" +CURRENT_HEAD = "4c17f5089ea3" +PREVIOUS_HEAD = "3b06e4f87d92" def current_single_head() -> str: diff --git a/tests/integration/schema_contract_probe.py b/tests/integration/schema_contract_probe.py index ea86f58..318f543 100644 --- a/tests/integration/schema_contract_probe.py +++ b/tests/integration/schema_contract_probe.py @@ -23,6 +23,7 @@ "thoth.adapters.storage.evaluation_run_schema", "thoth.adapters.storage.test_validity_schema", "thoth.adapters.storage.governance_history_schema", + "thoth.adapters.storage.operation_owner_schema", "thoth.apps.runtime", ) EXTENSIONS = ( @@ -32,6 +33,7 @@ "resource_scope_schema", "test_validity_schema", "governance_history_schema", + "operation_owner_schema", ) diff --git a/tests/integration/storage_coverage_helpers.py b/tests/integration/storage_coverage_helpers.py index 6387888..657e367 100644 --- a/tests/integration/storage_coverage_helpers.py +++ b/tests/integration/storage_coverage_helpers.py @@ -20,6 +20,7 @@ "events", "checkpoints", "operation_resource_bindings", + "operation_owners", } diff --git a/tests/integration/test_action_step_add_step_id.py b/tests/integration/test_action_step_add_step_id.py index 11ce5bd..84c8a57 100644 --- a/tests/integration/test_action_step_add_step_id.py +++ b/tests/integration/test_action_step_add_step_id.py @@ -14,7 +14,12 @@ from typing import Any import pytest -from tests.atomicity.harness import assert_phase_delta, failed_command_allowances, snapshot +from tests.atomicity.harness import ( + assert_phase_delta, + failed_command_allowances, + snapshot, + unchanged_or_operation_owner, +) from tests.integration.scoped_runtime import create_runtime from tests.integration.storage_coverage_helpers import request, value from tests.integration.test_a03_storage_atomicity import setup @@ -119,7 +124,10 @@ async def test_failed_step_add_never_publishes_a_plan_revision( assert named in await plan_steps(runtime, ctx, "d-step-read-success") return domain_changed = sorted( - name for name in before if name not in BOOKKEEPING and before[name] != after[name] + name + for name in before + if name not in BOOKKEEPING + and not unchanged_or_operation_owner(before, after, name, command) ) assert domain_changed == [], ("error response left published rows", domain_changed) assert heads_after == heads_before diff --git a/tests/integration/test_d1_object_followup_atomicity.py b/tests/integration/test_d1_object_followup_atomicity.py index 028415d..f99d28d 100644 --- a/tests/integration/test_d1_object_followup_atomicity.py +++ b/tests/integration/test_d1_object_followup_atomicity.py @@ -305,7 +305,11 @@ def wrapped(instance: Any, *args: Any, **kwargs: Any) -> Any: "idempotency_keys", "events", "operation_resource_bindings", - } + } | ( + {"operation_owners"} + if before["operation_owners"] != after["operation_owners"] + else set() + ) finally: runtime.close() reopened = create_runtime(tmp_path) diff --git a/tests/integration/test_m3_action_authorization_atomicity.py b/tests/integration/test_m3_action_authorization_atomicity.py index af99fea..17ed7b2 100644 --- a/tests/integration/test_m3_action_authorization_atomicity.py +++ b/tests/integration/test_m3_action_authorization_atomicity.py @@ -12,7 +12,12 @@ import pytest from sqlalchemy import func, select -from tests.atomicity.harness import assert_phase_delta, failed_command_allowances, snapshot +from tests.atomicity.harness import ( + assert_phase_delta, + failed_command_allowances, + snapshot, + unchanged_or_operation_owner, +) from tests.integration.scoped_runtime import create_runtime from tests.integration.storage_coverage_helpers import prepare_project, request, value from tests.integration.test_a04_storage_authority import ( @@ -313,7 +318,10 @@ def observe(connection: Any, table_name: str, identity: dict[str, Any]) -> None: ) external = snapshot(runtime.ledger.engine) external_changes = sorted( - name for name in before if name not in BOOKKEEPING and external[name] != before[name] + name + for name in before + if name not in BOOKKEEPING + and not unchanged_or_operation_owner(before, external, name, command) ) staged: list[dict[str, Any]] = [] for participant in state["participants"]: @@ -478,7 +486,11 @@ async def test_authorization_post_write_failure_is_atomic( "observed_resource_uses": actual_reads, "actual_callable": binding, "fault": state, - "changed_tables": sorted(name for name in before if before[name] != after[name]), + "changed_tables": sorted( + name + for name in before + if not unchanged_or_operation_owner(before, after, name, command) + ), "execution_rows_before_after": [ counts_before, (row_count(runtime, plan_executions), row_count(runtime, step_execution_attempts)), @@ -487,9 +499,11 @@ async def test_authorization_post_write_failure_is_atomic( assert len(state["hits"]) == 1, "HOLD_CONTRACT_MISMATCH: expected target hit1" assert not state["violations"], ("HOLD_DEFECT", state["violations"]) assert failed.error is not None, "HOLD_DEFECT: post-write failure reported success" - assert all(before[t] == after[t] for t in before if t not in BOOKKEEPING), ( - "HOLD_DEFECT: failed mutation published domain rows" - ) + assert all( + unchanged_or_operation_owner(before, after, t, command) + for t in before + if t not in BOOKKEEPING + ), "HOLD_DEFECT: failed mutation published domain rows" assert evidence["execution_rows_before_after"][1] == counts_before assert actual_reads == [(ref, "READ") for ref in reads], ( "HOLD_CONTRACT_MISMATCH: source-defined read-set differs", diff --git a/tests/integration/test_m3_action_mutation_atomicity.py b/tests/integration/test_m3_action_mutation_atomicity.py index 76f4b8c..adf6f50 100644 --- a/tests/integration/test_m3_action_mutation_atomicity.py +++ b/tests/integration/test_m3_action_mutation_atomicity.py @@ -8,7 +8,12 @@ import pytest from sqlalchemy import event, select -from tests.atomicity.harness import assert_phase_delta, failed_command_allowances, snapshot +from tests.atomicity.harness import ( + assert_phase_delta, + failed_command_allowances, + snapshot, + unchanged_or_operation_owner, +) from tests.integration.scoped_runtime import create_runtime from tests.integration.storage_coverage_helpers import request, value from tests.integration.test_a03_storage_atomicity import setup @@ -463,7 +468,10 @@ def observe(connection: Any, table_name: str, identity: dict[str, Any]) -> None: ) external = snapshot(runtime.ledger.engine) external_changes = sorted( - name for name in before if name not in BOOKKEEPING and external[name] != before[name] + name + for name in before + if name not in BOOKKEEPING + and not unchanged_or_operation_owner(before, external, name, command) ) staged: list[dict[str, Any]] = [] for participant in state["participants"]: @@ -799,14 +807,20 @@ async def test_action_post_write_failure_is_atomic( "observed_resource_uses": actual_reads, "actual_callable": binding, "fault": state, - "changed_tables": sorted(name for name in before if before[name] != after[name]), + "changed_tables": sorted( + name + for name in before + if not unchanged_or_operation_owner(before, after, name, command) + ), } assert len(state["hits"]) == 1, "HOLD_CONTRACT_MISMATCH: expected target hit1" assert not state["violations"], ("HOLD_DEFECT", state["violations"]) assert failed.error is not None, "HOLD_DEFECT: post-write failure reported success" - assert all(before[t] == after[t] for t in before if t not in BOOKKEEPING), ( - "HOLD_DEFECT: failed mutation published domain rows" - ) + assert all( + unchanged_or_operation_owner(before, after, t, command) + for t in before + if t not in BOOKKEEPING + ), "HOLD_DEFECT: failed mutation published domain rows" assert actual_reads == [(ref, "READ") for ref in reads], ( "HOLD_CONTRACT_MISMATCH: source-defined read-set differs", reads, diff --git a/tests/integration/test_m3_execution_effect_atomicity.py b/tests/integration/test_m3_execution_effect_atomicity.py index 5c6d58f..ce94603 100644 --- a/tests/integration/test_m3_execution_effect_atomicity.py +++ b/tests/integration/test_m3_execution_effect_atomicity.py @@ -21,7 +21,12 @@ import pytest from sqlalchemy import func, select -from tests.atomicity.harness import assert_phase_delta, failed_command_allowances, snapshot +from tests.atomicity.harness import ( + assert_phase_delta, + failed_command_allowances, + snapshot, + unchanged_or_operation_owner, +) from tests.integration.scoped_runtime import create_runtime from tests.integration.storage_coverage_helpers import request, value from tests.integration.test_m3_execution_revision_atomicity import ( @@ -330,7 +335,10 @@ def observe(connection: Any, table_name: str, identity: dict[str, Any]) -> None: ) external = snapshot(runtime.ledger.engine) external_changes = sorted( - name for name in before if name not in BOOKKEEPING and external[name] != before[name] + name + for name in before + if name not in BOOKKEEPING + and not unchanged_or_operation_owner(before, external, name, command) ) staged: list[dict[str, Any]] = [] # 2026-10-15 test-side correction: SqliteLedgerTransaction.set_head (sqlite.py:122-141) @@ -545,7 +553,11 @@ async def test_execution_effect_post_write_failure_is_atomic( "expected_writer_order": [list(step) for step in expected_order], "observed_writer_order": [list(step) for step in state["order"]], "fault": state, - "changed_tables": sorted(name for name in before if before[name] != after[name]), + "changed_tables": sorted( + name + for name in before + if not unchanged_or_operation_owner(before, after, name, command) + ), } assert len(state["hits"]) == 1, "HOLD_CONTRACT_MISMATCH: expected target hit1" assert state["order"] == expected_order, ( @@ -555,9 +567,11 @@ async def test_execution_effect_post_write_failure_is_atomic( ) assert not state["violations"], ("HOLD_DEFECT", state["violations"]) assert failed.error is not None, "HOLD_DEFECT: post-write failure reported success" - assert all(before[t] == after[t] for t in before if t not in BOOKKEEPING), ( - "HOLD_DEFECT: failed mutation published domain rows" - ) + assert all( + unchanged_or_operation_owner(before, after, t, command) + for t in before + if t not in BOOKKEEPING + ), "HOLD_DEFECT: failed mutation published domain rows" assert actual_reads == [(ref, "READ") for ref in reads], ( "HOLD_CONTRACT_MISMATCH: source-defined read-set differs", reads, diff --git a/tests/integration/test_m3_execution_revision_atomicity.py b/tests/integration/test_m3_execution_revision_atomicity.py index 113a9f9..ec9e6e3 100644 --- a/tests/integration/test_m3_execution_revision_atomicity.py +++ b/tests/integration/test_m3_execution_revision_atomicity.py @@ -16,7 +16,12 @@ import pytest from sqlalchemy import select -from tests.atomicity.harness import assert_phase_delta, failed_command_allowances, snapshot +from tests.atomicity.harness import ( + assert_phase_delta, + failed_command_allowances, + snapshot, + unchanged_or_operation_owner, +) from tests.integration.scoped_runtime import create_runtime from tests.integration.storage_coverage_helpers import request, value from tests.integration.test_a03_storage_atomicity import setup @@ -261,7 +266,10 @@ def observe(connection: Any, table_name: str, identity: dict[str, Any]) -> None: ) external = snapshot(runtime.ledger.engine) external_changes = sorted( - name for name in before if name not in BOOKKEEPING and external[name] != before[name] + name + for name in before + if name not in BOOKKEEPING + and not unchanged_or_operation_owner(before, external, name, command) ) staged: list[dict[str, Any]] = [] for participant in state["participants"]: @@ -414,14 +422,20 @@ async def test_execution_post_write_failure_is_atomic( "observed_resource_uses": actual_reads, "actual_callable": binding, "fault": state, - "changed_tables": sorted(name for name in before if before[name] != after[name]), + "changed_tables": sorted( + name + for name in before + if not unchanged_or_operation_owner(before, after, name, command) + ), } assert len(state["hits"]) == 1, "HOLD_CONTRACT_MISMATCH: expected target hit1" assert not state["violations"], ("HOLD_DEFECT", state["violations"]) assert failed.error is not None, "HOLD_DEFECT: post-write failure reported success" - assert all(before[t] == after[t] for t in before if t not in BOOKKEEPING), ( - "HOLD_DEFECT: failed mutation published domain rows" - ) + assert all( + unchanged_or_operation_owner(before, after, t, command) + for t in before + if t not in BOOKKEEPING + ), "HOLD_DEFECT: failed mutation published domain rows" assert actual_reads == [(ref, "READ") for ref in reads], ( "HOLD_CONTRACT_MISMATCH: source-defined read-set differs", reads, diff --git a/tests/integration/test_m3_object_mutation_atomicity.py b/tests/integration/test_m3_object_mutation_atomicity.py index 0139acd..e87d4f6 100644 --- a/tests/integration/test_m3_object_mutation_atomicity.py +++ b/tests/integration/test_m3_object_mutation_atomicity.py @@ -8,7 +8,12 @@ import pytest from sqlalchemy import event, select -from tests.atomicity.harness import assert_phase_delta, failed_command_allowances, snapshot +from tests.atomicity.harness import ( + assert_phase_delta, + failed_command_allowances, + snapshot, + unchanged_or_operation_owner, +) from tests.integration.scoped_runtime import create_runtime from tests.integration.storage_coverage_helpers import request, value from tests.integration.test_a03_storage_atomicity import setup @@ -407,7 +412,10 @@ def observe(connection: Any, table_name: str, identity: dict[str, Any]) -> None: ) external = snapshot(runtime.ledger.engine) external_changes = { - t: external[t] for t in before if t not in BOOKKEEPING and external[t] != before[t] + t: external[t] + for t in before + if t not in BOOKKEEPING + and not unchanged_or_operation_owner(before, external, t, command) } staged: list[dict[str, Any]] = [] for participant in state["participants"]: @@ -629,9 +637,11 @@ async def test_mutation_post_write_failure_is_atomic( if kind in {"split", "merge"}: assert state["selected_audit"]["payload"]["applied"] is False assert failed.error is not None, "HOLD_DEFECT: post-write failure reported success" - assert all(before[t] == after[t] for t in before if t not in BOOKKEEPING), ( - "HOLD_DEFECT: failed mutation published domain rows" - ) + assert all( + unchanged_or_operation_owner(before, after, t, command) + for t in before + if t not in BOOKKEEPING + ), "HOLD_DEFECT: failed mutation published domain rows" assert actual_reads == [(ref, "READ") for ref in reads], ( "HOLD_CONTRACT_MISMATCH: source-defined read-set differs", reads, diff --git a/tests/integration/test_m3_outcome_child_atomicity.py b/tests/integration/test_m3_outcome_child_atomicity.py index ffd38e0..b999770 100644 --- a/tests/integration/test_m3_outcome_child_atomicity.py +++ b/tests/integration/test_m3_outcome_child_atomicity.py @@ -27,7 +27,12 @@ import pytest from sqlalchemy import Connection, event, func, select -from tests.atomicity.harness import assert_phase_delta, failed_command_allowances, snapshot +from tests.atomicity.harness import ( + assert_phase_delta, + failed_command_allowances, + snapshot, + unchanged_or_operation_owner, +) from tests.atomicity.outcome_helpers import prepare_outcome from tests.integration.scoped_runtime import create_runtime from tests.integration.storage_coverage_helpers import request, value @@ -252,7 +257,10 @@ def observe(connection: Any, table_name: str, identity: dict[str, Any]) -> None: ) external = snapshot(runtime.ledger.engine) external_changes = sorted( - name for name in before if name not in BOOKKEEPING and external[name] != before[name] + name + for name in before + if name not in BOOKKEEPING + and not unchanged_or_operation_owner(before, external, name, command) ) staged: list[dict[str, Any]] = [] # 2026-10-15 (see 92b): set_head updates the working_heads row of an aggregate in place, @@ -471,7 +479,11 @@ async def test_outcome_child_post_write_failure_is_atomic( "expected_writer_order": [list(step) for step in expected_order], "observed_writer_order": [list(step) for step in state["order"]], "fault": state, - "changed_tables": sorted(name for name in before if before[name] != after[name]), + "changed_tables": sorted( + name + for name in before + if not unchanged_or_operation_owner(before, after, name, command) + ), } assert len(state["hits"]) == 1, "HOLD_CONTRACT_MISMATCH: expected target hit1" assert state["order"] == expected_order, ( @@ -482,9 +494,11 @@ async def test_outcome_child_post_write_failure_is_atomic( assert not state["violations"], ("HOLD_DEFECT", state["violations"]) assert failed.error is not None, "HOLD_DEFECT: post-write failure reported success" # Every non-bookkeeping table, Execution tables included, equals the state before. - assert all(before[t] == after[t] for t in before if t not in BOOKKEEPING), ( - "HOLD_DEFECT: failed mutation published domain rows" - ) + assert all( + unchanged_or_operation_owner(before, after, t, command) + for t in before + if t not in BOOKKEEPING + ), "HOLD_DEFECT: failed mutation published domain rows" assert actual_reads == [(ref, "READ") for ref in reads], ( "HOLD_CONTRACT_MISMATCH: source-defined read-set differs", reads, @@ -530,6 +544,7 @@ def statement( engine = runtime.ledger.engine try: + command = request(method, "94b-followup-normal", payload) before = snapshot(engine) event.listen(engine, "before_cursor_execute", statement) try: @@ -547,12 +562,20 @@ def statement( "INSERT INTO CHECKPOINTS", "UPDATE CHECKPOINTS SET", } + assert unchanged_or_operation_owner(before, after, "operation_owners", command) + owner_count = len(after.get("operation_owners", ())) - len( + before.get("operation_owners", ()) + ) + assert statements.count("INSERT INTO OPERATION_OWNERS") == owner_count + if owner_count: + bus_tables.add("INSERT INTO OPERATION_OWNERS") domain = sorted(item for item in statements if item not in bus_tables) assert domain == ["INSERT INTO OUTCOME_AUDIT"], domain changed = sorted( name for name in before - if name not in BOOKKEEPING | {"checkpoints"} and before[name] != after[name] + if name not in BOOKKEEPING | {"checkpoints"} + and not unchanged_or_operation_owner(before, after, name, command) ) assert changed == ["outcome_audit"], changed assert "digest" in result["candidates"] @@ -592,9 +615,11 @@ def after_sql( after = snapshot(runtime.ledger.engine) assert hits == [True], hits assert failed.error is not None - assert all(before[t] == after[t] for t in before if t not in BOOKKEEPING), ( - "HOLD_DEFECT: failed follow-up published domain rows" - ) + assert all( + unchanged_or_operation_owner(before, after, t, command) + for t in before + if t not in BOOKKEEPING + ), "HOLD_DEFECT: failed follow-up published domain rows" assert_phase_delta( before, after, diff --git a/tests/integration/test_m3_outcome_revision_atomicity.py b/tests/integration/test_m3_outcome_revision_atomicity.py index 809a34e..03c7fbd 100644 --- a/tests/integration/test_m3_outcome_revision_atomicity.py +++ b/tests/integration/test_m3_outcome_revision_atomicity.py @@ -21,7 +21,12 @@ import pytest from sqlalchemy import func, select -from tests.atomicity.harness import assert_phase_delta, failed_command_allowances, snapshot +from tests.atomicity.harness import ( + assert_phase_delta, + failed_command_allowances, + snapshot, + unchanged_or_operation_owner, +) from tests.atomicity.outcome_helpers import prepare_outcome from tests.integration.scoped_runtime import create_runtime from tests.integration.storage_coverage_helpers import request, value @@ -271,7 +276,10 @@ def observe(connection: Any, table_name: str, identity: dict[str, Any]) -> None: ) external = snapshot(runtime.ledger.engine) external_changes = sorted( - name for name in before if name not in BOOKKEEPING and external[name] != before[name] + name + for name in before + if name not in BOOKKEEPING + and not unchanged_or_operation_owner(before, external, name, command) ) staged: list[dict[str, Any]] = [] # 2026-10-15 (see 92b): set_head updates the working_heads row of an aggregate in place, @@ -460,7 +468,11 @@ async def test_outcome_revision_post_write_failure_is_atomic( "expected_writer_order": [list(step) for step in expected_order], "observed_writer_order": [list(step) for step in state["order"]], "fault": state, - "changed_tables": sorted(name for name in before if before[name] != after[name]), + "changed_tables": sorted( + name + for name in before + if not unchanged_or_operation_owner(before, after, name, command) + ), } assert len(state["hits"]) == 1, "HOLD_CONTRACT_MISMATCH: expected target hit1" assert state["order"] == expected_order, ( @@ -471,9 +483,11 @@ async def test_outcome_revision_post_write_failure_is_atomic( assert not state["violations"], ("HOLD_DEFECT", state["violations"]) assert failed.error is not None, "HOLD_DEFECT: post-write failure reported success" # Every non-bookkeeping table, Execution tables included, equals the state before. - assert all(before[t] == after[t] for t in before if t not in BOOKKEEPING), ( - "HOLD_DEFECT: failed mutation published domain rows" - ) + assert all( + unchanged_or_operation_owner(before, after, t, command) + for t in before + if t not in BOOKKEEPING + ), "HOLD_DEFECT: failed mutation published domain rows" assert actual_reads == [(ref, "READ") for ref in reads], ( "HOLD_CONTRACT_MISMATCH: source-defined read-set differs", reads, diff --git a/tests/integration/test_m4_memory_control_multiwrite_observation.py b/tests/integration/test_m4_memory_control_multiwrite_observation.py index 162c8a9..4f2b110 100644 --- a/tests/integration/test_m4_memory_control_multiwrite_observation.py +++ b/tests/integration/test_m4_memory_control_multiwrite_observation.py @@ -27,7 +27,7 @@ import pytest from sqlalchemy import select -from tests.atomicity.harness import snapshot +from tests.atomicity.harness import snapshot, unchanged_or_operation_owner from tests.integration.storage_coverage_helpers import request, value from thoth.adapters.runtime import SystemClock, UuidIdGenerator @@ -104,7 +104,12 @@ def table_changes(before: Any, after: Any) -> dict[str, dict[str, int]]: return result -def domain_changes(changes: dict[str, dict[str, int]]) -> dict[str, dict[str, int]]: +def domain_changes( + changes: dict[str, dict[str, int]], before: Any, after: Any, command: Any +) -> dict[str, dict[str, int]]: + if "operation_owners" in changes: + assert unchanged_or_operation_owner(before, after, "operation_owners", command) + changes = {name: delta for name, delta in changes.items() if name != "operation_owners"} return {name: delta for name, delta in changes.items() if name not in BOOKKEEPING} @@ -271,9 +276,15 @@ async def test_control_route_success_writes_the_two_rows_in_source_order( await call(runtime, method, f"step0-{route}-ok", payload) added = rows(runtime)[len(before_rows) :] assert [(item["type"]) for item in added] == [t for t, _ in EXPECTED_ORDER[route]] - assert set(domain_changes(table_changes(before, snapshot(runtime.ledger.engine)))) == { - "control_records" - } + after = snapshot(runtime.ledger.engine) + assert set( + domain_changes( + table_changes(before, after), + before, + after, + request(method, f"step0-{route}-ok", payload), + ) + ) == {"control_records"} save( f"{route}-success", { @@ -306,13 +317,16 @@ async def test_second_control_write_failure_leaves_the_first_row_and_a_failed_op assert failed.error is not None assert [item["type"] for item in hits] == [t for t, _ in EXPECTED_ORDER[route]] rows_after = rows(runtime) - changes = table_changes(before, snapshot(runtime.ledger.engine)) + fault_after = snapshot(runtime.ledger.engine) + changes = table_changes(before, fault_after) added = rows_after[len(rows_before) :] expected_added = 1 if mode == "before" else 2 assert [item["type"] for item in added] == [t for t, _ in EXPECTED_ORDER[route]][ :expected_added ] - assert set(domain_changes(changes)) == {"control_records"} + assert set(domain_changes(changes, before, fault_after, request(method, key, payload))) == { + "control_records" + } reads_after_fault = await later_reads(runtime, route, candidate_id, "fault") runtime.close() @@ -350,7 +364,9 @@ async def test_second_control_write_failure_leaves_the_first_row_and_a_failed_op "hits_in_order": hits, "error": failed.error.model_dump(mode="json"), "rows_added_by_failed_request": added, - "domain_tables_changed_by_failed_request": domain_changes(changes), + "domain_tables_changed_by_failed_request": domain_changes( + changes, before, fault_after, request(method, key, payload) + ), "later_reads_before": reads_before, "later_reads_after_fault": reads_after_fault, "reopened_rows_equal": True, diff --git a/tests/integration/test_operation_owner_atomicity.py b/tests/integration/test_operation_owner_atomicity.py new file mode 100644 index 0000000..51f45f8 --- /dev/null +++ b/tests/integration/test_operation_owner_atomicity.py @@ -0,0 +1,98 @@ +"""Two sealers and injected durable-event failure use one terminal transaction.""" + +import asyncio +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from threading import Barrier + +import pytest +from tests.integration.scoped_runtime import create_runtime +from tests.integration.test_operation_owner_recovery import abandoned, terminal_counts + +from thoth.adapters.storage.events import SqliteEventStore +from thoth.adapters.storage.operations import SqliteOperationStore +from thoth.adapters.storage.transaction import _AMBIENT # pyright: ignore[reportPrivateUsage] +from thoth.adapters.worker_identity import LocalWorkerIdentity +from thoth.domain.event import EventRecord +from thoth.protocol.jsonrpc import JsonRpcRequest + + +async def test_two_sealers_publish_one_failure_and_one_measurement( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + first, command, identifier = await abandoned(tmp_path) + second = create_runtime(tmp_path / "workspace") + original = SqliteOperationStore(first.ledger.engine).read(identifier) + assert original is not None and original.owner is not None + stale_worker = original.owner.worker_id + barrier = Barrier(2) + alive = LocalWorkerIdentity.is_alive + observations: list[bool | None] = [] + measurements: list[str] = [] + + def observed(self: LocalWorkerIdentity, pid: int, marker: str, worker: str) -> bool | None: + assert _AMBIENT.get() is None + result = alive(self, pid, marker, worker) + if worker == stale_worker: + observations.append(result) + barrier.wait(timeout=10) + return result + + def measured(request: JsonRpcRequest, project: str, outcome: str) -> None: + del request, project + assert _AMBIENT.get() is None + measurements.append(outcome) + + monkeypatch.setattr(LocalWorkerIdentity, "is_alive", observed) + monkeypatch.setattr(first.bus, "_measure", measured) + monkeypatch.setattr(second.bus, "_measure", measured) + try: + with ThreadPoolExecutor(max_workers=2) as executor: + a = executor.submit(asyncio.run, first.bus.dispatch(command)) + b = executor.submit(asyncio.run, second.bus.dispatch(command)) + replies = [a.result(timeout=15), b.result(timeout=15)] + assert observations == [False, False] + assert replies[0] == replies[1] and replies[0].error is not None + assert measurements == ["FAILED"] and terminal_counts(first, identifier) == (1, 1) + stored = SqliteOperationStore(first.ledger.engine).read(identifier) + assert stored is not None and stored.error == replies[0].error.model_dump(mode="json") + assert stored.resource_uses is not None + finally: + second.close() + first.close() + + +async def test_event_insert_failure_rolls_back_failure_binding_and_event( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + runtime, command, identifier = await abandoned(tmp_path) + store = SqliteOperationStore(runtime.ledger.engine) + events = SqliteEventStore(runtime.ledger.engine) + before, before_events = store.read(identifier), events.list_events(identifier) + measurements: list[str] = [] + insert = SqliteEventStore.insert_event + + def injected(self: SqliteEventStore, event: EventRecord) -> None: + insert(self, event) + if event.operation_id == identifier and event.event_type == "operation.failed": + assert _AMBIENT.get() is not None + raise RuntimeError("injected after durable failure event") + + def measured(request: JsonRpcRequest, project: str, outcome: str) -> None: + del request, project + measurements.append(outcome) + + monkeypatch.setattr(runtime.bus, "_measure", measured) + try: + with monkeypatch.context() as patch: + patch.setattr(SqliteEventStore, "insert_event", injected) + with pytest.raises(RuntimeError, match="injected after durable failure event"): + await runtime.bus.dispatch(command) + assert store.read(identifier) == before + assert events.list_events(identifier) == before_events + assert terminal_counts(runtime, identifier) == (0, 0) and measurements == [] + reply = await runtime.bus.dispatch(command) + assert reply.error is not None and reply.error.data["reason_code"] == "OPERATION_OWNER_GONE" + assert terminal_counts(runtime, identifier) == (1, 1) and measurements == ["FAILED"] + finally: + runtime.close() diff --git a/tests/integration/test_operation_owner_completion_race.py b/tests/integration/test_operation_owner_completion_race.py new file mode 100644 index 0000000..8a2d9c0 --- /dev/null +++ b/tests/integration/test_operation_owner_completion_race.py @@ -0,0 +1,177 @@ +"""A real completion must replay the sealing winner, never announce a false success.""" + +import asyncio +import json +from collections.abc import Callable, Generator +from concurrent.futures import ThreadPoolExecutor +from contextlib import AbstractContextManager, contextmanager +from datetime import datetime +from pathlib import Path +from threading import Event +from typing import cast + +import pytest +from pydantic import JsonValue +from tests.integration.scoped_runtime import create_runtime +from tests.integration.storage_coverage_helpers import prepare_project, request + +from thoth.adapters.storage.events import SqliteEventStore +from thoth.adapters.storage.operations import SqliteOperationStore +from thoth.domain.enums import OperationState +from thoth.domain.operation import OperationRecord +from thoth.protocol.bus import DispatchTicket +from thoth.protocol.jsonrpc import RpcApplicationError, RpcErrorCode + + +@pytest.mark.parametrize( + ("outcome", "winner"), + [(name, "seal") for name in ["success", "rpc-error", "permission", "validation", "internal"]] + + [("success", "completion")], +) +async def test_sealing_winner_is_replayed_by_original_completion( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + record_property: Callable[[str, object], None], + outcome: str, + winner: str, +) -> None: + workspace = tmp_path / "workspace" + original, project = await prepare_project(workspace) + command = request( + "project/metadata/update", + "complete-race", + { + "project_id": project, + "expected_revision": 0, + "name": "Saved once", + }, + ) + if outcome != "success": + + async def rejected(values: dict[str, JsonValue]) -> dict[str, JsonValue]: + del values + if outcome == "rpc-error": + raise RpcApplicationError(RpcErrorCode.DOMAIN_REJECTED, "synthetic rejection") + if outcome == "permission": + raise PermissionError("SYNTHETIC_AUTHORITY_REJECTED") + if outcome == "validation": + OperationRecord.model_validate({}) + raise RuntimeError("synthetic internal failure") + + original.bus._registry.decorate("project/metadata/update", lambda handler: rejected) # pyright: ignore[reportPrivateUsage] + ticket = original.bus.claim(command) + assert isinstance(ticket, DispatchTicket) and ticket.operation.owner is not None + identifier = ticket.operation.operation_id + before_complete, resume_complete = Event(), Event() + complete = SqliteOperationStore.complete + fail = SqliteOperationStore.fail + + def paused_complete( + self: SqliteOperationStore, + operation_id: str, + result: dict[str, JsonValue], + *, + completed_at: datetime, + ) -> OperationRecord: + if operation_id == identifier: + before_complete.set() + assert resume_complete.wait(timeout=10), "completion gate was not released" + return complete(self, operation_id, result, completed_at=completed_at) + + monkeypatch.setattr(SqliteOperationStore, "complete", paused_complete) + + def paused_fail( + self: SqliteOperationStore, + operation_id: str, + error: dict[str, JsonValue], + *, + completed_at: datetime, + ) -> OperationRecord: + if operation_id == identifier and self._engine is original.ledger.engine: # pyright: ignore[reportPrivateUsage] + before_complete.set() + assert resume_complete.wait(timeout=10), "failure gate was not released" + return fail(self, operation_id, error, completed_at=completed_at) + + monkeypatch.setattr(SqliteOperationStore, "fail", paused_fail) + peer = None + closed = False + pin_reached, allow_pin = Event(), Event() + with ThreadPoolExecutor(max_workers=2) as executor: + future = executor.submit(asyncio.run, original.bus.execute(ticket)) + try: + assert before_complete.wait(timeout=10), "original handler never reached completion" + # close() retires this runtime's worker while its general completion is waiting. + original.close() + closed = True + peer = create_runtime(workspace) + if winner == "completion": + policy = vars(peer.bus)["_owner"] + original_pin = cast( + Callable[[], AbstractContextManager[bool]], vars(policy)["_pin_workspace"] + ) + + @contextmanager + def gated_pin() -> Generator[bool]: + with original_pin() as held: + pin_reached.set() + assert allow_pin.wait(timeout=10) + yield held + + monkeypatch.setattr(policy, "_pin_workspace", gated_pin) + sealing = executor.submit(asyncio.run, peer.bus.dispatch(command)) + assert pin_reached.wait(timeout=10) + resume_complete.set() + completed = future.result(timeout=10) + allow_pin.set() + sealed = sealing.result(timeout=10) + else: + sealed = await peer.bus.dispatch(command) + assert sealed.error is not None + assert sealed.error.data["reason_code"] == "OPERATION_OWNER_GONE" + resume_complete.set() + completed = future.result(timeout=10) + stored = SqliteOperationStore(peer.ledger.engine).read(identifier) + events = SqliteEventStore(peer.ledger.engine).list_events(identifier) + observed = { + "original_outcome": outcome, + "winner": winner, + "stored_state": None if stored is None else stored.state.value, + "sealing_response_error": sealed.error is not None, + "original_response_error": completed.error is not None, + "original_result": completed.result, + "events": [item.event_type for item in events], + } + record_property("race_observation", json.dumps(observed)) + print(json.dumps(observed)) + assert completed == sealed and stored is not None + with peer.ledger.engine.connect() as connection: + binding = json.loads( + connection.exec_driver_sql( + "SELECT content_json FROM operation_resource_bindings WHERE operation_id=?", + (identifier,), + ).scalar_one() + ) + if winner == "completion": + assert completed.error is None and stored.state is OperationState.SUCCEEDED + assert binding["output_kind"] == "RESULT" + assert [item.event_type for item in events] == [ + "operation.started", + "project/metadata/updated", + "operation.succeeded", + ] + else: + assert sealed.error is not None + assert stored.error == sealed.error.model_dump(mode="json") + assert binding["output_kind"] == "ERROR" + assert [item.event_type for item in events] == [ + "operation.started", + "operation.failed", + ] + finally: + allow_pin.set() + resume_complete.set() + future.result(timeout=10) + if peer is not None: + peer.close() + if not closed: + original.close() diff --git a/tests/integration/test_operation_owner_migration.py b/tests/integration/test_operation_owner_migration.py new file mode 100644 index 0000000..6e38225 --- /dev/null +++ b/tests/integration/test_operation_owner_migration.py @@ -0,0 +1,118 @@ +"""Old RUNNING operations stay ownerless; new DDL is one additive table.""" + +import hashlib +import json +import sqlite3 +from contextlib import closing +from pathlib import Path + +import pytest +from alembic import command +from alembic.config import Config +from sqlalchemy import create_engine, insert +from tests.integration.scoped_runtime import create_runtime +from tests.integration.test_operation_owner_storage import candidate, owner + +from thoth.adapters.storage.operations import SqliteOperationStore +from thoth.adapters.storage.schema import idempotency_keys, operations +from thoth.domain.enums import OperationState +from thoth.protocol.registry import PUBLIC_METHODS + + +def test_upgrade_keeps_old_rows_and_reopen_roundtrip( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + database = tmp_path / "old.sqlite3" + url = "sqlite+pysqlite:///" + database.as_posix() + monkeypatch.setenv("THOTH_ALEMBIC_URL", url) + config = Config("alembic.ini") + command.upgrade(config, "3b06e4f87d92") + original = candidate() + engine = create_engine(url) + with engine.begin() as connection: + connection.execute( + insert(operations).values( + operation_id=original.operation_id, + project_id=original.project_id, + method=original.method, + idempotency_key=original.idempotency_key, + scope_digest=original.scope_digest, + state="RUNNING", + created_at=original.created_at.isoformat(), + epoch=0, + owner_data_scopes_json="[]", + ) + ) + connection.execute( + insert(idempotency_keys).values( + project_id=original.project_id, + method=original.method, + idempotency_key=original.idempotency_key, + scope_digest=original.scope_digest, + operation_id=original.operation_id, + ) + ) + engine.dispose() + with closing(sqlite3.connect(database)) as connection: + before = connection.execute("SELECT * FROM operations").fetchall() + columns = connection.execute("PRAGMA table_info(operations)").fetchall() + command.upgrade(config, "head") + command.upgrade(config, "head") + engine = create_engine(url) + try: + store = SqliteOperationStore(engine) + stored = store.read(original.operation_id) + assert stored is not None and stored == original and stored.owner is None + assert store.claim(original.model_copy(update={"owner": owner()})) == (original, True) + replayed = store.read(original.operation_id) + assert replayed is not None and replayed.state is OperationState.RUNNING + with engine.connect() as connection: + assert ( + connection.exec_driver_sql("SELECT count(*) FROM operation_owners").scalar_one() + == 0 + ) + finally: + engine.dispose() + command.downgrade(config, "3b06e4f87d92") + command.upgrade(config, "head") + with closing(sqlite3.connect(database)) as connection: + assert connection.execute("SELECT * FROM operations").fetchall() == before + assert connection.execute("PRAGMA table_info(operations)").fetchall() == columns + assert connection.execute("SELECT count(*) FROM operation_owners").fetchone() == (0,) + + +def test_sealable_fixture_is_the_approved_67_registered_methods(tmp_path: Path) -> None: + fixture = Path(__file__).parents[1] / "fixtures/sealable_methods.json" + record = json.loads(fixture.read_text(encoding="utf-8")) + methods = record["methods"] + assert methods == sorted(set(methods)) and len(methods) == 67 + assert set(methods) <= set(PUBLIC_METHODS) + runtime = create_runtime(tmp_path / "workspace") + try: + for method in methods: + assert callable(runtime.bus._registry.resolve(method)) # pyright: ignore[reportPrivateUsage] + finally: + runtime.close() + assert not set(methods).intersection( + { + "thread/start", + "thread/input", + "thread/steer", + "thread/pause", + "thread/resume", + "thread/stop", + "revision/restore", + "revision/restore/apply", + "revision/changeSet/commit", + "project/archive", + "project/delete", + "project/restore", + "workspace/setup/update", + "improvement/evaluation/run", + "action/preflight/read", + } + ) + digest = hashlib.sha256(("\n".join(methods) + "\n").encode()).hexdigest() + assert digest == "83993c93cbe89314247caa791d292cbc806a73d0c955d424d8fcf48ebeb18da4" + assert record["evidence"]["approved_methods_sha256"] == digest + assert record["evidence"]["path"] == "outputs/refactor-vs2-20261019/sealable-evidence.json" diff --git a/tests/integration/test_operation_owner_recovery.py b/tests/integration/test_operation_owner_recovery.py new file mode 100644 index 0000000..a84a93a --- /dev/null +++ b/tests/integration/test_operation_owner_recovery.py @@ -0,0 +1,169 @@ +"""Normal LOCAL entry: dead workers seal, unknown/live/old rows remain untouched.""" + +from pathlib import Path +from typing import Any + +import pytest +from tests.integration.scoped_runtime import create_runtime +from tests.integration.storage_coverage_helpers import prepare_project, request, value + +from thoth.adapters.storage.events import SqliteEventStore +from thoth.adapters.storage.operations import SqliteOperationStore +from thoth.adapters.storage.transaction import _AMBIENT # pyright: ignore[reportPrivateUsage] +from thoth.apps.runtime import AppRuntime +from thoth.domain.enums import OperationState +from thoth.domain.resource_scope import ResourceScopeError +from thoth.protocol.bus import DispatchTicket +from thoth.protocol.jsonrpc import JsonRpcRequest + + +async def abandoned(tmp_path: Path) -> tuple[AppRuntime, JsonRpcRequest, str]: + workspace = tmp_path / "workspace" + first, project = await prepare_project(workspace) + command = request( + "project/metadata/update", + "abandoned", + { + "project_id": project, + "expected_revision": 0, + "name": "Never executed", + }, + ) + claimed = first.bus.claim(command) + assert isinstance(claimed, DispatchTicket) and claimed.operation.owner is not None + first.close() + return create_runtime(workspace), command, claimed.operation.operation_id + + +def terminal_counts(runtime: AppRuntime, identifier: str) -> tuple[int, int]: + with runtime.ledger.engine.connect() as connection: + events = connection.exec_driver_sql( + "SELECT count(*) FROM events WHERE operation_id=? AND event_type='operation.failed'", + (identifier,), + ).scalar_one() + bindings = connection.exec_driver_sql( + "SELECT count(*) FROM operation_resource_bindings WHERE operation_id=?", + (identifier,), + ).scalar_one() + return events, bindings + + +async def test_closed_runtime_same_pid_seals_once_then_replays( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + runtime, command, identifier = await abandoned(tmp_path) + measurements: list[str] = [] + + def measured(request: JsonRpcRequest, project: str, outcome: str) -> None: + del request, project + assert _AMBIENT.get() is None + stored = SqliteOperationStore(runtime.ledger.engine).read(identifier) + assert stored is not None and stored.state is OperationState.FAILED + assert terminal_counts(runtime, identifier) == (1, 1) + measurements.append(outcome) + + monkeypatch.setattr(runtime.bus, "_measure", measured) + try: + first = await runtime.bus.dispatch(command) + second = await runtime.bus.dispatch(command) + assert first == second and first.error is not None + assert first.error.data["reason_code"] == "OPERATION_OWNER_GONE" + assert first.error.data["remote_observation"] == "UNKNOWN" + assert first.error.data["new_request_required"] is True + assert measurements == ["FAILED"] and terminal_counts(runtime, identifier) == (1, 1) + project = value( + await runtime.bus.query( + request( + "project/read", + "read", + { + "project_id": command.params.input["project_id"], + }, + ) + ) + ) + assert project["name"] == "Storage coverage" and project["revision"] == 0 + finally: + runtime.close() + + +@pytest.mark.parametrize("case", ["owner-missing", "host-mismatch", "unknown-liveness"]) +async def test_uncertain_or_old_owner_keeps_running( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, case: str +) -> None: + runtime, command, identifier = await abandoned(tmp_path) + try: + if case == "unknown-liveness": + from thoth.adapters.worker_identity import LocalWorkerIdentity + + def unknown(*args: object) -> None: + return None + + monkeypatch.setattr(LocalWorkerIdentity, "is_alive", unknown) + else: + with runtime.ledger.engine.begin() as connection: + if case == "owner-missing": + connection.exec_driver_sql( + "DELETE FROM operation_owners WHERE operation_id=?", (identifier,) + ) + else: + connection.exec_driver_sql( + "UPDATE operation_owners SET host_token=? WHERE operation_id=?", + ("f" * 64, identifier), + ) + result = await runtime.bus.dispatch(command) + assert result.error is None and result.result is not None + assert result.result["state"] == "RUNNING" and terminal_counts(runtime, identifier) == ( + 0, + 0, + ) + finally: + runtime.close() + + +async def test_live_owner_keeps_running(tmp_path: Path) -> None: + runtime, project = await prepare_project(tmp_path / "workspace") + command = request( + "project/metadata/update", + "live", + {"project_id": project, "expected_revision": 0, "name": "Later"}, + ) + try: + claimed = runtime.bus.claim(command) + assert isinstance(claimed, DispatchTicket) + response = await runtime.bus.dispatch(command) + assert response.error is None and response.result is not None + assert response.result["state"] == "RUNNING" + assert terminal_counts(runtime, claimed.operation.operation_id) == (0, 0) + assert value(await runtime.bus.execute(claimed))["revision"] == 1 + finally: + runtime.close() + + +async def test_authorization_refusal_precedes_any_sealing_write( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + from thoth.application.services.resource_scope_service import ResourceScopeService + + runtime, command, identifier = await abandoned(tmp_path) + store = SqliteOperationStore(runtime.ledger.engine) + before = store.read(identifier) + before_events = SqliteEventStore(runtime.ledger.engine).list_events(identifier) + + def denied(self: Any, operation: Any) -> None: + del self + assert operation.operation_id == identifier + raise ResourceScopeError("RESOURCE_SCOPE_READ_DENIED") + + monkeypatch.setattr(ResourceScopeService, "require_operation", denied) + try: + response = await runtime.bus.dispatch(command) + assert ( + response.error is not None + and response.error.data["reason_code"] == "RESOURCE_SCOPE_READ_DENIED" + ) + assert store.read(identifier) == before + assert SqliteEventStore(runtime.ledger.engine).list_events(identifier) == before_events + assert terminal_counts(runtime, identifier) == (0, 0) + finally: + runtime.close() diff --git a/tests/integration/test_operation_owner_storage.py b/tests/integration/test_operation_owner_storage.py new file mode 100644 index 0000000..f4cee7f --- /dev/null +++ b/tests/integration/test_operation_owner_storage.py @@ -0,0 +1,231 @@ +"""Internal ownership is atomic with claim and absent from every public view.""" + +from __future__ import annotations + +from collections.abc import Iterator +from concurrent.futures import ThreadPoolExecutor +from datetime import UTC, datetime +from pathlib import Path +from threading import Barrier +from typing import Any + +import httpx +import pytest +from sqlalchemy import Connection, create_engine, event + +from thoth.adapters.http.app import create_app +from thoth.adapters.runtime import SystemClock, UuidIdGenerator +from thoth.adapters.storage import SqliteLedger, migrate_sqlite_database +from thoth.adapters.storage.events import SqliteEventStore +from thoth.adapters.storage.operations import IdempotencyConflict, SqliteOperationStore +from thoth.application.commands.operations import OperationCommandHandlers +from thoth.application.services.operation_journal import OperationJournal +from thoth.domain.enums import OperationState +from thoth.domain.operation import OperationOwnerRecord, OperationRecord +from thoth.protocol.bus import CommandBus +from thoth.protocol.registry import MethodRegistry + + +def owner(token: str | None = "b" * 64) -> OperationOwnerRecord: + return OperationOwnerRecord( + worker_id="worker:synthetic", + process_id=987654, + process_marker="synthetic-marker", + host_token=token, + ) + + +def candidate(ownership: OperationOwnerRecord | None = None) -> OperationRecord: + return OperationRecord( + operation_id="operation:owner", + project_id="project:owner", + method="project/create", + idempotency_key="owner-key", + scope_digest="a" * 64, + state=OperationState.RUNNING, + created_at=datetime(2026, 10, 19, tzinfo=UTC), + owner=ownership, + ) + + +@pytest.fixture +def ledger(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[SqliteLedger]: + monkeypatch.setenv("THOTH_DEPLOYMENT_MODE", "LOCAL") + monkeypatch.delenv("THOTH_ALEMBIC_URL", raising=False) + database = tmp_path / "test.sqlite3" + migrate_sqlite_database(database) + instance = SqliteLedger(database) + instance.initialize() + try: + yield instance + finally: + instance.close() + + +def counts(ledger: SqliteLedger) -> dict[str, int]: + with ledger.engine.connect() as connection: + return { + name: connection.exec_driver_sql(f"SELECT count(*) FROM {name}").scalar_one() + for name in ("operations", "idempotency_keys", "operation_owners", "events") + } + + +def test_owner_survives_read_terminal_and_duplicate_claim(ledger: SqliteLedger) -> None: + store = SqliteOperationStore(ledger.engine) + original = candidate(owner()) + journal = OperationJournal(SqliteEventStore(ledger.engine), SystemClock(), UuidIdGenerator()) + stored, replayed = store.claim( + original, started_event=lambda: journal.prepare_started(original) + ) + assert stored == original and not replayed + before = counts(ledger) + assert list(before.values()) == [1, 1, 1, 1] + replacement = original.model_copy(update={"owner": owner("c" * 64)}) + replay, replayed = store.claim(replacement) + assert replayed and replay.owner == original.owner and counts(ledger) == before + with pytest.raises(IdempotencyConflict) as conflict: + store.claim(replacement.model_copy(update={"scope_digest": "d" * 64})) + assert conflict.value.existing.owner == original.owner and counts(ledger) == before + assert SqliteOperationStore(ledger.engine).read(original.operation_id) == original + completed = store.complete( + original.operation_id, {"done": True}, completed_at=original.created_at + ) + assert completed.owner == original.owner + assert store.read(original.operation_id) == completed + assert store.list_by_project(original.project_id)[0].owner is None + + +def test_competing_claims_keep_one_owner_and_reopen_it(ledger: SqliteLedger) -> None: + barrier = Barrier(2) + calls: list[str] = [] + + def claim(token: str) -> tuple[OperationRecord, bool]: + engine = create_engine(ledger.engine.url) + original = candidate(owner(token * 64)).model_copy( + update={"operation_id": "operation:" + token} + ) + journal = OperationJournal(SqliteEventStore(engine), SystemClock(), UuidIdGenerator()) + + def started(): + calls.append(token) + return journal.prepare_started(original) + + try: + barrier.wait(timeout=5) + return SqliteOperationStore(engine).claim(original, started_event=started) + finally: + engine.dispose() + + with ThreadPoolExecutor(max_workers=2) as pool: + results = list(pool.map(claim, ("b", "c"))) + assert sorted(replayed for _, replayed in results) == [False, True] + assert results[0][0] == results[1][0] and len(calls) == 1 + assert set(counts(ledger).values()) == {1} + reopened = create_engine(ledger.engine.url) + try: + assert SqliteOperationStore(reopened).read(results[0][0].operation_id) == results[0][0] + finally: + reopened.dispose() + + +@pytest.mark.parametrize("ownership", [None, owner(None)]) +def test_unknown_host_never_records_an_owner( + ledger: SqliteLedger, ownership: OperationOwnerRecord | None +) -> None: + store = SqliteOperationStore(ledger.engine) + stored, replayed = store.claim(candidate(ownership)) + assert not replayed and stored.owner is None + assert counts(ledger)["operation_owners"] == 0 + assert store.read(stored.operation_id) == stored + + +@pytest.mark.parametrize("fault", ["owner_insert", "event_factory", "event_insert"]) +def test_owner_or_started_failure_rolls_back_the_entire_claim( + ledger: SqliteLedger, fault: str +) -> None: + store = SqliteOperationStore(ledger.engine) + original = candidate(owner()) + journal = OperationJournal(SqliteEventStore(ledger.engine), SystemClock(), UuidIdGenerator()) + calls: list[str] = [] + + def inserted( + connection: Connection, + cursor: Any, + statement: str, + parameters: Any, + context: Any, + many: bool, + ) -> None: + del connection, cursor, parameters, context, many + if statement.startswith("INSERT INTO "): + table = statement.split()[2] + calls.append(table) + if (fault == "owner_insert" and table == "operation_owners") or ( + fault == "event_insert" and table == "events" + ): + raise RuntimeError("injected owner boundary failure") + + def started(): + calls.append("started_factory") + if fault == "event_factory": + raise RuntimeError("injected owner boundary failure") + return journal.prepare_started(original) + + event.listen(ledger.engine, "after_cursor_execute", inserted) + try: + with pytest.raises(RuntimeError, match="injected owner boundary failure"): + store.claim(original, started_event=started) + assert calls[:3] == ["operations", "idempotency_keys", "operation_owners"] + assert ("started_factory" in calls) is (fault != "owner_insert") + assert set(counts(ledger).values()) == {0} + finally: + event.remove(ledger.engine, "after_cursor_execute", inserted) + assert ( + store.claim(original, started_event=lambda: journal.prepare_started(original))[1] is False + ) + assert set(counts(ledger).values()) == {1} + + +async def test_get_rpc_and_events_keep_the_previous_public_payload(ledger: SqliteLedger) -> None: + store = SqliteOperationStore(ledger.engine) + original = candidate(owner()) + clock, ids = SystemClock(), UuidIdGenerator() + events = SqliteEventStore(ledger.engine) + journal = OperationJournal(events, clock, ids) + assert journal.prepare_started(original).payload == {"method": original.method} + store.claim(original, started_event=lambda: journal.prepare_started(original)) + registry = MethodRegistry() + registry.register("operation/read", OperationCommandHandlers(store, clock=clock).read) + bus = CommandBus(registry, store, clock, ids) + application = create_app(bus) + baseline = candidate().model_dump(mode="json") + assert original.model_dump(mode="json") == baseline + assert original.model_dump_json() == candidate().model_dump_json() + assert "owner" not in OperationRecord.model_json_schema(mode="serialization")["properties"] + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=application), base_url="http://127.0.0.1:8765" + ) as client: + response = await client.get( + f"/operations/{original.operation_id}", params={"project_id": original.project_id} + ) + assert response.status_code == 200 and response.json() == baseline + response = await client.post( + "/rpc", + json={ + "id": "read", + "method": "operation/read", + "params": { + "_meta": {"idempotencyKey": "read"}, + "input": { + "project_id": original.project_id, + "operation_id": original.operation_id, + }, + }, + }, + ) + assert response.status_code == 200 + assert response.json()["result"]["value"] == baseline + wire = " ".join(item.model_dump_json() for item in events.list_events(original.operation_id)) + for secret in ("synthetic-marker", "worker:synthetic", "987654", "b" * 64): + assert secret not in wire + assert events.list_events(original.operation_id)[0].payload == {"method": original.method} diff --git a/tests/integration/test_resource_scope_transition_boundaries.py b/tests/integration/test_resource_scope_transition_boundaries.py index 47d12f2..5804fbf 100644 --- a/tests/integration/test_resource_scope_transition_boundaries.py +++ b/tests/integration/test_resource_scope_transition_boundaries.py @@ -12,6 +12,7 @@ import pytest from sqlalchemy import Engine, create_engine, delete, select +from tests.atomicity.harness import AllowedRow, assert_phase_delta, operation_owner_allowances from tests.integration.resource_scope_helpers import ScopeHarness, denial, scope_harness, value from tests.integration.test_a02_autonomous_acquisition import StaticModelResolver from tests.integration.test_m5_revision_control_atomicity import NoModel @@ -41,11 +42,15 @@ "checkpoints", "operation_resource_bindings", } -Snapshot = dict[str, tuple[str, ...]] + + +class Snapshot(dict[str, tuple[str, ...]]): + # Original operation rows identify fresh requests; they are not domain projection rows. + operation_rows: tuple[str, ...] = () def snapshot(engine: Engine) -> Snapshot: - result: Snapshot = {} + result = Snapshot() with engine.connect() as connection: names = ( connection.exec_driver_sql( @@ -57,6 +62,15 @@ def snapshot(engine: Engine) -> Snapshot: for name in names: name = str(name) if name in BOOKKEEPING: + if name == "operations": + result.operation_rows = tuple( + sorted( + json.dumps(dict(row), sort_keys=True, default=str) + for row in connection.exec_driver_sql( + "SELECT * FROM operations" + ).mappings() + ) + ) continue quoted = '"' + name.replace('"', '""') + '"' rows = connection.exec_driver_sql("SELECT * FROM " + quoted).mappings() @@ -72,10 +86,42 @@ def snapshot(engine: Engine) -> Snapshot: return result -def reopened(h: ScopeHarness, expected: Snapshot) -> None: +def owner_adjusted( + before: Snapshot, after: Snapshot, project: str, method: str, keys: tuple[str, ...] +) -> dict[str, tuple[str, ...]]: + """Validate one added owner per explicit request; retain every domain row verbatim.""" + rules: list[AllowedRow] = [] + for key in keys: + rows = [ + json.loads(row) + for row in after.operation_rows + if json.loads(row)["project_id"] == project + and json.loads(row)["method"] == method + and json.loads(row)["idempotency_key"] == key + ] + assert len(rows) == 1 + rules.append(AllowedRow("operations", rows[0], "added", {})) + allowances = operation_owner_allowances( + {**before, "operations": before.operation_rows}, + {**after, "operations": after.operation_rows}, + tuple(rules), + ) + assert_phase_delta( + {"operation_owners": before.get("operation_owners", ())}, + {"operation_owners": after.get("operation_owners", ())}, + allowances, + ) + return {**after, "operation_owners": before.get("operation_owners", ())} + + +def reopened( + h: ScopeHarness, expected: Snapshot, *, method: str = "", keys: tuple[str, ...] = () +) -> None: engine = create_engine(h.runtime.ledger.engine.url) try: - assert snapshot(engine) == expected + observed = snapshot(engine) + compared = owner_adjusted(expected, observed, h.project, method, keys) if keys else observed + assert compared == expected finally: engine.dispose() @@ -242,7 +288,17 @@ def failed_bookkeeping(h: ScopeHarness, method: str, key: str, error: dict[str, assert json.loads(row["error_json"]) == error -def assert_scope_delta(before: Snapshot, after: Snapshot, probe: Probe, *, assigned: bool) -> None: +def assert_scope_delta( + before: Snapshot, + after: Snapshot, + probe: Probe, + *, + assigned: bool, + project: str, + method: str, + keys: tuple[str, ...], +) -> None: + after = Snapshot(owner_adjusted(before, after, project, method, keys)) assert len(probe.writes) == len(probe.receipts) == 1 record, receipt = probe.writes[0], probe.receipts[0] for name in before: @@ -338,17 +394,32 @@ async def test_scope_transition_post_append_rollback_and_retries( assert error["code"] == -32603 and probe.hits == 1 and len(probe.writes) == 1 after = snapshot(h.runtime.ledger.engine) save(operation + "-fault", before, after, probe, error_code=error["code"]) - assert after == before, "HOLD_PARTIAL_PUBLICATION: post-append transition" - reopened(h, before) + assert owner_adjusted(before, after, h.project, method, ("fault",)) == before, ( + "HOLD_PARTIAL_PUBLICATION: post-append transition" + ) + reopened(h, before, method=method, keys=("fault",)) replay = await h.call(actor, method, "fault", body) assert replay.json()["error"] == error and probe.hits == 1 and len(probe.writes) == 1 - assert snapshot(h.runtime.ledger.engine) == before + assert ( + owner_adjusted( + before, snapshot(h.runtime.ledger.engine), h.project, method, ("fault",) + ) + == before + ) failed_bookkeeping(h, method, "fault", error) with monkeypatch.context() as patch: retry = install(patch, h, scopes, resource, event, mode="observe") response = value(await h.call(actor, method, "new-key", body))["scope"] after = snapshot(h.runtime.ledger.engine) - assert_scope_delta(before, after, retry, assigned=operation == "assign") + assert_scope_delta( + before, + after, + retry, + assigned=operation == "assign", + project=h.project, + method=method, + keys=("fault", "new-key"), + ) assert response == retry.writes[0].model_dump(mode="json") if operation != "assign": assert all(g["state"] == "REVOKED" for g in response["grants"]) @@ -407,8 +478,10 @@ async def test_http_assign_expiry_after_append_rolls_back_denied_publication( original_actor_check=True, session_active_before_clock_advance=True, ) - assert after == before, "HOLD_PARTIAL_PUBLICATION: actual post-append expiry denial" - reopened(h, before) + assert ( + owner_adjusted(before, after, h.project, method, ("expired-after-write",)) == before + ), "HOLD_PARTIAL_PUBLICATION: actual post-append expiry denial" + reopened(h, before, method=method, keys=("expired-after-write",)) assert file_hashes(tmp_path) == files and model.calls == 0 failed_bookkeeping(h, method, "expired-after-write", rejected.json()["error"]) # No replay or recovery after reversing a synthetic clock is claimed. diff --git a/tests/integration/test_revision_full_rpc.py b/tests/integration/test_revision_full_rpc.py index d36bf9c..b89a184 100644 --- a/tests/integration/test_revision_full_rpc.py +++ b/tests/integration/test_revision_full_rpc.py @@ -140,6 +140,13 @@ def assert_restore_proposal_delta( "resource_scope_history": 2, "resource_scope_receipts": 2, } + owner_rows = [ + json.loads(row) + for row in after.get("operation_owners", ()) + if json.loads(row)["operation_id"] == operation_id + ] + if owner_rows: + expected_counts["operation_owners"] = 1 for table, count in expected_counts.items(): rows = [json.loads(row) for row in set(after[table]) - set(before[table])] assert len(rows) == count diff --git a/tests/integration/test_stale_running_current_behavior.py b/tests/integration/test_stale_running_current_behavior.py index 0bb53d3..3743eb0 100644 --- a/tests/integration/test_stale_running_current_behavior.py +++ b/tests/integration/test_stale_running_current_behavior.py @@ -4,16 +4,12 @@ must change exactly what is pinned here and nothing else. Children are real processes killed for real, with a synthetic home; no model, login or network is used. -Observed today: - - a request that was interrupted after its domain change was committed stays RUNNING, and the same - idempotency key returns RUNNING for as long as it is asked (the handler never runs again); - - a request that was interrupted inside its handler stays RUNNING with nothing changed, and only a - new key makes it run; - - the operation row records no process that owned the request, so nothing can tell "owner gone" - from "owner slow" (rows that exist without such information are exactly these rows); - - the domain change and the terminal result are two separate commits for this command; - - the repeat of an applied change under a new key is refused by the revision guard, so nothing is - applied twice. +Updated D-B2 observations: + - measured commands with a confirmed dead owner seal on same-key replay; + - an unmeasured source connection stays RUNNING without automatic redispatch; + - operations columns stay unchanged; process identity lives in operation_owners; + - domain and normal terminal publication remain separate commits; + - a new key after an applied change is still refused by its revision guard. """ from __future__ import annotations @@ -73,6 +69,20 @@ async def main(workspace, mode, marker): assert created.error is None, created.error update = build("project/metadata/update", "rename", { "project_id": "project:stale-running", "expected_revision": 0, "name": "Renamed"}) + if mode == "excluded": + inbox = workspace / "inbox" + inbox.mkdir(exist_ok=True) + (inbox / "source.md").write_text("Synthetic source", encoding="utf-8") + async def stuck_connect(value): + Path(marker).write_text(str(os.getpid()), encoding="utf-8") + await asyncio.Event().wait() + return {} + runtime.bus._registry.decorate("project/source/connect", lambda handler: stuck_connect) + update = build("project/source/connect", "connect", { + "project_id": "project:stale-running", "relative_path": "source.md", + "media_type": "text/markdown", "authority": "OFFICIAL", + "cutoff_state": "ELIGIBLE", "security_class": "INTERNAL", + "expected_project_revision": 0}) if mode == "after-domain-commit": def die(*_args, **_kwargs): Path(marker).write_text(str(os.getpid()), encoding="utf-8") @@ -145,7 +155,7 @@ def _rename(key: str, expected_revision: int) -> JsonRpcRequest: ) -async def test_a_request_interrupted_after_its_change_was_committed_stays_running( +async def test_a_request_interrupted_after_its_change_was_committed_is_sealed( tmp_path: Path, ) -> None: workspace = tmp_path / "workspace" @@ -159,15 +169,16 @@ async def test_a_request_interrupted_after_its_change_was_committed_stays_runnin ) assert read["name"] == "Renamed" and read["revision"] == 1 - # The same key is answered with RUNNING every time; the handler never runs again. + # Same-key replay seals the confirmed dead owner once; it never reruns the handler. first = await runtime.bus.dispatch(_rename("rename", 0)) again = await runtime.bus.dispatch(_rename("rename", 0)) for replay in (first, again): - assert replay.error is None and replay.result is not None - assert replay.result["state"] == "RUNNING" - assert replay.result["value"] is None - assert replay.result["execution_observation"] == "UNKNOWN" - assert [row[1] for row in _rows(runtime, "project/metadata/update")] == ["RUNNING"] + assert replay.error is not None and replay.result is None + assert replay.error.data["reason_code"] == "OPERATION_OWNER_GONE" + assert replay.error.data["remote_observation"] == "UNKNOWN" + assert replay.error.data["new_request_required"] is True + assert first.error == again.error + assert [row[1] for row in _rows(runtime, "project/metadata/update")] == ["FAILED"] after_replays = domain_snapshot(runtime.ledger.engine) # The repeat of the applied change under a NEW key is refused by the revision guard: @@ -177,14 +188,14 @@ async def test_a_request_interrupted_after_its_change_was_committed_stays_runnin assert "expected_revision" in repeat.error.message assert domain_snapshot(runtime.ledger.engine) == after_replays assert [row[1] for row in _rows(runtime, "project/metadata/update")] == [ - "RUNNING", + "FAILED", "FAILED", ] finally: runtime.close() -async def test_a_request_interrupted_inside_its_handler_stays_running_with_nothing_changed( +async def test_a_request_interrupted_inside_its_handler_is_sealed_with_nothing_changed( tmp_path: Path, ) -> None: workspace = tmp_path / "workspace" @@ -197,11 +208,11 @@ async def test_a_request_interrupted_inside_its_handler_stays_running_with_nothi ) assert before["name"] == "Original" and before["revision"] == 0 - # Asking again with the same key neither runs the change nor closes the request. + # Asking again seals the request without running the interrupted handler. for _ in range(2): replay = await runtime.bus.dispatch(_rename("rename", 0)) - assert replay.error is None and replay.result is not None - assert replay.result["state"] == "RUNNING" and replay.result["value"] is None + assert replay.error is not None and replay.result is None + assert replay.error.data["reason_code"] == "OPERATION_OWNER_GONE" unchanged = value( await runtime.bus.query(request("project/read", "unchanged", {"project_id": PROJECT})) ) @@ -211,14 +222,14 @@ async def test_a_request_interrupted_inside_its_handler_stays_running_with_nothi fresh = value(await runtime.bus.dispatch(_rename("rename-new", 0))) assert fresh["name"] == "Renamed" and fresh["revision"] == 1 assert [row[1] for row in _rows(runtime, "project/metadata/update")] == [ - "RUNNING", + "FAILED", "SUCCEEDED", ] finally: runtime.close() -async def test_the_operation_row_records_no_owning_process(tmp_path: Path) -> None: +async def test_process_owner_is_separate_from_unchanged_operation_columns(tmp_path: Path) -> None: workspace = tmp_path / "workspace" assert _interrupted_child(tmp_path, workspace, "after-domain-commit") == 37 runtime = _open(workspace) @@ -235,8 +246,13 @@ async def test_the_operation_row_records_no_owning_process(tmp_path: Path) -> No .mappings() .one() ) - # Only the authenticated actor is recorded, never the process that was running the work, so - # a RUNNING row cannot say whether its owner is gone. D-B adds those columns on purpose. + # Runtime now supplies the separate owner table, never new operations columns. + from thoth.adapters.storage.operations import SqliteOperationStore + + stored = SqliteOperationStore(runtime.ledger.engine).read(str(stuck["operation_id"])) + assert stored is not None and stored.owner is not None + assert stored.owner.host_token is not None + assert "owner" not in stored.model_dump(mode="json") assert not _OWNER_PROCESS_COLUMNS & columns assert not _OWNER_PROCESS_COLUMNS & set(OperationRecord.model_fields) assert stuck["state"] == "RUNNING" @@ -340,3 +356,34 @@ def die(*_args: object, **_kwargs: object) -> None: assert domain_snapshot(runtime.ledger.engine) == applied finally: runtime.close() + + +async def test_excluded_source_connection_stays_running_after_actual_child_kill( + tmp_path: Path, +) -> None: + workspace = tmp_path / "workspace" + assert _interrupted_child(tmp_path, workspace, "excluded") is None + runtime = _open(workspace) + try: + before = domain_snapshot(runtime.ledger.engine) + command = request( + "project/source/connect", + "connect", + { + "project_id": PROJECT, + "relative_path": "source.md", + "media_type": "text/markdown", + "authority": "OFFICIAL", + "cutoff_state": "ELIGIBLE", + "security_class": "INTERNAL", + "expected_project_revision": 0, + }, + ) + for _ in range(2): + response = await runtime.bus.dispatch(command) + assert response.error is None and response.result is not None + assert response.result["state"] == "RUNNING" and response.result["value"] is None + assert [r[1] for r in _rows(runtime, "project/source/connect")] == ["RUNNING"] + assert domain_snapshot(runtime.ledger.engine) == before + finally: + runtime.close() diff --git a/tests/integration/test_workspace_owner_pin.py b/tests/integration/test_workspace_owner_pin.py new file mode 100644 index 0000000..0260e95 --- /dev/null +++ b/tests/integration/test_workspace_owner_pin.py @@ -0,0 +1,46 @@ +"""Pinning cannot manufacture ownership and protects a live reference from release.""" + +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +from thoth.adapters.storage.workspace_owner import ( + acquire_workspace_owner, + holds_workspace, + pin_workspace_owner, +) + + +def test_missing_workspace_is_not_created_or_owned(tmp_path: Path) -> None: + workspace = tmp_path / "absent" + assert not holds_workspace(workspace) + with pin_workspace_owner(workspace) as held: + assert not held and not workspace.exists() + assert not workspace.exists() + + +def test_pin_keeps_existing_lock_across_original_release(tmp_path: Path) -> None: + ownership = acquire_workspace_owner(tmp_path) + with pin_workspace_owner(tmp_path) as held: + assert held and holds_workspace(tmp_path) + with ThreadPoolExecutor(max_workers=1) as pool: + pool.submit(ownership.release).result(timeout=5) + assert holds_workspace(tmp_path) + with pin_workspace_owner(tmp_path) as nested: + assert nested + assert holds_workspace(tmp_path) + assert not holds_workspace(tmp_path) + + +def test_failed_body_releases_only_the_pin(tmp_path: Path) -> None: + ownership = acquire_workspace_owner(tmp_path) + try: + try: + with pin_workspace_owner(tmp_path) as held: + assert held + raise RuntimeError("injected") + except RuntimeError: + pass + assert holds_workspace(tmp_path) + finally: + ownership.release() + assert not holds_workspace(tmp_path) diff --git a/tests/unit/test_operation_host_identity.py b/tests/unit/test_operation_host_identity.py new file mode 100644 index 0000000..bd4ea36 --- /dev/null +++ b/tests/unit/test_operation_host_identity.py @@ -0,0 +1,204 @@ +"""Synthetic OS identities: raw values never leave the supplier as tokens.""" + +import hashlib +import hmac +import sys +import threading +from concurrent.futures import ThreadPoolExecutor +from contextlib import nullcontext +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from thoth.adapters import operation_host_identity as identity +from thoth.domain.deployment_mode import DeploymentMode + +MACHINE = "12345678-1234-1234-1234-123456789abc" +BOOT = "87654321-4321-4321-4321-cba987654321" + + +@pytest.fixture(autouse=True) +def clean_cache(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(identity, "_cache", None) + + +def registry( + monkeypatch: pytest.MonkeyPatch, + boots: tuple[object, object] = (12, 12), + *, + machine: object = MACHINE, + kind: int = 1, +): + values = iter(boots) + calls: list[str] = [] + + def opened(root: int, path: str, reserved: int, access: int): + assert root == 100 and reserved == 0 and access == (2 | 4) + assert path in { + r"SOFTWARE\Microsoft\Cryptography", + ( + r"SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management" + r"\PrefetchParameters" + ), + } + return nullcontext(path) + + def query(key: str, name: str): + del key + calls.append(name) + return (next(values), 3) if name == "BootId" else (machine, kind) + + monkeypatch.setitem( + sys.modules, + "winreg", + SimpleNamespace( + HKEY_LOCAL_MACHINE=100, + KEY_READ=2, + KEY_WOW64_64KEY=4, + REG_DWORD=3, + REG_SZ=1, + OpenKey=opened, + QueryValueEx=query, + ), + ) + monkeypatch.setattr(identity.sys, "platform", "win32") + return calls + + +def test_windows_double_read_and_hash_only( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + calls = registry(monkeypatch) + token = identity.operation_host_token(DeploymentMode.LOCAL) + expected = hmac.new( + b"thoth.operation-owner.host.v1", + ("win32\0" + MACHINE.replace("-", "") + "\0prefetch-bootid:12").encode(), + hashlib.sha256, + ).hexdigest() + assert token is not None and token == expected and len(token) == 64 + assert identity.operation_host_token(DeploymentMode.LOCAL) == token + assert calls == ["BootId", "MachineGuid", "BootId"] + assert vars(identity)["_cache"] == (identity.os.getpid(), token) + assert MACHINE not in repr(vars(identity)["_cache"]) + assert capsys.readouterr() == ("", "") + + +@pytest.mark.parametrize( + "boots,machine,kind", + [ + ((12, 13), MACHINE, 1), + ((-1, -1), MACHINE, 1), + ((2**32, 2**32), MACHINE, 1), + ((True, True), MACHINE, 1), + ((12, 12), "bad-id", 1), + ((12, 12), "00000000-0000-0000-0000-000000000000", 1), + ((12, 12), MACHINE, 3), + ], +) +def test_invalid_registry_or_changed_boot_caches_none( + monkeypatch: pytest.MonkeyPatch, boots: tuple[object, object], machine: object, kind: int +) -> None: + calls = registry(monkeypatch, boots, machine=machine, kind=kind) + assert identity.operation_host_token(DeploymentMode.LOCAL) is None + count = len(calls) + assert identity.operation_host_token(DeploymentMode.LOCAL) is None + assert len(calls) == count + + +@pytest.mark.parametrize("error", [FileNotFoundError, PermissionError, OSError]) +def test_registry_failure_returns_none_without_exposing_error( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], error: type[OSError] +) -> None: + registry(monkeypatch) + + def fail(*args: object): + raise error(MACHINE) + + monkeypatch.setattr(sys.modules["winreg"], "OpenKey", fail) + assert identity.operation_host_token(DeploymentMode.LOCAL) is None + assert capsys.readouterr() == ("", "") + + +def test_zero_counter_is_valid(monkeypatch: pytest.MonkeyPatch) -> None: + registry(monkeypatch, (0, 0)) + assert identity.operation_host_token(DeploymentMode.LOCAL) is not None + + +def test_hosted_never_reads_even_when_cache_exists(monkeypatch: pytest.MonkeyPatch) -> None: + calls = registry(monkeypatch) + assert identity.operation_host_token(DeploymentMode.HOSTED_REVIEW) is None + assert calls == [] and vars(identity)["_cache"] is None + assert identity.operation_host_token(DeploymentMode.LOCAL) is not None + assert identity.operation_host_token(DeploymentMode.HOSTED_REVIEW) is None + assert len(calls) == 3 + + +def test_linux_reads_bounded_files_and_uses_os_specific_hash( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + machine, boot = tmp_path / "machine", tmp_path / "boot" + machine.write_text(MACHINE.replace("-", "") + "\n") + boot.write_text(BOOT + "\n") + real_open = Path.open + + def opened(path: Path, *args: object, **kwargs: object): + target = {"/etc/machine-id": machine, "/proc/sys/kernel/random/boot_id": boot}[ + path.as_posix() + ] + return real_open(target, "rb") + + monkeypatch.setattr(identity.sys, "platform", "linux") + monkeypatch.setattr(Path, "open", opened) + token = identity.operation_host_token(DeploymentMode.LOCAL) + expected = hmac.new( + b"thoth.operation-owner.host.v1", + ("linux\0" + MACHINE.replace("-", "") + "\0" + BOOT.replace("-", "")).encode(), + hashlib.sha256, + ).hexdigest() + assert token == expected + + +@pytest.mark.parametrize("raw", [b"", b"0" * 32, b"x" * 129, b"\xff" * 32]) +def test_linux_invalid_or_oversize_identity_fails_closed( + monkeypatch: pytest.MonkeyPatch, raw: bytes +) -> None: + import io + + monkeypatch.setattr(identity.sys, "platform", "linux") + + def opened(*args: object, **kwargs: object) -> io.BytesIO: + return io.BytesIO(raw) + + monkeypatch.setattr(Path, "open", opened) + assert identity.operation_host_token(DeploymentMode.LOCAL) is None + + +def test_concurrent_first_calls_read_once_and_new_pid_gets_new_cache( + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls: list[int] = [] + + def read() -> str: + calls.append(1) + return "a" * 64 + + monkeypatch.setattr(identity, "_read_token", read) + with ThreadPoolExecutor(max_workers=4) as pool: + assert ( + list(pool.map(identity.operation_host_token, [DeploymentMode.LOCAL] * 8)) + == ["a" * 64] * 8 + ) + assert calls == [1] + monkeypatch.setattr(identity.os, "getpid", lambda: 123456789) + assert identity.operation_host_token(DeploymentMode.LOCAL) == "a" * 64 + assert calls == [1, 1] + old_lock = vars(identity)["_lock"] + vars(identity)["_after_fork"]() + assert vars(identity)["_cache"] is None and vars(identity)["_lock"] is not old_lock + assert isinstance(vars(identity)["_lock"], type(threading.Lock())) + + +def test_unsupported_os_does_not_read(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(identity.sys, "platform", "unsupported") + assert identity.operation_host_token(DeploymentMode.LOCAL) is None diff --git a/tests/unit/test_operation_owner_allowances.py b/tests/unit/test_operation_owner_allowances.py new file mode 100644 index 0000000..c815758 --- /dev/null +++ b/tests/unit/test_operation_owner_allowances.py @@ -0,0 +1,139 @@ +"""Bookkeeping accepts one exact new owner, never unrelated or existing-row changes.""" + +import json +from collections.abc import Mapping +from copy import deepcopy + +import pytest +from tests.atomicity.harness import ( + AllowedRow, + Snapshot, + assert_phase_delta, + unchanged_or_operation_owner, +) +from tests.integration.storage_coverage_helpers import request + + +def encoded(value: Mapping[str, object]) -> str: + return json.dumps(value, sort_keys=True) + + +def states() -> tuple[Snapshot, Snapshot, tuple[AllowedRow, ...]]: + operation: dict[str, object] = { + "operation_id": "operation:new", + "project_id": "project:test", + "method": "project/metadata/update", + "idempotency_key": "new", + "state": "FAILED", + } + owner: dict[str, object] = { + "operation_id": "operation:new", + "worker_id": "worker:synthetic", + "process_id": 123, + "process_marker": "marker", + "host_token": "b" * 64, + } + before: Snapshot = { + "operations": (), + "operation_owners": (), + "domain": (encoded({"value": 1}),), + } + after: Snapshot = { + **before, + "operations": (encoded(operation),), + "operation_owners": (encoded(owner),), + } + return before, after, (AllowedRow("operations", operation, "added", {}),) + + +def test_exact_new_owner_and_ownerless_environment_are_allowed() -> None: + before, after, allowed = states() + assert_phase_delta(before, after, allowed) + after["operation_owners"] = () + assert_phase_delta(before, after, allowed) + + +@pytest.mark.parametrize( + "case", ["foreign", "two", "duplicate", "modify", "delete", "backfill", "domain"] +) +def test_owner_rules_never_weaken_other_row_or_domain_checks(case: str) -> None: + before, after, allowed = states() + own = json.loads(after["operation_owners"][0]) + old = {**own, "operation_id": "operation:old"} + if case == "foreign": + after["operation_owners"] = (encoded(old),) + if case == "two": + after["operation_owners"] += (encoded(old),) + if case == "duplicate": + after["operation_owners"] *= 2 + if case in {"modify", "delete"}: + before["operation_owners"] = (encoded(old),) + if case == "modify": + after["operation_owners"] += (encoded({**old, "process_id": 456}),) + if case == "backfill": + before["operations"] = after["operations"] + if case == "domain": + after["domain"] = (encoded({"value": 2}),) + with pytest.raises(AssertionError): + assert_phase_delta(before, after, allowed) + + +def test_domain_projection_still_compares_every_domain_byte() -> None: + before, after, _ = states() + command = request("project/metadata/update", "new", {"project_id": "project:test"}) + assert unchanged_or_operation_owner(before, after, "operation_owners", command) + assert unchanged_or_operation_owner(before, after, "domain", command) + changed = deepcopy(after) + changed["domain"] = (encoded({"value": 2}),) + assert not unchanged_or_operation_owner(before, changed, "domain", command) + changed["operation_owners"] += ( + encoded( + {**json.loads(changed["operation_owners"][0]), "operation_id": "operation:foreign"} + ), + ) + with pytest.raises(AssertionError): + unchanged_or_operation_owner(before, changed, "operation_owners", command) + + +@pytest.mark.parametrize( + "case", ["valid", "foreign", "two", "modify", "delete", "backfill", "domain"] +) +def test_scope_view_keeps_raw_evidence_and_rejects_other_changes(case: str) -> None: + from tests.integration.test_resource_scope_transition_boundaries import ( + Snapshot as ScopeSnapshot, + ) + from tests.integration.test_resource_scope_transition_boundaries import ( + owner_adjusted, + ) + + before, after, _ = states() + owner = json.loads(after["operation_owners"][0]) + old = {**owner, "operation_id": "operation:old"} + if case == "foreign": + after["operation_owners"] = (encoded(old),) + if case == "two": + after["operation_owners"] += (encoded(old),) + if case in {"modify", "delete"}: + before["operation_owners"] = (encoded(old),) + if case == "modify": + after["operation_owners"] += (encoded({**old, "process_id": 456}),) + if case == "backfill": + before["operations"] = after["operations"] + if case == "domain": + after["domain"] = (encoded({"value": 2}),) + left = ScopeSnapshot({k: v for k, v in before.items() if k != "operations"}) + left.operation_rows = before["operations"] + right = ScopeSnapshot({k: v for k, v in after.items() if k != "operations"}) + right.operation_rows = after["operations"] + saved = deepcopy(right) + if case == "valid": + assert ( + owner_adjusted(left, right, "project:test", "project/metadata/update", ("new",)) == left + ) + else: + with pytest.raises(AssertionError): + assert ( + owner_adjusted(left, right, "project:test", "project/metadata/update", ("new",)) + == left + ) + assert right == saved diff --git a/tests/unit/test_operation_owner_policy.py b/tests/unit/test_operation_owner_policy.py new file mode 100644 index 0000000..624b547 --- /dev/null +++ b/tests/unit/test_operation_owner_policy.py @@ -0,0 +1,180 @@ +"""Every owner predicate fails closed; the approved list and wire text are exact.""" + +import json +from contextlib import nullcontext +from dataclasses import dataclass +from datetime import UTC, datetime +from pathlib import Path +from typing import cast + +import pytest + +from thoth.domain.deployment_mode import STALE_RUNNING_MESSAGE, STALE_RUNNING_REASON +from thoth.domain.enums import OperationState +from thoth.domain.operation import OperationOwnerRecord, OperationRecord +from thoth.ports.journal import JournalPort +from thoth.ports.operation import OperationStorePort +from thoth.ports.runtime import AtomicUnitOfWorkPort, ClockPort +from thoth.protocol.jsonrpc import JsonRpcError, JsonRpcRequest, RpcErrorCode +from thoth.protocol.operation_owner import ( + SEALABLE_METHODS, + OperationOwner, + owner_gone_error, + stale_running_error, +) +from thoth.protocol.registry import PUBLIC_METHODS + + +@dataclass +class Worker: + alive: bool | None = False + worker_id: str = "worker:current" + process_id: int = 20 + process_marker: str = "marker:current" + calls: int = 0 + + def is_alive(self, process_id: int, process_marker: str, worker_id: str) -> bool | None: + self.calls += 1 + assert (process_id, process_marker, worker_id) == (10, "marker:old", "worker:old") + return self.alive + + def close(self) -> None: + pass + + +def operation() -> OperationRecord: + return OperationRecord( + operation_id="operation:policy", + project_id="project:policy", + method="project/metadata/update", + idempotency_key="key", + scope_digest="a" * 64, + created_at=datetime(2026, 10, 19, tzinfo=UTC), + state=OperationState.RUNNING, + owner=OperationOwnerRecord( + worker_id="worker:old", process_id=10, process_marker="marker:old", host_token="b" * 64 + ), + ) + + +def policy( + worker: Worker, token: str | None = "b" * 64, held: bool = True, pin: bool = True +) -> OperationOwner: + return OperationOwner( + worker=worker, + host_token=token, + holds_workspace=lambda: held, + pin_workspace=lambda: nullcontext(pin), + operations=cast(OperationStorePort, object()), + uow=cast(AtomicUnitOfWorkPort, object()), + journal=cast(JournalPort, object()), + clock=cast(ClockPort, object()), + ) + + +@pytest.mark.parametrize( + "case", + [ + "eligible", + "alive", + "unknown", + "no-owner", + "no-token", + "different-token", + "old-null-token", + "old-unknown-marker", + "current-unknown-marker", + "no-lock", + "pending", + "terminal", + "excluded", + "method-mismatch", + ], +) +def test_all_conditions_are_required(case: str) -> None: + worker, original = Worker(), operation() + token: str | None = "b" * 64 + method = original.method + if case == "alive": + worker.alive = True + if case == "unknown": + worker.alive = None + if case == "no-owner": + original = original.model_copy(update={"owner": None}) + if case == "no-token": + token = None + if case == "different-token": + token = "c" * 64 + if case in {"old-null-token", "old-unknown-marker"}: + assert original.owner is not None + update = {"host_token": None} if case == "old-null-token" else {"process_marker": "UNKNOWN"} + original = original.model_copy(update={"owner": original.owner.model_copy(update=update)}) + if case == "current-unknown-marker": + worker.process_marker = "UNKNOWN" + if case == "pending": + original = original.model_copy(update={"state": OperationState.PENDING}) + if case == "terminal": + original = original.model_copy(update={"state": OperationState.SUCCEEDED}) + if case == "excluded": + method = "thread/resume" + original = original.model_copy(update={"method": method}) + if case == "method-mismatch": + method = "project/activate" + subject = policy(worker, token, held=case != "no-lock") + assert subject.gone(original, method) is (case == "eligible") + if case not in {"eligible", "alive", "unknown"}: + assert worker.calls == 0 + if case in {"no-token", "current-unknown-marker"}: + assert subject.record() is None + + +def test_lock_lost_before_pin_and_different_key_cannot_write() -> None: + def unmeasured(*args: object) -> None: + pass + + original = operation() + request = JsonRpcRequest.model_validate( + { + "id": "key", + "method": original.method, + "params": {"_meta": {"idempotencyKey": "key"}, "input": {}}, + } + ) + assert policy(Worker(), pin=False).seal(request, original, unmeasured) is None + other = request.model_copy( + update={ + "params": request.params.model_copy( + update={ + "meta": request.params.meta.model_copy(update={"idempotency_key": "different"}), + } + ) + } + ) + assert policy(Worker()).seal(other, original, unmeasured) is None + + +def test_list_and_bilingual_error_are_exact() -> None: + fixture = json.loads((Path(__file__).parents[1] / "fixtures/sealable_methods.json").read_text()) + assert frozenset(fixture["methods"]) == SEALABLE_METHODS and len(SEALABLE_METHODS) == 67 + assert set(PUBLIC_METHODS) >= SEALABLE_METHODS + expected = ( + "이전에 보낸 같은 요청을 실행하던 THOTH가 끝나서 요청이 마무리되지 못했습니다. " + "변경이 저장됐는지 확인한 뒤 필요하면 새 요청으로 다시 보내세요. / " + "The THOTH process that was running this request ended before it finished. " + "Check whether the change was saved, then send it again as a new request " + "if you still need it." + ) + assert owner_gone_error().message == expected + assert owner_gone_error().data == { + "reason_code": "OPERATION_OWNER_GONE", + "remote_observation": "UNKNOWN", + "new_request_required": True, + } + assert ( + stale_running_error().model_dump_json() + == JsonRpcError( + code=RpcErrorCode.DOMAIN_REJECTED, + message=STALE_RUNNING_MESSAGE, + data={"reason_code": STALE_RUNNING_REASON, "remote_observation": "UNKNOWN"}, + ).model_dump_json() + )