simcord 3.2.1__tar.gz → 3.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simcord-3.2.1 → simcord-3.2.2}/CHANGELOG.md +28 -0
- {simcord-3.2.1 → simcord-3.2.2}/PKG-INFO +2 -1
- simcord-3.2.2/benchmarks/simulation_attribution.py +570 -0
- simcord-3.2.2/benchmarks/test_dispatch_benchmarks.py +73 -0
- simcord-3.2.2/benchmarks/test_simulation_benchmarks.py +167 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/architecture.md +21 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/installation.md +3 -2
- simcord-3.2.2/docs/performance.md +205 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/stability.md +6 -3
- {simcord-3.2.1 → simcord-3.2.2}/pyproject.toml +2 -2
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/_dpy_internals.py +49 -63
- simcord-3.2.2/src/simcord/_payload.py +54 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/_runtime.py +157 -13
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/_settlement.py +108 -62
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/actors.py +5 -3
- simcord-3.2.2/src/simcord/backend/_deadlines.py +162 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/access.py +11 -2
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/cdn.py +11 -1
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/automod.py +4 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/base.py +164 -16
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/channels.py +14 -8
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/guilds.py +6 -1
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/messages.py +139 -29
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/monetization.py +59 -9
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/polls.py +22 -18
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/scheduled_events.py +41 -20
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/serializers.py +15 -13
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/builders.py +15 -12
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/components.py +50 -4
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/env.py +41 -66
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/gateway.py +3 -3
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/_helpers.py +48 -89
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/router.py +3 -1
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/interactions.py +6 -20
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/messages.py +33 -21
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/monetization.py +12 -10
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/__init__.py +2 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_action_plans.py +28 -16
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_commands.py +9 -7
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_markdown.py +11 -2
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_messages.py +4 -1
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_pages.py +7 -11
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_queries.py +93 -15
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_snapshot.py +44 -34
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/results.py +2 -1
- {simcord-3.2.1 → simcord-3.2.2}/tests/conformance/_world.py +1 -1
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_automod.py +74 -0
- simcord-3.2.2/tests/integration/test_callback_thread_safety.py +121 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_interaction_followups.py +2 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_commands.py +146 -2
- simcord-3.2.2/tests/integration/test_preview_navigation_contracts.py +449 -0
- simcord-3.2.2/tests/integration/test_preview_render_contracts.py +125 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_security.py +22 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_runtime_executor_ownership.py +0 -2
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_settlement_contract.py +489 -25
- simcord-3.2.2/tests/unit/test_automod_preparation.py +264 -0
- simcord-3.2.2/tests/unit/test_backend_performance_contracts.py +517 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_dpy_internals.py +3 -91
- simcord-3.2.2/tests/unit/test_local_eliminations.py +228 -0
- simcord-3.2.2/tests/unit/test_payload_ownership.py +595 -0
- {simcord-3.2.1 → simcord-3.2.2}/uv.lock +3 -1
- simcord-3.2.1/benchmarks/test_dispatch_benchmarks.py +0 -54
- simcord-3.2.1/docs/performance.md +0 -58
- simcord-3.2.1/tests/integration/test_preview_navigation_contracts.py +0 -198
- simcord-3.2.1/tests/integration/test_preview_render_contracts.py +0 -54
- {simcord-3.2.1 → simcord-3.2.2}/.github/ISSUE_TEMPLATE/bug.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/.github/ISSUE_TEMPLATE/feature.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/.github/ISSUE_TEMPLATE/parity-gap.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/.github/dependabot.yml +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/.github/workflows/ci.yml +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/.github/workflows/dpy-master.yml +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/.github/workflows/release.yml +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/.github/workflows/verify.yml +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/.gitignore +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/.pre-commit-config.yaml +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/.python-version +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/.readthedocs.yaml +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/CODE_OF_CONDUCT.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/CONTRIBUTING.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/LICENSE +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/README.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/SECURITY.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/benchmarks/_helpers.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/benchmarks/test_perf_guards.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/changes/README.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/api.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/adventure/alpine.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/adventure/canyon.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/preview/attachments.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/preview/buttons.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/preview/embeds.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/preview/entities.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/preview/layouts.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/preview/markdown.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/preview/modals.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/preview/polls.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/preview/replies.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/preview/selects.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/preview/slash-commands.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/simcord-showcase-poll-refresh.gif +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/simcord-terminal-demo.gif +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/simcord-transparent.webp +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/assets/social-card.png +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/changelog.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/concepts.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/cookbook.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/faq.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/ai-coding-agents.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/components.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/diagnostics.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/discord-visual-calibration.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/fixtures.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/intents.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/interactions.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/messages.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/mocks-vs-simulation.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/moderation-and-guild.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/monetization.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/permissions.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/preview.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/test-without-token.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/testing-discord-py-bots.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/testing-slash-commands.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/threads-reactions-dms.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/guides/time-control.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/index.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/llms.txt +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/parity-matrix.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/preview-attestation.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/quickstart.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/docs/robots.txt +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/examples/README.md +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/examples/__init__.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/examples/bot.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/examples/conftest.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/examples/preview_example.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/examples/test_bot.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/jsconfig.preview.json +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/mkdocs.yml +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/overrides/main.html +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/package-lock.json +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/package.json +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/capture_discord_reference.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/capture_monetization_reference.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/capture_remaining_monetization.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/capture_visual_reference.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/check_discoverability.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/check_package.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/check_reference_modal_offline.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/compare_visual_reference.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/discord_reference_bot.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/monetization_reference_checkout.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/monetization_reference_config.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/monetization_reference_expiry.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/monetization_reference_journal.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/monetization_reference_lifecycle.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/monetization_reference_transport.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/monetization_reference_upgrade.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/preview_dogfood.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/record_readme_demo.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/replay_visual_reference.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/validate_discord_reference_pack.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/scripts/validate_monetization_reference.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/__init__.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/_clock.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/_modal.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/asserts.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/__init__.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/command_access.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/errors.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/__init__.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/auditlog.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/channel.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/expression.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/guild.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/interaction.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/invite.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/member.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/message.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/monetization.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/role.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/scheduled_event.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/stage_instance.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/user.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/voice.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/models/webhook.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/__init__.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/audit.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/commands.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/expressions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/invites.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/permissions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/reactions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/voice.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/ops/webhooks.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/permissions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/backend/state.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/enums.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/__init__.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/client.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/__init__.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/application.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/auditlogs.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/automod.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/channels.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/commands.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/expressions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/guilds.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/invites.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/reactions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/scheduled_events.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/http/routes/stage_instances.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/intents.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/interactions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/monetization.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/parity.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_actions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_assets.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_capture.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_component_projection.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_dependencies.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_diagnostics.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_identity.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_media.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_media_worker.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/_server.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/protocol.schema.json +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/actions.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/app.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/assets.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/backend-diagnostics.json +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/command-catalog.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/command-drafts.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/commands.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/components.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/composer.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/connection.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/diagnostics.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/dom.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/focus.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Arabic.txt +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/OFL-1.1-Noto-CJK.txt +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Devanagari.txt +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Emoji.txt +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/OFL-1.1-Noto-Hebrew.txt +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/OFL-1.1-Noto.txt +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/manifest.json +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/noto-color-emoji-v2.051.ttf +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/noto-sans-arabic-v2.012.ttf +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/noto-sans-devanagari-v2.007.ttf +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/noto-sans-hebrew-v3.001.ttf +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/noto-sans-latin-italic-v2.015.ttf +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/noto-sans-latin-v2.015.ttf +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/noto-sans-mono-v2.014.ttf +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/fonts/noto-sans-sc-v2.004.ttf +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/geometry.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/highlight-types.d.ts +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/index.html +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/listbox.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/media.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/messages.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/modal.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/presentation.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/preview-types.d.ts +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/preview.css +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/select-drafts.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/selects.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/session.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/surfaces.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/text.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/timeline.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/transport.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/LICENSE +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/SHA256SUMS +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/core.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/bash.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/cpp.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/csharp.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/css.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/diff.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/dockerfile.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/go.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/ini.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/java.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/javascript.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/json.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/markdown.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/python.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/rust.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/sql.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/typescript.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/xml.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/highlight/es/languages/yaml.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/lottie/5.12.2/lottie_light_canvas.min.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/lottie/LICENSE +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/vendor/lottie/SHA256SUMS +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/preview/static/workbench.js +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/py.typed +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/pytest_plugin.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/src/simcord/types.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/conformance/test_field_honesty.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/conformance/test_field_honesty_fuzz.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/conformance/test_route_coverage.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/conformance/test_serializers.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/conftest.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/__init__.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/commands/option_cases.json +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/preview/__init__.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/preview/catalog.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/preview/coverage.json +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/preview/measurements.json +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/preview/moving-square.json +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/preview/profiles.json +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/preview/states.json +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/preview/video.mp4 +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/preview/voice.ogg +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/sample_bot/__init__.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/sample_bot/events.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/sample_bot/general.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/sample_bot/interactions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/sample_bot/moderation.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/fixtures/sample_bot/picker.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/preview_helpers.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_access_boundaries.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_app_commands.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_application_emojis.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_asserts.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_atomic_mutations.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_audit_logs.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_bot_users_and_webhooks.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_channels.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_command_permissions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_command_visibility.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_common_bot_sweep.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_components_v2_interactions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_components_v2_messages.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_coverage_sweep.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_diagnostics.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_edge_cases.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_entitlement_journey.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_error_capture_and_edits.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_expressions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_forum.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_forwarding.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_gateway_and_automod_exempt.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_guilds.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_intents.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_interactions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_invites.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_lifecycle.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_members.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_message_content_rest.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_message_routes.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_messages.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_misc_branches.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_moderation.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_observation_contracts.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_polls.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_action_admission.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_additional.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_browser_ownership.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_capture.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_capture_dx.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_channel.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_command_picker.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_components_v2.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_coverage.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_dx.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_edges.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_entity_contracts.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_fixes.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_fonts.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_identity.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_message_actions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_modal_contracts.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_observations.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_provenance.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_support_report.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_viewport_ownership.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_preview_workbench_contracts.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_reactions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_reference_replay.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_roles_and_ordering.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_scheduled_events.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_sdk_compatibility.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_setup_hook_suspension.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_sharding.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_shared_clock.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_stage_instances.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_threads.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_time.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_transport_edges.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_visual_reference_geometry.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_voice_state.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/integration/test_world_helpers.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_assert_helpers.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_backend_lookup_errors.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_command_access.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_components_v2_validation.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_errors.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_intent_rules.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_lazy_import.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_monetization_backend.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_monetization_http.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_monetization_reference_capture.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_monetization_reference_checkout.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_monetization_reference_expiry.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_monetization_reference_lifecycle.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_monetization_reference_upgrade.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_monetization_reference_validation.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_option_validation.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_parity.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_parity_tooling.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_permission_validators.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_permissions.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_preview_action_specs.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_pytest_clock.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_pytest_plugin.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_remaining_monetization_capture.py +0 -0
- {simcord-3.2.1 → simcord-3.2.2}/tests/unit/test_router.py +0 -0
|
@@ -4,6 +4,34 @@ This changelog is generated with [towncrier](https://towncrier.readthedocs.io/).
|
|
|
4
4
|
|
|
5
5
|
<!-- towncrier release notes start -->
|
|
6
6
|
|
|
7
|
+
## 3.2.2 (2026-10-31)
|
|
8
|
+
|
|
9
|
+
### Bug fixes
|
|
10
|
+
|
|
11
|
+
- Detach nested AutoMod create/edit inputs and returned rule data so SDK object mutation cannot silently change enforcement. Lazily retain visited keyword patterns on each live rule, replacing preparation when its actual keyword filter changes and avoiding dependence on the shared regex cache.
|
|
12
|
+
- Synchronize per-Env callback publication, retirement and settlement snapshots when owned executor threads schedule callbacks. Preserve cancellation, context and exact-once completed effects without holding the lifecycle lock while user callbacks run.
|
|
13
|
+
- Settlement now considers the earliest live recognized wake across tracked and native timers, so a later timer cannot make an actor operation return before an earlier wake completes. Future-valued timer results are not mistaken for wake targets. Retired callbacks release their Handle links without changing native cancellation, Context or error behavior.
|
|
14
|
+
- Payload detachment uses exact type identity for its wire fast path. Custom metaclass equality can no longer bypass independent copying or the original whole-graph deepcopy hooks and failures.
|
|
15
|
+
- Retire all live message deadlines before publishing explicit finalization of a shared backend Poll, including when a subscriber raises. Keep automatic Poll expiry index-driven without an additional alias index.
|
|
16
|
+
- Preserve scheduled-event pass membership during synchronous subscriber creation without losing later-event rescheduling. Accept unhashable AutoMod string keywords and keep text-identical keyword definitions distinct with lazy positional preparation. Centralize edit preparation and callback-index lifecycle ownership while preserving upload atomicity, deferred-response identity, cancellation and native callback behavior; remove the superseded sleep recognizer.
|
|
17
|
+
Keep the standalone attribution overlay aligned with the canonical edit commit so documented comparison runs remain executable.
|
|
18
|
+
|
|
19
|
+
### Miscellaneous
|
|
20
|
+
|
|
21
|
+
- AutoMod lazily skips impossible ASCII keyword matches before compiling or searching regexes, using the existing live-rule preparation owner. Keyword priority, Unicode and wildcard semantics, in-place edit detection and detached rule payloads are unchanged. No-rule, early-hit and Unicode workloads are not claimed as universal speedups.
|
|
22
|
+
Deferred preparation retains the original keyword's regex definition, including across equal-valued string-subclass replacements.
|
|
23
|
+
- Reconstruct send, edit and history dispatch worlds for every measured benchmark round, including their HTTP/transcript state. Add a small complete command/reply scenario to the existing public-operation lane and distinguish its setup/shutdown scope from bare dispatch and stress workloads.
|
|
24
|
+
- Reuse the publication schema fingerprint when validating live command catalogs, hashing only published keys without reconstructing wire option trees. Preserve live permissions, stale-entry removal, bounded publication and refresh behavior.
|
|
25
|
+
- Reduce settlement CPU for bots with live Views and native I/O: drain owned progress before rescanning SDK waits, and read verified View lifecycle fields directly rather than scanning their attributes. Preserve completed effects, executor ownership, virtual time and settlement deadlines. Add a fresh-world local-I/O/live-View scenario to the existing reported benchmark lane without adding caches or scheduling indexes.
|
|
26
|
+
- Avoid redundant wait recognition when neither an owned task nor its waiter has an indexed timer wake. Skip Preview mention/emoji discovery and command-map construction when all relevant message texts lack their opening syntax, preserving everyone mentions, channel mention inventories, rendered search and live authorization. No new cache, scheduler, ownership representation or tuning option; sparse search, repeated composition classification and expanded reply-history retention retain their existing scaling limits.
|
|
27
|
+
- Detach nested REST/SDK message responses, edit inputs and retained HTTP/gateway history from canonical state and live subscriber envelopes. Complete fallible ingress detachment before upload, deferred-response and index side effects; retain shared-Poll and ordered subscriber/partial-commit behavior. These correctness fixes add copying CPU and retained-memory costs. Avoid task scans for callbacks without wake targets, eager ordered-bucket allocation, unchanged message index membership updates and per-ID removal of discarded channel buckets. Exact rendered Preview text search now seeks bounded pages with correct surviving-pivot witnesses and empty-page semantics; sparse/absent queries remain linear. No new dependency, cache, retention policy or public protocol change.
|
|
28
|
+
|
|
29
|
+
Preserve previously functioning deep reply ancestry using stack-safe wire-payload detachment at event, response and SDK parsing boundaries. Keep independent mutable snapshots and custom-data deepcopy behavior; avoid an unused live envelope when no subscribers exist. Add reproducible interleaved original/correctness-reference/optimized comparisons rather than attributing all measured overhead to necessary isolation.
|
|
30
|
+
- Reduce simulator CPU overhead in callback tracking, wait classification, virtual-time scheduling, component preparation, bounded history/entitlement queries, and Preview projection. Wake settlement on tracked progress rather than imposing a callback-only polling delay. Preserve detached request/result payloads and authorization; mutate indexed backend state through Backend methods, not raw internal fields. Ordered selection adds the pure-Python Sorted Containers runtime dependency. Rejected actor/webhook message validation no longer leaves partially installed upload assets.
|
|
31
|
+
|
|
32
|
+
Retire callbacks canceled during settlement and inactive due poll schedules without waiting for a safety sweep. Preserve traversal-order errors when malformed deadlines and mixed timezone awareness coexist.
|
|
33
|
+
|
|
34
|
+
|
|
7
35
|
## 3.2.1 (2026-10-24)
|
|
8
36
|
|
|
9
37
|
### Bug fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simcord
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.2
|
|
4
4
|
Summary: Offline discord.py bot testing with pytest. No network, token, or test server.
|
|
5
5
|
Project-URL: Homepage, https://github.com/SilentHacks/simcord
|
|
6
6
|
Project-URL: Documentation, https://simcord.readthedocs.io/
|
|
@@ -23,6 +23,7 @@ Classifier: Topic :: Software Development :: Testing :: Mocking
|
|
|
23
23
|
Classifier: Typing :: Typed
|
|
24
24
|
Requires-Python: >=3.11
|
|
25
25
|
Requires-Dist: discord-py<3,>=2.7.1
|
|
26
|
+
Requires-Dist: sortedcontainers<3,>=2.4
|
|
26
27
|
Provides-Extra: bench
|
|
27
28
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'bench'
|
|
28
29
|
Requires-Dist: pytest-benchmark>=4; extra == 'bench'
|
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
"""Standalone, process-isolated simulation attribution (stdlib runner).
|
|
2
|
+
|
|
3
|
+
Example (paths must contain src/simcord):
|
|
4
|
+
python benchmarks/simulation_attribution.py --original /evidence/reference \
|
|
5
|
+
--before /evidence/bd21e55 --output /evidence/new-run --smoke
|
|
6
|
+
Remove --smoke for nine randomized forward/reverse source batches. Add
|
|
7
|
+
--instrument profile or --instrument allocation for separate instrumented runs;
|
|
8
|
+
never compare those numbers with uninstrumented timing. Output directories must
|
|
9
|
+
not exist; raw JSON and profiles are exclusive-created and made read-only.
|
|
10
|
+
|
|
11
|
+
correctness-only is a *controlled candidate-source overlay*: current ownership
|
|
12
|
+
repairs remain, while original0083422 exact-search, wake recognition and local
|
|
13
|
+
index maintenance are restored, and emission always makes separate envelope and
|
|
14
|
+
history copies, as bd21e55 did. Stack-safe detach is shared with the candidate
|
|
15
|
+
because recursive deepcopy cannot support the required ancestry. Its traversal
|
|
16
|
+
efficiency is inseparable from this correctness baseline; this is not a pure
|
|
17
|
+
measurement of every ownership cost. No Git operations occur.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import ast
|
|
24
|
+
import asyncio
|
|
25
|
+
import cProfile
|
|
26
|
+
import gc
|
|
27
|
+
import hashlib
|
|
28
|
+
import importlib.metadata
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import platform
|
|
32
|
+
import random
|
|
33
|
+
import shutil
|
|
34
|
+
import subprocess
|
|
35
|
+
import sys
|
|
36
|
+
import time
|
|
37
|
+
import traceback
|
|
38
|
+
import tracemalloc
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
|
|
41
|
+
CONSTRUCTION = {
|
|
42
|
+
"original0083422": "Unmodified supplied original source; ownership defects intentionally retained.",
|
|
43
|
+
"bd21e55": "Unmodified supplied pre-copy-repair source, not the original baseline.",
|
|
44
|
+
"candidate": "Current repository src, imported directly in a fresh subprocess.",
|
|
45
|
+
"correctness-only": (
|
|
46
|
+
"Copy candidate src; AST-select original _message_navigation, "
|
|
47
|
+
"_wakes_recognized_wait, BackendBase._index_message/_drop_channel_messages; "
|
|
48
|
+
"restore unconditional edit remove/reindex; remove obsolete index helpers; "
|
|
49
|
+
"restore unconditional separate envelope/history copies (bd21e55 strategy). "
|
|
50
|
+
"Keep stack-safe detach at original full-payload copying boundaries. "
|
|
51
|
+
"This isolates old search/wake/index gains and new unused-envelope savings, "
|
|
52
|
+
"but stack-safe traversal efficiency is inseparable from correctness."
|
|
53
|
+
),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def source_root(path):
|
|
58
|
+
path = path.resolve()
|
|
59
|
+
root = path / "src" if (path / "src" / "simcord").is_dir() else path
|
|
60
|
+
if not (root / "simcord" / "__init__.py").is_file():
|
|
61
|
+
raise ValueError(f"Not a simcord source directory: {path}")
|
|
62
|
+
return root
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def fingerprint(root):
|
|
66
|
+
digest = hashlib.sha256()
|
|
67
|
+
for path in sorted((root / "simcord").rglob("*.py")):
|
|
68
|
+
digest.update(str(path.relative_to(root)).encode())
|
|
69
|
+
digest.update(b"\0")
|
|
70
|
+
digest.update(path.read_bytes())
|
|
71
|
+
return digest.hexdigest()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def save_json(path, value):
|
|
75
|
+
with path.open("x") as stream:
|
|
76
|
+
json.dump(value, stream, indent=2)
|
|
77
|
+
stream.write("\n")
|
|
78
|
+
path.chmod(0o444)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def definition(tree, name, owner=None):
|
|
82
|
+
scope = (
|
|
83
|
+
tree.body
|
|
84
|
+
if owner is None
|
|
85
|
+
else next(item.body for item in tree.body if isinstance(item, ast.ClassDef) and item.name == owner)
|
|
86
|
+
)
|
|
87
|
+
return next(
|
|
88
|
+
item
|
|
89
|
+
for item in scope
|
|
90
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)) and item.name == name
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def correctness_overlay(original, candidate, destination):
|
|
95
|
+
shutil.copytree(candidate, destination, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
|
96
|
+
replacements = {
|
|
97
|
+
"preview/_queries.py": [(None, "_message_navigation")],
|
|
98
|
+
"_settlement.py": [(None, "_wakes_recognized_wait")],
|
|
99
|
+
"backend/ops/base.py": [("BackendBase", "_index_message"), ("BackendBase", "_drop_channel_messages")],
|
|
100
|
+
}
|
|
101
|
+
for relative, names in replacements.items():
|
|
102
|
+
target = destination / "simcord" / relative
|
|
103
|
+
lines = target.read_text().splitlines(keepends=True)
|
|
104
|
+
old_text = (original / "simcord" / relative).read_text()
|
|
105
|
+
old_tree, tree = ast.parse(old_text), ast.parse("".join(lines))
|
|
106
|
+
patches = []
|
|
107
|
+
for owner, name in names:
|
|
108
|
+
node, old_node = definition(tree, name, owner), definition(old_tree, name, owner)
|
|
109
|
+
patches.append(
|
|
110
|
+
(
|
|
111
|
+
node.lineno - 1,
|
|
112
|
+
node.end_lineno,
|
|
113
|
+
old_text.splitlines(keepends=True)[old_node.lineno - 1 : old_node.end_lineno],
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
if relative == "backend/ops/base.py":
|
|
117
|
+
for name in ("_message_index_keys", "_remove_message_index"):
|
|
118
|
+
node = definition(tree, name, "BackendBase")
|
|
119
|
+
patches.append((node.lineno - 1, node.end_lineno, []))
|
|
120
|
+
node = definition(tree, "emit", "BackendBase")
|
|
121
|
+
patches.append(
|
|
122
|
+
(
|
|
123
|
+
node.lineno - 1,
|
|
124
|
+
node.end_lineno,
|
|
125
|
+
[
|
|
126
|
+
" def emit(self, event: str, payload: Mapping[str, Any]) -> None:\n",
|
|
127
|
+
" envelope = detach(payload)\n",
|
|
128
|
+
" self.transcript.append(('GATEWAY', event, detach(envelope)))\n",
|
|
129
|
+
" for listener in self.subscribers:\n",
|
|
130
|
+
" listener(event, envelope)\n",
|
|
131
|
+
],
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
for start, end, replacement in sorted(patches, reverse=True):
|
|
135
|
+
lines[start:end] = replacement
|
|
136
|
+
target.write_text("".join(lines))
|
|
137
|
+
target = destination / "simcord/backend/ops/messages.py"
|
|
138
|
+
lines = target.read_text().splitlines(keepends=True)
|
|
139
|
+
method = definition(ast.parse("".join(lines)), "commit_message_edit", "MessageMixin")
|
|
140
|
+
old_id = next(
|
|
141
|
+
node
|
|
142
|
+
for node in method.body
|
|
143
|
+
if isinstance(node, ast.Assign)
|
|
144
|
+
and any(isinstance(t, ast.Name) and t.id == "old_id" for t in node.targets)
|
|
145
|
+
)
|
|
146
|
+
old_keys = next(
|
|
147
|
+
node
|
|
148
|
+
for node in method.body
|
|
149
|
+
if isinstance(node, ast.Assign)
|
|
150
|
+
and any(isinstance(t, ast.Name) and t.id == "old_keys" for t in node.targets)
|
|
151
|
+
)
|
|
152
|
+
reindex = next(
|
|
153
|
+
node
|
|
154
|
+
for node in method.body
|
|
155
|
+
if isinstance(node, ast.If)
|
|
156
|
+
and any(isinstance(n, ast.Name) and n.id == "old_keys" for n in ast.walk(node.test))
|
|
157
|
+
)
|
|
158
|
+
# Refuse a changed layout rather than silently construct a different variant.
|
|
159
|
+
assert old_keys.lineno == old_id.end_lineno + 1
|
|
160
|
+
lines[reindex.lineno - 1 : reindex.end_lineno] = [" self._index_message(message)\n"]
|
|
161
|
+
lines[old_id.lineno - 1 : old_keys.end_lineno] = [" self._index_message(message, remove=True)\n"]
|
|
162
|
+
target.write_text("".join(lines))
|
|
163
|
+
return destination
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def clock():
|
|
167
|
+
return time.perf_counter(), time.process_time()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def elapsed(start):
|
|
171
|
+
return {"wall_s": time.perf_counter() - start[0], "cpu_s": time.process_time() - start[1]}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
async def operations(count, rich, discord, simcord):
|
|
175
|
+
seen = []
|
|
176
|
+
|
|
177
|
+
class Bot(discord.Client):
|
|
178
|
+
async def on_message(self, message):
|
|
179
|
+
if message.author.bot:
|
|
180
|
+
return
|
|
181
|
+
index = int(message.content.split()[1])
|
|
182
|
+
asyncio.get_running_loop().call_soon(seen.append, index)
|
|
183
|
+
await asyncio.sleep(0)
|
|
184
|
+
kwargs = {}
|
|
185
|
+
if rich:
|
|
186
|
+
embed = discord.Embed(title=f"card {index}", description="rich payload " * 12)
|
|
187
|
+
embed.add_field(name="field", value="nested value")
|
|
188
|
+
view = discord.ui.View(timeout=None)
|
|
189
|
+
view.add_item(discord.ui.Button(label="Button", custom_id=f"button:{index}"))
|
|
190
|
+
kwargs = {"embed": embed, "view": view}
|
|
191
|
+
await message.reply(f"reply {index}", **kwargs)
|
|
192
|
+
|
|
193
|
+
setup = clock()
|
|
194
|
+
async with simcord.run(Bot(intents=discord.Intents.all())) as env:
|
|
195
|
+
guild = env.create_guild("isolated")
|
|
196
|
+
channel = guild.create_text_channel("general")
|
|
197
|
+
actor = guild.add_member(env.create_user("alice"))
|
|
198
|
+
setup_time = elapsed(setup)
|
|
199
|
+
start = clock()
|
|
200
|
+
for index in range(count):
|
|
201
|
+
await actor.send(channel, f"ping {index}")
|
|
202
|
+
assert seen == list(range(index + 1))
|
|
203
|
+
last = channel.last_message
|
|
204
|
+
assert last is not None and last.content == f"reply {index}"
|
|
205
|
+
assert last.reference is not None
|
|
206
|
+
if rich:
|
|
207
|
+
assert last.embeds[0].title == f"card {index}"
|
|
208
|
+
assert last.components[0].children[0].custom_id == f"button:{index}"
|
|
209
|
+
await last.edit(content=f"edited {index}")
|
|
210
|
+
assert channel.last_message.content == f"edited {index}"
|
|
211
|
+
history = channel.history()
|
|
212
|
+
assert [m.content for m in history] == [s for i in range(count) for s in (f"ping {i}", f"edited {i}")]
|
|
213
|
+
assert not env.errors
|
|
214
|
+
work_time = elapsed(start)
|
|
215
|
+
result = {
|
|
216
|
+
"messages": len(history),
|
|
217
|
+
"callbacks": len(seen),
|
|
218
|
+
"edits": count,
|
|
219
|
+
"virtual_s": env.backend.clock.elapsed,
|
|
220
|
+
"setup": setup_time,
|
|
221
|
+
"operations": work_time,
|
|
222
|
+
}
|
|
223
|
+
if tracemalloc.is_tracing():
|
|
224
|
+
current, peak = tracemalloc.get_traced_memory()
|
|
225
|
+
result["live_world_allocation"] = {"retained_bytes": current, "peak_bytes": peak}
|
|
226
|
+
if rich:
|
|
227
|
+
history[-1].embeds[0].to_dict()["fields"][0]["value"] = "SDK mutation"
|
|
228
|
+
result["sdk_isolated"] = (
|
|
229
|
+
channel.last_message.embeds[0].fields[0].value == "nested value"
|
|
230
|
+
and env.backend.get_message(channel.id, last.id).embeds[0]["fields"][0]["value"]
|
|
231
|
+
== "nested value"
|
|
232
|
+
)
|
|
233
|
+
return result
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
async def search(count, discord, simcord):
|
|
237
|
+
from simcord.preview._queries import _message_navigation
|
|
238
|
+
|
|
239
|
+
setup = clock()
|
|
240
|
+
async with simcord.run(discord.Client(intents=discord.Intents.all())) as env:
|
|
241
|
+
guild = env.create_guild("search")
|
|
242
|
+
channel = guild.create_text_channel("general")
|
|
243
|
+
actor = guild.add_member(env.create_user("alice"))
|
|
244
|
+
native = env.bot.get_channel(channel.id)
|
|
245
|
+
messages = [await native.send(f"needle {index:04} Café ||hiddenmarker||") for index in range(count)]
|
|
246
|
+
async with env.preview(channel, viewers=[actor]) as preview:
|
|
247
|
+
page = preview._open_page(actor.id)
|
|
248
|
+
setup_time = elapsed(setup)
|
|
249
|
+
start = clock()
|
|
250
|
+
results = {}
|
|
251
|
+
for query in ("needle", "absentmarker", "café", "hiddenmarker"):
|
|
252
|
+
page.navigation_query = query
|
|
253
|
+
page.navigation_cursor = None
|
|
254
|
+
ids, pages = [], 0
|
|
255
|
+
query_start = clock()
|
|
256
|
+
first_page = None
|
|
257
|
+
while True:
|
|
258
|
+
rows, nav = _message_navigation(preview, page)
|
|
259
|
+
if first_page is None:
|
|
260
|
+
first_page = elapsed(query_start)
|
|
261
|
+
ids.extend(int(row["id"]) for row in rows)
|
|
262
|
+
pages += 1
|
|
263
|
+
if not nav["hasNext"]:
|
|
264
|
+
assert nav["nextCursor"] is None
|
|
265
|
+
break
|
|
266
|
+
assert nav["nextCursor"]
|
|
267
|
+
page.navigation_cursor = nav["nextCursor"]
|
|
268
|
+
expected = [m.id for m in messages] if query in ("needle", "café") else []
|
|
269
|
+
assert ids == expected
|
|
270
|
+
results[query] = {
|
|
271
|
+
"matches": len(ids),
|
|
272
|
+
"pages": pages,
|
|
273
|
+
"first_page": first_page,
|
|
274
|
+
"all_pages": elapsed(query_start),
|
|
275
|
+
}
|
|
276
|
+
assert not env.errors
|
|
277
|
+
return {"queries": results, "setup": setup_time, "operations": elapsed(start)}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
async def settlement(count, discord, simcord):
|
|
281
|
+
seen, roots = [], []
|
|
282
|
+
|
|
283
|
+
class Bot(discord.Client):
|
|
284
|
+
async def on_message(self, message):
|
|
285
|
+
if message.content == "park":
|
|
286
|
+
roots.extend(asyncio.create_task(self.wait_for("release")) for _ in range(count))
|
|
287
|
+
elif message.content == "go":
|
|
288
|
+
loop = asyncio.get_running_loop()
|
|
289
|
+
for index in range(128):
|
|
290
|
+
loop.call_later(0.02, seen.append, index)
|
|
291
|
+
|
|
292
|
+
setup = clock()
|
|
293
|
+
async with simcord.run(Bot(intents=discord.Intents.all())) as env:
|
|
294
|
+
guild = env.create_guild("settlement")
|
|
295
|
+
channel = guild.create_text_channel("timers")
|
|
296
|
+
actor = guild.add_member(env.create_user("alice"))
|
|
297
|
+
await actor.send(channel, "park")
|
|
298
|
+
setup_time = elapsed(setup)
|
|
299
|
+
start = clock()
|
|
300
|
+
await actor.send(channel, "go")
|
|
301
|
+
operation_time = elapsed(start)
|
|
302
|
+
assert seen == list(range(128))
|
|
303
|
+
assert all(not root.done() for root in roots)
|
|
304
|
+
env.bot.dispatch("release", "ok")
|
|
305
|
+
await env.settle()
|
|
306
|
+
assert all(root.result() == "ok" for root in roots)
|
|
307
|
+
assert not env.errors
|
|
308
|
+
return {"callbacks": len(seen), "released": len(roots), "setup": setup_time, "operations": operation_time}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def ancestry(depth):
|
|
312
|
+
from simcord.backend import Backend
|
|
313
|
+
|
|
314
|
+
setup = clock()
|
|
315
|
+
backend = Backend()
|
|
316
|
+
guild = backend.create_guild("depth")
|
|
317
|
+
channel = backend.create_channel(guild.id, "depth")
|
|
318
|
+
previous = None
|
|
319
|
+
for index in range(depth):
|
|
320
|
+
previous = backend.create_message(
|
|
321
|
+
channel.id,
|
|
322
|
+
backend.bot_user.id,
|
|
323
|
+
str(index),
|
|
324
|
+
broadcast=False,
|
|
325
|
+
reference={"message_id": str(previous.id)} if previous is not None else None,
|
|
326
|
+
)
|
|
327
|
+
setup_time = elapsed(setup)
|
|
328
|
+
assert not backend.subscribers
|
|
329
|
+
start = clock()
|
|
330
|
+
backend.announce_message_create(previous)
|
|
331
|
+
payload = backend.transcript[-1][2]
|
|
332
|
+
for index in reversed(range(depth)):
|
|
333
|
+
assert payload["content"] == str(index)
|
|
334
|
+
payload = payload.get("referenced_message")
|
|
335
|
+
assert payload is None
|
|
336
|
+
return {
|
|
337
|
+
"depth": depth,
|
|
338
|
+
"retained_nodes": depth,
|
|
339
|
+
"subscribers": 0,
|
|
340
|
+
"setup": setup_time,
|
|
341
|
+
"operations": elapsed(start),
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def ownership():
|
|
346
|
+
from simcord.backend import Backend
|
|
347
|
+
|
|
348
|
+
backend = Backend()
|
|
349
|
+
guild = backend.create_guild("ownership")
|
|
350
|
+
channel = backend.create_channel(guild.id, "ownership")
|
|
351
|
+
embeds = [{"title": "original", "fields": [{"name": "n", "value": "v"}]}]
|
|
352
|
+
message = backend.create_message(channel.id, backend.bot_user.id, "x", embeds=embeds)
|
|
353
|
+
embeds[0]["fields"][0]["value"] = "input mutation"
|
|
354
|
+
input_isolated = message.embeds[0]["fields"][0]["value"] == "v"
|
|
355
|
+
live = []
|
|
356
|
+
backend.subscribers.append(lambda event, payload: live.append(payload))
|
|
357
|
+
payload = {"nested": [{"value": "before"}]}
|
|
358
|
+
backend.emit("OWNERSHIP_PROBE", payload)
|
|
359
|
+
retained = backend.transcript[-1][2]
|
|
360
|
+
live[0]["nested"][0]["value"] = "subscriber mutation"
|
|
361
|
+
return {
|
|
362
|
+
"input_isolated": input_isolated,
|
|
363
|
+
"canonical_isolated": payload["nested"][0]["value"] == "before",
|
|
364
|
+
"history_isolated": retained["nested"][0]["value"] == "before",
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def worker(args):
|
|
369
|
+
root = source_root(args.source)
|
|
370
|
+
sys.path.insert(0, str(root))
|
|
371
|
+
start = clock()
|
|
372
|
+
import discord
|
|
373
|
+
|
|
374
|
+
import simcord
|
|
375
|
+
|
|
376
|
+
import_time = elapsed(start)
|
|
377
|
+
assert Path(simcord.__file__).resolve().is_relative_to(root)
|
|
378
|
+
result = {
|
|
379
|
+
"source": str(root),
|
|
380
|
+
"imported_source": simcord.__file__,
|
|
381
|
+
"source_sha256": fingerprint(root),
|
|
382
|
+
"variant": args.variant,
|
|
383
|
+
"construction": CONSTRUCTION[args.variant],
|
|
384
|
+
"python": sys.version,
|
|
385
|
+
"executable": sys.executable,
|
|
386
|
+
"platform": platform.platform(),
|
|
387
|
+
"dependencies": {
|
|
388
|
+
p: importlib.metadata.version(p) for p in ("discord.py", "sortedcontainers", "aiohttp", "attrs")
|
|
389
|
+
},
|
|
390
|
+
"import": import_time,
|
|
391
|
+
"recursion_limit": sys.getrecursionlimit(),
|
|
392
|
+
"case": args.case,
|
|
393
|
+
"instrument": args.instrument,
|
|
394
|
+
}
|
|
395
|
+
try:
|
|
396
|
+
result["ownership"] = ownership()
|
|
397
|
+
if args.variant in ("candidate", "correctness-only"):
|
|
398
|
+
assert all(result["ownership"].values()), result["ownership"]
|
|
399
|
+
gc.collect()
|
|
400
|
+
profiler = cProfile.Profile() if args.instrument == "profile" else None
|
|
401
|
+
if args.instrument == "allocation":
|
|
402
|
+
tracemalloc.start()
|
|
403
|
+
if profiler:
|
|
404
|
+
profiler.enable()
|
|
405
|
+
start = clock()
|
|
406
|
+
if args.case == "ancestry":
|
|
407
|
+
completed = ancestry(20 if args.smoke else 750)
|
|
408
|
+
elif args.case == "search":
|
|
409
|
+
completed = asyncio.run(search(101 if args.smoke else 401, discord, simcord))
|
|
410
|
+
elif args.case == "settlement":
|
|
411
|
+
completed = asyncio.run(settlement(3 if args.smoke else 400, discord, simcord))
|
|
412
|
+
else:
|
|
413
|
+
completed = asyncio.run(
|
|
414
|
+
operations(3 if args.smoke else 100, args.case == "rich", discord, simcord)
|
|
415
|
+
)
|
|
416
|
+
if args.case == "rich" and args.variant in ("candidate", "correctness-only"):
|
|
417
|
+
assert completed["sdk_isolated"], completed
|
|
418
|
+
result.update({"total": elapsed(start), "completed": completed, "status": "ok"})
|
|
419
|
+
except Exception:
|
|
420
|
+
result.update({"status": "error", "error": traceback.format_exc()})
|
|
421
|
+
finally:
|
|
422
|
+
if args.instrument == "allocation" and tracemalloc.is_tracing():
|
|
423
|
+
current, peak = tracemalloc.get_traced_memory()
|
|
424
|
+
result["allocation"] = {"retained_bytes_before_gc": current, "peak_bytes": peak}
|
|
425
|
+
gc.collect()
|
|
426
|
+
result["allocation"]["retained_bytes_after_gc"] = tracemalloc.get_traced_memory()[0]
|
|
427
|
+
tracemalloc.stop()
|
|
428
|
+
if args.instrument == "profile" and "profiler" in locals() and profiler:
|
|
429
|
+
profiler.disable()
|
|
430
|
+
profile_path = args.result.with_suffix(".prof")
|
|
431
|
+
with profile_path.open("xb") as stream:
|
|
432
|
+
import marshal
|
|
433
|
+
|
|
434
|
+
profiler.create_stats()
|
|
435
|
+
marshal.dump(profiler.stats, stream)
|
|
436
|
+
profile_path.chmod(0o444)
|
|
437
|
+
result["profile"] = str(profile_path)
|
|
438
|
+
save_json(args.result, result)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def main():
|
|
442
|
+
parser = argparse.ArgumentParser(
|
|
443
|
+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
|
444
|
+
)
|
|
445
|
+
parser.add_argument("--original", type=Path)
|
|
446
|
+
parser.add_argument("--before", type=Path)
|
|
447
|
+
parser.add_argument("--candidate", type=Path, default=Path(__file__).resolve().parents[1])
|
|
448
|
+
parser.add_argument("--output", type=Path)
|
|
449
|
+
parser.add_argument("--rounds", type=int, default=9)
|
|
450
|
+
parser.add_argument("--seed", type=int, default=20261029)
|
|
451
|
+
parser.add_argument("--smoke", action="store_true")
|
|
452
|
+
parser.add_argument("--instrument", choices=("timing", "allocation", "profile"), default="timing")
|
|
453
|
+
parser.add_argument(
|
|
454
|
+
"--cases",
|
|
455
|
+
nargs="+",
|
|
456
|
+
choices=("plain", "rich", "ancestry", "search", "settlement"),
|
|
457
|
+
default=["plain", "rich", "ancestry", "search", "settlement"],
|
|
458
|
+
)
|
|
459
|
+
parser.add_argument("--variant", choices=tuple(CONSTRUCTION), help=argparse.SUPPRESS)
|
|
460
|
+
parser.add_argument("--source", type=Path, help=argparse.SUPPRESS)
|
|
461
|
+
parser.add_argument("--result", type=Path, help=argparse.SUPPRESS)
|
|
462
|
+
parser.add_argument("--case", help=argparse.SUPPRESS)
|
|
463
|
+
args = parser.parse_args()
|
|
464
|
+
if args.source:
|
|
465
|
+
worker(args)
|
|
466
|
+
return
|
|
467
|
+
if args.original is None or args.output is None or args.rounds < 1:
|
|
468
|
+
parser.error("--original and --output required; --rounds must be positive")
|
|
469
|
+
original, candidate = source_root(args.original), source_root(args.candidate)
|
|
470
|
+
args.output.mkdir(parents=True, exist_ok=False)
|
|
471
|
+
variants = {
|
|
472
|
+
"original0083422": original,
|
|
473
|
+
"correctness-only": correctness_overlay(original, candidate, args.output / "correctness-only-src"),
|
|
474
|
+
"candidate": candidate,
|
|
475
|
+
}
|
|
476
|
+
if args.before:
|
|
477
|
+
variants["bd21e55"] = source_root(args.before)
|
|
478
|
+
hashes = {name: fingerprint(root) for name, root in variants.items()}
|
|
479
|
+
manifest = {
|
|
480
|
+
"sources": {
|
|
481
|
+
name: {"path": str(root), "sha256": hashes[name], "construction": CONSTRUCTION[name]}
|
|
482
|
+
for name, root in variants.items()
|
|
483
|
+
},
|
|
484
|
+
"seed": args.seed,
|
|
485
|
+
"rounds": 1 if args.smoke else args.rounds,
|
|
486
|
+
"instrument": args.instrument,
|
|
487
|
+
"smoke": args.smoke,
|
|
488
|
+
"scope": "Fresh interpreter per sample. Initial discord/simcord imports timed separately. "
|
|
489
|
+
"Total includes lazy imports, asyncio.run, Env setup, operations, validation and teardown; "
|
|
490
|
+
"setup and operation spans also reported. "
|
|
491
|
+
"Parent process launch time excluded. Ownership probe and pre-sample gc excluded. "
|
|
492
|
+
"Allocation/profile are separate modes, never combined with clean timing. "
|
|
493
|
+
"Allocation covers complete workloads, including setup; worker retained bytes are "
|
|
494
|
+
"post-teardown; plain/rich also record live-world retained and peak before SDK mutation. "
|
|
495
|
+
"Search calls actual signed-cursor navigation without HTTP.",
|
|
496
|
+
"order": [],
|
|
497
|
+
"samples": [],
|
|
498
|
+
}
|
|
499
|
+
save_json(args.output / "construction.json", manifest)
|
|
500
|
+
rng = random.Random(args.seed)
|
|
501
|
+
for round_number in range(manifest["rounds"]):
|
|
502
|
+
order = list(variants)
|
|
503
|
+
rng.shuffle(order)
|
|
504
|
+
# Balanced randomized ABBA-style batches: ABCD, DCBA, reshuffled next round.
|
|
505
|
+
for name in order + order[::-1]:
|
|
506
|
+
cases = list(args.cases)
|
|
507
|
+
rng.shuffle(cases)
|
|
508
|
+
for case in cases:
|
|
509
|
+
sample_id = f"{len(manifest['samples']):04}-{name}-{case}"
|
|
510
|
+
result_path = args.output / f"{sample_id}.json"
|
|
511
|
+
command = [
|
|
512
|
+
sys.executable,
|
|
513
|
+
str(Path(__file__).resolve()),
|
|
514
|
+
"--source",
|
|
515
|
+
str(variants[name]),
|
|
516
|
+
"--variant",
|
|
517
|
+
name,
|
|
518
|
+
"--case",
|
|
519
|
+
case,
|
|
520
|
+
"--result",
|
|
521
|
+
str(result_path),
|
|
522
|
+
"--instrument",
|
|
523
|
+
args.instrument,
|
|
524
|
+
]
|
|
525
|
+
if args.smoke:
|
|
526
|
+
command.append("--smoke")
|
|
527
|
+
process = subprocess.run(
|
|
528
|
+
command, capture_output=True, text=True, env={**os.environ, "PYTHONHASHSEED": "0"}
|
|
529
|
+
)
|
|
530
|
+
save_json(
|
|
531
|
+
args.output / f"{sample_id}.process.json",
|
|
532
|
+
{
|
|
533
|
+
"command": command,
|
|
534
|
+
"returncode": process.returncode,
|
|
535
|
+
"stdout": process.stdout,
|
|
536
|
+
"stderr": process.stderr,
|
|
537
|
+
},
|
|
538
|
+
)
|
|
539
|
+
if not result_path.exists():
|
|
540
|
+
raise RuntimeError(f"Worker did not produce {result_path}; inspect process JSON")
|
|
541
|
+
row = json.loads(result_path.read_text())
|
|
542
|
+
if row["source_sha256"] != hashes[name]:
|
|
543
|
+
raise RuntimeError(f"Source changed during measurement: {name}")
|
|
544
|
+
manifest["order"].append({"round": round_number, "variant": name, "case": case})
|
|
545
|
+
manifest["samples"].append(
|
|
546
|
+
{
|
|
547
|
+
"file": result_path.name,
|
|
548
|
+
"variant": name,
|
|
549
|
+
"case": case,
|
|
550
|
+
"status": row["status"],
|
|
551
|
+
"total": row.get("total"),
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
manifest["all_completed"] = all(row["status"] == "ok" for row in manifest["samples"])
|
|
555
|
+
save_json(args.output / "report.json", manifest)
|
|
556
|
+
print(
|
|
557
|
+
json.dumps(
|
|
558
|
+
{
|
|
559
|
+
"report": str(args.output / "report.json"),
|
|
560
|
+
"all_completed": manifest["all_completed"],
|
|
561
|
+
"samples": len(manifest["samples"]),
|
|
562
|
+
}
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
if not manifest["all_completed"]:
|
|
566
|
+
raise SystemExit(1)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
if __name__ == "__main__":
|
|
570
|
+
main()
|