narrativetrace 0.4.2__tar.gz → 0.5.0__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.
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/LICENSE +2 -2
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/PKG-INFO +1 -1
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/pyproject.toml +1 -1
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/__init__.py +1 -1
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/canonical.py +3 -2
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/context.py +14 -8
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/events.py +26 -4
- narrativetrace-0.5.0/src/narrativetrace/exception_message.py +46 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/export.py +2 -1
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/logging_bridge.py +2 -1
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/pipeline/perishable.py +43 -20
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/indented.py +3 -2
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/markdown.py +3 -2
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/prose.py +3 -2
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/rendering.py +222 -33
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/trace_object.py +1 -2
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/tree_canonical.py +3 -2
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_context.py +20 -2
- narrativetrace-0.5.0/tests/test_exception_message.py +47 -0
- narrativetrace-0.5.0/tests/test_exception_message_sinks.py +114 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_exclusions.py +15 -2
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_exclusions_docs.py +14 -2
- narrativetrace-0.5.0/tests/test_group_event_ownership.py +83 -0
- narrativetrace-0.5.0/tests/test_no_fixed_depth_repo_paths.py +45 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_no_poison_contract.py +8 -10
- narrativetrace-0.5.0/tests/test_pipeline_perishable.py +199 -0
- narrativetrace-0.5.0/tests/test_render_paths_agree_on_origin.py +235 -0
- narrativetrace-0.5.0/tests/test_render_shared_subgraph_bound.py +264 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_smoke.py +1 -1
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_trace_object.py +4 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_verify_all_exec.py +1 -2
- narrativetrace-0.4.2/tests/test_pipeline_perishable.py +0 -77
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/.gitignore +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/README.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_boundary.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_generated.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/agents/add-narrative-tracing/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/agents/add-narrativetrace-clarity/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/agents/narrativetrace-debug/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/agents/narrativetrace-doctor/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/agents/narrativetrace-explain/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/agents/narrativetrace-feedback/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/agents/narrativetrace-verify/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/catalogue.json +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/claude/add-narrative-tracing/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/claude/add-narrativetrace-clarity/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/claude/narrativetrace-debug/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/claude/narrativetrace-doctor/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/claude/narrativetrace-explain/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/claude/narrativetrace-feedback/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/_skills/claude/narrativetrace-verify/SKILL.md +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/chapter.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/cli.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/concurrency.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/config.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/context_export.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/decorators.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/doctor/__init__.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/doctor/cli_bin.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/doctor/environment.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/doctor/feedback_cli.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/doctor/gh_auth_probe.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/escape.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/exclusions.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/groups.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/identity.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/ids.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/levels.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/loss.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/markers.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/metadata.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/namer.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/national_id_shapes.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/nodes.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/outcomes.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/output/__init__.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/output/approval.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/output/artifact_identity.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/output/line_diff.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/output/manifest.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/output/paths.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/output/reporter.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/output/run_identity.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/output/structural_delta.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/output/warnings.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/output/writer.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/pipeline/__init__.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/pipeline/bounded_buffer.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/pipeline/buffered_consumer.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/pipeline/dual_path.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/pipeline/event_store.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/pipeline/subscriber.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/py.typed +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/redacted_paths.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/redaction.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/__init__.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/base.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/camel.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/concurrency.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/frontmatter.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/scenario.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/scenario_result.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/span_id.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/structural.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/value_delta.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/render/value_reference.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/secret_value_shapes.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/signature.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/span.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/template.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/tree.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/tree_walk.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/src/narrativetrace/values.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/conftest.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/doctor/test_adversarial_feedback_cli.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/doctor/test_cli_built_wheels.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/doctor/test_cli_feedback_arguments.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/doctor/test_cli_feedback_verb.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/doctor/test_cli_installer_arguments.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/doctor/test_cli_installer_verbs.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/doctor/test_doctor_cli_bin.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/doctor/test_doctor_environment.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/doctor/test_gh_auth_probe.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/fixtures/java-negative-expense.nt +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/live_annotation_support.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/logging_wiring.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/stress_support.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_adversarial_openai_plugin_pack.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_adversarial_skills_render.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_adversarial_vendor_validation.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_adversarial_verify_all_heavy.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_approval.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_artifact_identity.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_async_concurrency_kind.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_async_lifecycle_contract.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_async_snapshot_adoption.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_bench_capture.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_bench_redaction.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_bench_rendering.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_canonical.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_canonical_props.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_capture_identity.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_chapter.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_chapter_props.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_check_no_license_headers.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_cli.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_comment_hygiene.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_concurrency_info.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_config.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_console_delta.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_context7_check.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_context_export.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_context_extra.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_context_stress.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_contract_check.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_contract_dispatch_coverage.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_contract_lint.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_coverage_source_completeness.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_decorators.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_deep_fixture_budget.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_distribution_licensing.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_duplication_check.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_duplication_report.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_errors_only_level.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_escape.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_events.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_export.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_feedback_docs.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_framework_table_docs.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_fuzz_report.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_gate_lint_never_trusts_a_cache.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_generated_targets.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_groups.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_identity.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_identity_conformance.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_ids.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_installer_from_another_package.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_issue_form_publishes.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_levels.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_live_snapshot_scope_visibility.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_logging_bridge.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_logging_wiring.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_manifest.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_metadata.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_mutation_accounting.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_mutation_baseline_selection.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_mutation_gate.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_mutation_tooling.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_mutation_workers.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_namer.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_national_id_shapes.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_nested_chain_adoption.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_nodes.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_notice_render_targets.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_openai_plugin_pack.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_outcomes.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_output.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_parameter_name_redaction.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_parameter_value_redaction.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_pipeline_buffered.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_pipeline_dual_path.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_pipeline_props.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_pipeline_stress.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_pipeline_subscriber.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_platform_pointer_from_another_package.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_platform_table_docs.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_promotion_render.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_publish_agents_md_section.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_publish_public.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_publish_public_host_venv.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_publish_workflow_packages.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_quality_gate_status.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_redacted_paths.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_redacted_value_containment.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_redaction.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_render.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_render_reads_state.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_render_reentrancy.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_rendering.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_rendering_extra.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_run_identity.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_run_security_tool.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_scenario_result.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_secret_value_shapes.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_signature.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_skills_carrier_wheel_contents.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_skills_render.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_snippet_check.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_span.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_span_id.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_span_id_flavours.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_span_id_props.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_structlog_dependency.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_structural.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_structural_conformance.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_structural_delta.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_template.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_timeout_floor.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_trace_loss.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_trace_object_catch_all_warning.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_trace_stack_live_child.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_translation_check.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_tree.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_tree_canonical.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_tree_props.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_tree_walk.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_tree_writes_guard.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_value_delta.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_value_reference.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_values.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_vendor_validation.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_verify_all.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_verify_all_arch_stress.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_verify_all_heavy.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_verify_all_schema.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_verify_all_security.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_verify_all_static.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_verify_all_testrun.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_verify_publication_packages.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_verify_publication_provenance.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_verify_publication_registry.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_version_guard_ignores_a_stale_installed_carrier.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_version_literals.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_version_literals_props.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/test_writer_structural.py +0 -0
- {narrativetrace-0.4.2 → narrativetrace-0.5.0}/tests/wheel_support.py +0 -0
|
@@ -7,7 +7,7 @@ Parameters
|
|
|
7
7
|
|
|
8
8
|
Licensor: Empower Agile
|
|
9
9
|
|
|
10
|
-
Licensed Work: NarrativeTrace for Python (the narrativetrace PyPI distributions) version 0.
|
|
10
|
+
Licensed Work: NarrativeTrace for Python (the narrativetrace PyPI distributions) version 0.5.0. The Licensed Work is (c) 2026 Empower Agile.
|
|
11
11
|
|
|
12
12
|
Additional Use Grant: You may make production use of the Licensed Work for any
|
|
13
13
|
purpose, including internal use and use in products and
|
|
@@ -18,7 +18,7 @@ Additional Use Grant: You may make production use of the Licensed Work for any
|
|
|
18
18
|
parties as a logging, tracing, or code-narrative product
|
|
19
19
|
or service.
|
|
20
20
|
|
|
21
|
-
Change Date: Four years from the date the Licensed Work is published. (2030-10-
|
|
21
|
+
Change Date: Four years from the date the Licensed Work is published. (2030-10-24 for this version)
|
|
22
22
|
|
|
23
23
|
Change License: Apache License, Version 2.0
|
|
24
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: narrativetrace
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Narrative execution tracing for Python — human- and LLM-readable traces
|
|
5
5
|
Project-URL: Homepage, https://github.com/narrativetrace/narrativetrace-python
|
|
6
6
|
Project-URL: Source, https://github.com/narrativetrace/narrativetrace-python
|
|
@@ -20,6 +20,7 @@ import time
|
|
|
20
20
|
from dataclasses import dataclass, replace
|
|
21
21
|
from datetime import UTC, datetime
|
|
22
22
|
|
|
23
|
+
from narrativetrace import exception_message
|
|
23
24
|
from narrativetrace.concurrency import ThreadIdentity
|
|
24
25
|
from narrativetrace.events import (
|
|
25
26
|
EnterEvent,
|
|
@@ -252,7 +253,7 @@ def _split_span_name(span_name: str | None) -> tuple[str, str]:
|
|
|
252
253
|
|
|
253
254
|
def _exit_message(span: SpanContext, outcome: TraceOutcome) -> str:
|
|
254
255
|
if isinstance(outcome, Threw):
|
|
255
|
-
return f"!! {type(outcome.exception).__name__}: {outcome.exception}"
|
|
256
|
+
return f"!! {type(outcome.exception).__name__}: {exception_message.text(outcome.exception)}"
|
|
256
257
|
name = span.span_name if span.span_name is not None else "method"
|
|
257
258
|
if isinstance(outcome, Returned) and outcome.rendered_value is not None:
|
|
258
259
|
return f"← {name} returned {outcome.rendered_value}"
|
|
@@ -286,7 +287,7 @@ def _from_exit_event(event: ExitEvent, anchor: MonotonicAnchor) -> CanonicalEntr
|
|
|
286
287
|
nt_outcome=_exit_outcome(outcome),
|
|
287
288
|
nt_return_value=returned.rendered_value if returned is not None else None,
|
|
288
289
|
exception_type=type(threw.exception).__name__ if threw is not None else None,
|
|
289
|
-
exception_message=
|
|
290
|
+
exception_message=exception_message.of(threw.exception) if threw is not None else None,
|
|
290
291
|
nt_exception_package=_package_of(threw.exception) if threw is not None else None,
|
|
291
292
|
)
|
|
292
293
|
return _with_correlation(entry, span)
|
|
@@ -227,7 +227,7 @@ class NarrativeContext(ABC):
|
|
|
227
227
|
self,
|
|
228
228
|
trace_id: TraceId,
|
|
229
229
|
remote_parent_span_id: SpanId | None = None,
|
|
230
|
-
trace_flags: int =
|
|
230
|
+
trace_flags: int | None = None,
|
|
231
231
|
) -> None:
|
|
232
232
|
"""Adopts an inbound (e.g. W3C ``traceparent``) trace id for the current scope.
|
|
233
233
|
|
|
@@ -236,7 +236,9 @@ class NarrativeContext(ABC):
|
|
|
236
236
|
``parent_span_id``, so a consumer that follows parent ids links this service's trace to the
|
|
237
237
|
caller's; it never appears as a span in this scope's own tree. ``trace_flags`` is the
|
|
238
238
|
``traceparent`` flags byte (bit 0 = sampled); every span this scope creates carries it as
|
|
239
|
-
:attr:`~narrativetrace.span.SpanContext.trace_flags`.
|
|
239
|
+
:attr:`~narrativetrace.span.SpanContext.trace_flags`. ``None`` (the default, a bare id with
|
|
240
|
+
no ``traceparent``) keeps the scope's current flags, sampled unless a flags byte was adopted
|
|
241
|
+
earlier; only an explicit value overwrites them.
|
|
240
242
|
"""
|
|
241
243
|
|
|
242
244
|
def story_id(self) -> str | None:
|
|
@@ -276,6 +278,9 @@ class _RequestMetadata:
|
|
|
276
278
|
|
|
277
279
|
_EMPTY_METADATA = _RequestMetadata()
|
|
278
280
|
|
|
281
|
+
# W3C trace-flags bit 0. A fresh root is sampled; only an adopted ``traceparent`` overwrites it.
|
|
282
|
+
_SAMPLED = 1
|
|
283
|
+
|
|
279
284
|
MAX_ADOPTED_SPANS = 10_000
|
|
280
285
|
"""Ceiling on spans one stack accepts from worker scopes, and on its live-child registrations.
|
|
281
286
|
|
|
@@ -304,7 +309,7 @@ class _TraceStack:
|
|
|
304
309
|
scoped_parent: SpanId | None = None
|
|
305
310
|
snapshot_parent: SpanId | None = None
|
|
306
311
|
remote_parent: SpanId | None = None
|
|
307
|
-
trace_flags: int =
|
|
312
|
+
trace_flags: int = _SAMPLED
|
|
308
313
|
story_id: str | None = None
|
|
309
314
|
chapter_id: str | None = None
|
|
310
315
|
metadata: _RequestMetadata = _EMPTY_METADATA
|
|
@@ -773,12 +778,13 @@ class ContextVarNarrativeContext(NarrativeContext):
|
|
|
773
778
|
self,
|
|
774
779
|
trace_id: TraceId,
|
|
775
780
|
remote_parent_span_id: SpanId | None = None,
|
|
776
|
-
trace_flags: int =
|
|
781
|
+
trace_flags: int | None = None,
|
|
777
782
|
) -> None:
|
|
778
783
|
stack = self._get_stack()
|
|
779
784
|
stack.trace_id = trace_id
|
|
780
785
|
stack.remote_parent = remote_parent_span_id
|
|
781
|
-
|
|
786
|
+
if trace_flags is not None:
|
|
787
|
+
stack.trace_flags = trace_flags
|
|
782
788
|
|
|
783
789
|
def story_id(self) -> str | None:
|
|
784
790
|
return self._get_stack().story_id
|
|
@@ -788,13 +794,13 @@ class ContextVarNarrativeContext(NarrativeContext):
|
|
|
788
794
|
|
|
789
795
|
# -- concurrency lifecycle --------------------------------------------- #
|
|
790
796
|
def on_fork_created(self, group_id: str) -> None:
|
|
791
|
-
self._store.add(ForkCreatedEvent(group_id, _now_nanos()))
|
|
797
|
+
self._store.add(ForkCreatedEvent(group_id, _now_nanos(), self.current_span_id()))
|
|
792
798
|
|
|
793
799
|
def on_merge(self, group_id: str, members: list[TraceNode]) -> None:
|
|
794
|
-
self._store.add(MergeEvent(group_id, len(members), _now_nanos()))
|
|
800
|
+
self._store.add(MergeEvent(group_id, len(members), _now_nanos(), self.current_span_id()))
|
|
795
801
|
|
|
796
802
|
def on_fire_and_forget_launched(self, group_id: str) -> None:
|
|
797
|
-
self._store.add(FireAndForgetEvent(group_id, _now_nanos()))
|
|
803
|
+
self._store.add(FireAndForgetEvent(group_id, _now_nanos(), self.current_span_id()))
|
|
798
804
|
|
|
799
805
|
def emit_trace_node(self, node: TraceNode, parent_span_id: SpanId | None) -> None:
|
|
800
806
|
self._emit_trace_node(node, parent_span_id, TreeWalk())
|
|
@@ -53,31 +53,53 @@ class ExitEvent(TraceEvent):
|
|
|
53
53
|
|
|
54
54
|
@dataclass(frozen=True, slots=True)
|
|
55
55
|
class ForkCreatedEvent(TraceEvent):
|
|
56
|
-
"""Lifecycle marker: a fork group was created.
|
|
56
|
+
"""Lifecycle marker: a fork group was created.
|
|
57
|
+
|
|
58
|
+
``owner_span_id`` is the span current on the launching thread when the group was created
|
|
59
|
+
(B-166); ``None`` for a group created outside every span, which no request ever removes.
|
|
60
|
+
"""
|
|
57
61
|
|
|
58
62
|
group_id: str
|
|
59
63
|
timestamp_nanos: int
|
|
64
|
+
owner_span_id: SpanId | None = None
|
|
60
65
|
|
|
61
66
|
|
|
62
67
|
@dataclass(frozen=True, slots=True)
|
|
63
68
|
class MergeEvent(TraceEvent):
|
|
64
|
-
"""Lifecycle marker: a fork group merged its collected children.
|
|
69
|
+
"""Lifecycle marker: a fork group merged its collected children.
|
|
70
|
+
|
|
71
|
+
``owner_span_id`` is the span current on the merging (launching) thread (B-166); ``None``
|
|
72
|
+
outside every span.
|
|
73
|
+
"""
|
|
65
74
|
|
|
66
75
|
group_id: str
|
|
67
76
|
member_count: int
|
|
68
77
|
timestamp_nanos: int
|
|
78
|
+
owner_span_id: SpanId | None = None
|
|
69
79
|
|
|
70
80
|
|
|
71
81
|
@dataclass(frozen=True, slots=True)
|
|
72
82
|
class FireAndForgetEvent(TraceEvent):
|
|
73
|
-
"""Lifecycle marker: a fire-and-forget group was launched.
|
|
83
|
+
"""Lifecycle marker: a fire-and-forget group was launched.
|
|
84
|
+
|
|
85
|
+
``owner_span_id`` is the span current on the launching thread (B-166); ``None`` for a group
|
|
86
|
+
launched outside every span.
|
|
87
|
+
"""
|
|
74
88
|
|
|
75
89
|
group_id: str
|
|
76
90
|
timestamp_nanos: int
|
|
91
|
+
owner_span_id: SpanId | None = None
|
|
77
92
|
|
|
78
93
|
|
|
79
94
|
def span_id_of(event: TraceEvent) -> SpanId | None:
|
|
80
|
-
"""Returns the span id an event belongs to
|
|
95
|
+
"""Returns the span id an event belongs to.
|
|
96
|
+
|
|
97
|
+
Enter/exit events answer their own span; group lifecycle events answer the span that
|
|
98
|
+
launched the group (``owner_span_id``), so a request's cleanup removes them with it. A group
|
|
99
|
+
created outside every span has no owner: ``None``, and the event is retained (B-166).
|
|
100
|
+
"""
|
|
81
101
|
if isinstance(event, EnterEvent | ExitEvent):
|
|
82
102
|
return event.span_context.span_id
|
|
103
|
+
if isinstance(event, ForkCreatedEvent | MergeEvent | FireAndForgetEvent):
|
|
104
|
+
return event.owner_span_id
|
|
83
105
|
return None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# SPDX-License-Identifier: BUSL-1.1
|
|
2
|
+
# Licensed under the Business Source License 1.1 (see LICENSE); Change Date: four
|
|
3
|
+
# years from publication; Change License: Apache-2.0
|
|
4
|
+
# Copyright (c) 2026 Empower Agile
|
|
5
|
+
"""The one reading of an exception's message every emitter shares (B-174).
|
|
6
|
+
|
|
7
|
+
``Threw`` holds the live exception, unlike ``Returned``, which
|
|
8
|
+
holds text the value renderer already rendered, so a message never passed the redaction policy:
|
|
9
|
+
``ValueError("bad token: eyJ...")`` reached every output in full while the identical string as a
|
|
10
|
+
parameter answered ``[REDACTED]``. Every renderer, exporter and listener reads the message here.
|
|
11
|
+
|
|
12
|
+
**@llmNote** Only the value-shape axis applies: a JWT, a Luhn-valid card number or a
|
|
13
|
+
``Set-Cookie`` string inside a message is never legitimate. The name deny-list has nothing to match
|
|
14
|
+
on, and blanking messages by keyword would destroy the diagnostic value the message exists for.
|
|
15
|
+
Length is deliberately not capped.
|
|
16
|
+
|
|
17
|
+
**@sideEffects** ``str(exc)`` is application code run once per emitter; a rogue ``__str__`` that
|
|
18
|
+
raises degrades to ``<error>`` instead of failing the emitter.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from narrativetrace.escape import control_sanitize
|
|
24
|
+
from narrativetrace.redaction import REDACTED_MARKER, RedactionPolicy
|
|
25
|
+
|
|
26
|
+
UNREADABLE = "<error>"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def of(exception: BaseException | None) -> str | None:
|
|
30
|
+
"""The message an emitter may print: replaced whole by the redaction marker when its bytes are
|
|
31
|
+
a credential, control-escaped always; ``None`` for no exception, ``<error>`` when unreadable."""
|
|
32
|
+
if exception is None:
|
|
33
|
+
return None
|
|
34
|
+
try:
|
|
35
|
+
raw = str(exception)
|
|
36
|
+
except Exception:
|
|
37
|
+
return UNREADABLE
|
|
38
|
+
if RedactionPolicy.DEFAULT.should_redact_value(raw):
|
|
39
|
+
return REDACTED_MARKER
|
|
40
|
+
return control_sanitize(raw)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def text(exception: BaseException | None) -> str:
|
|
44
|
+
""":func:`of` where text rather than ``None`` is needed: ``null`` for no exception."""
|
|
45
|
+
message = of(exception)
|
|
46
|
+
return "null" if message is None else message
|
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
import json
|
|
21
21
|
from typing import Any
|
|
22
22
|
|
|
23
|
+
from narrativetrace import exception_message
|
|
23
24
|
from narrativetrace.concurrency import ConcurrencyInfo
|
|
24
25
|
from narrativetrace.identity import resolve_identity
|
|
25
26
|
from narrativetrace.nodes import TraceNode
|
|
@@ -151,7 +152,7 @@ def _append_outcome(event: dict[str, Any], outcome: TraceOutcome | None) -> None
|
|
|
151
152
|
elif isinstance(outcome, Threw):
|
|
152
153
|
event["outcome"] = "threw"
|
|
153
154
|
event["errorType"] = type(outcome.exception).__name__
|
|
154
|
-
event["errorMessage"] =
|
|
155
|
+
event["errorMessage"] = exception_message.of(outcome.exception)
|
|
155
156
|
elif isinstance(outcome, Incomplete):
|
|
156
157
|
event["outcome"] = "incomplete"
|
|
157
158
|
else:
|
|
@@ -44,6 +44,7 @@ from collections.abc import Iterator
|
|
|
44
44
|
from contextlib import contextmanager
|
|
45
45
|
from enum import Enum
|
|
46
46
|
|
|
47
|
+
from narrativetrace import exception_message
|
|
47
48
|
from narrativetrace.context import ContextVarNarrativeContext
|
|
48
49
|
from narrativetrace.escape import control_sanitize
|
|
49
50
|
from narrativetrace.events import (
|
|
@@ -254,7 +255,7 @@ class LoggingTraceConsumer:
|
|
|
254
255
|
self, exception: BaseException, error_context: str | None, keys: dict[str, str]
|
|
255
256
|
) -> None:
|
|
256
257
|
exc_type = type(exception).__name__
|
|
257
|
-
message =
|
|
258
|
+
message = exception_message.text(exception)
|
|
258
259
|
if error_context is not None:
|
|
259
260
|
self._logger.log(
|
|
260
261
|
self._exception,
|
|
@@ -16,6 +16,7 @@ pinned .NET/TS semantics and is the more predictable contract.
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
import threading
|
|
19
20
|
import time
|
|
20
21
|
from collections.abc import Callable
|
|
21
22
|
from dataclasses import dataclass
|
|
@@ -23,14 +24,25 @@ from dataclasses import dataclass
|
|
|
23
24
|
_DEFAULT_CLOCK = time.monotonic
|
|
24
25
|
|
|
25
26
|
|
|
26
|
-
@dataclass(slots=True)
|
|
27
|
+
@dataclass(slots=True, eq=False)
|
|
27
28
|
class _TimedEntry[V]:
|
|
29
|
+
"""Compared by identity: a replacement entry never equals the one it replaced."""
|
|
30
|
+
|
|
28
31
|
value: V
|
|
29
32
|
created: float
|
|
30
33
|
|
|
31
34
|
|
|
32
35
|
class PerishableMap[K, V]:
|
|
33
|
-
"""A capacity- and TTL-bounded map with an eviction callback.
|
|
36
|
+
"""A capacity- and TTL-bounded map with an eviction callback.
|
|
37
|
+
|
|
38
|
+
Thread-safe (B-169): an :class:`~threading.RLock` guards every read-modify-write, because the
|
|
39
|
+
OTel listener puts and removes on caller threads. A sweep removes exactly the entry it
|
|
40
|
+
examined (by identity), so a replacement put under an expired key is never evicted in its
|
|
41
|
+
place, and an entry is called back only when its own removal won -- never twice. Callbacks run
|
|
42
|
+
*after* the lock is released (collect under the lock, release, then call): a callback that
|
|
43
|
+
re-enters the map cannot deadlock and never observes a half-finished sweep. By then every
|
|
44
|
+
evicted entry is already out of the map, so a callback's ``remove`` of one answers ``None``.
|
|
45
|
+
"""
|
|
34
46
|
|
|
35
47
|
def __init__(
|
|
36
48
|
self,
|
|
@@ -45,43 +57,54 @@ class PerishableMap[K, V]:
|
|
|
45
57
|
self._ttl = ttl_seconds
|
|
46
58
|
self._on_evict = on_evict
|
|
47
59
|
self._clock = clock
|
|
60
|
+
self._lock = threading.RLock()
|
|
48
61
|
self._entries: dict[K, _TimedEntry[V]] = {}
|
|
49
62
|
|
|
50
63
|
def put(self, key: K, value: V) -> None:
|
|
51
64
|
"""Inserts/overwrites, evicting expired (and, for new keys only, over-capacity) first."""
|
|
52
|
-
self.
|
|
53
|
-
|
|
54
|
-
self.
|
|
55
|
-
|
|
65
|
+
with self._lock:
|
|
66
|
+
evicted = self._evict_expired()
|
|
67
|
+
if key not in self._entries: # divergence: overwrite at capacity evicts nothing
|
|
68
|
+
evicted += self._evict_over_capacity()
|
|
69
|
+
self._entries[key] = _TimedEntry(value, self._clock())
|
|
70
|
+
for entry in evicted:
|
|
71
|
+
self._on_evict(entry.value)
|
|
56
72
|
|
|
57
73
|
def get(self, key: K) -> V | None:
|
|
58
74
|
"""Returns the value for ``key`` or ``None``."""
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
with self._lock:
|
|
76
|
+
entry = self._entries.get(key)
|
|
77
|
+
return entry.value if entry is not None else None
|
|
61
78
|
|
|
62
79
|
def remove(self, key: K) -> V | None:
|
|
63
80
|
"""Removes and returns the value for ``key`` or ``None``."""
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
with self._lock:
|
|
82
|
+
entry = self._entries.pop(key, None)
|
|
83
|
+
return entry.value if entry is not None else None
|
|
66
84
|
|
|
67
85
|
def size(self) -> int:
|
|
68
86
|
"""Number of entries currently held."""
|
|
69
|
-
|
|
87
|
+
with self._lock:
|
|
88
|
+
return len(self._entries)
|
|
70
89
|
|
|
71
|
-
def _evict_expired(self) ->
|
|
90
|
+
def _evict_expired(self) -> list[_TimedEntry[V]]:
|
|
91
|
+
"""Removes the expired entries; the caller (holding the lock) calls back afterwards."""
|
|
72
92
|
now = self._clock()
|
|
73
|
-
|
|
74
|
-
for key in
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
evicted: list[_TimedEntry[V]] = []
|
|
94
|
+
for key, examined in list(self._entries.items()):
|
|
95
|
+
if now - examined.created > self._ttl and self._entries.get(key) is examined:
|
|
96
|
+
del self._entries[key]
|
|
97
|
+
evicted.append(examined)
|
|
98
|
+
return evicted
|
|
99
|
+
|
|
100
|
+
def _evict_over_capacity(self) -> list[_TimedEntry[V]]:
|
|
101
|
+
evicted: list[_TimedEntry[V]] = []
|
|
79
102
|
while len(self._entries) >= self._max_capacity:
|
|
80
103
|
oldest = self._find_oldest()
|
|
81
104
|
if oldest is None:
|
|
82
105
|
break
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
evicted.append(self._entries.pop(oldest))
|
|
107
|
+
return evicted
|
|
85
108
|
|
|
86
109
|
def _find_oldest(self) -> K | None:
|
|
87
110
|
oldest_key: K | None = None
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
|
|
17
|
+
from narrativetrace import exception_message
|
|
17
18
|
from narrativetrace.escape import control_sanitize
|
|
18
19
|
from narrativetrace.nodes import TraceNode
|
|
19
20
|
from narrativetrace.outcomes import Incomplete, Returned, Threw, TraceOutcome
|
|
@@ -211,7 +212,7 @@ class IndentedTextRenderer:
|
|
|
211
212
|
if isinstance(outcome, Returned):
|
|
212
213
|
parts.append(f" → {_return_text(outcome.rendered_value)}")
|
|
213
214
|
elif isinstance(outcome, Threw):
|
|
214
|
-
message =
|
|
215
|
+
message = exception_message.text(outcome.exception)
|
|
215
216
|
parts.append(f" !! {_exception_type(outcome.exception)}: {message}")
|
|
216
217
|
if sig.error_context is not None:
|
|
217
218
|
parts.append(f" | {control_sanitize(sig.error_context)}")
|
|
@@ -224,7 +225,7 @@ class IndentedTextRenderer:
|
|
|
224
225
|
if isinstance(outcome, Returned):
|
|
225
226
|
parts.append(f"→ {_return_text(outcome.rendered_value)}")
|
|
226
227
|
elif isinstance(outcome, Threw):
|
|
227
|
-
message =
|
|
228
|
+
message = exception_message.text(outcome.exception)
|
|
228
229
|
parts.append(f"!! {_exception_type(outcome.exception)}: {message}")
|
|
229
230
|
if sig.error_context is not None:
|
|
230
231
|
parts.append(f" | {control_sanitize(sig.error_context)}")
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
|
|
17
|
+
from narrativetrace import exception_message
|
|
17
18
|
from narrativetrace.escape import markdown_code, markdown_text
|
|
18
19
|
from narrativetrace.nodes import TraceNode
|
|
19
20
|
from narrativetrace.outcomes import Incomplete, Returned, Threw, TraceOutcome
|
|
@@ -229,7 +230,7 @@ class MarkdownRenderer:
|
|
|
229
230
|
elif isinstance(outcome, Threw):
|
|
230
231
|
error_indent = " " * (depth + 1)
|
|
231
232
|
exc_type = markdown_code(type(outcome.exception).__name__)
|
|
232
|
-
message = markdown_text(
|
|
233
|
+
message = markdown_text(exception_message.text(outcome.exception))
|
|
233
234
|
parts.append(f"\n\n{error_indent}> ❌ {exc_type}: {message}")
|
|
234
235
|
if sig.error_context is not None:
|
|
235
236
|
parts.append(f"\n{error_indent}> {markdown_text(sig.error_context)}")
|
|
@@ -249,7 +250,7 @@ class MarkdownRenderer:
|
|
|
249
250
|
elif isinstance(outcome, Threw):
|
|
250
251
|
error_indent = " " * (depth + 1)
|
|
251
252
|
exc_type = markdown_code(type(outcome.exception).__name__)
|
|
252
|
-
message = markdown_text(
|
|
253
|
+
message = markdown_text(exception_message.text(outcome.exception))
|
|
253
254
|
parts.append(f"❌ {exc_type}: {message}")
|
|
254
255
|
if sig.error_context is not None:
|
|
255
256
|
parts.append(f"\n{error_indent}> {markdown_text(sig.error_context)}")
|
|
@@ -12,6 +12,7 @@ Every span is named by its span id in parentheses (``(#1.3)``,
|
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
|
+
from narrativetrace import exception_message
|
|
15
16
|
from narrativetrace.escape import control_sanitize
|
|
16
17
|
from narrativetrace.nodes import TraceNode
|
|
17
18
|
from narrativetrace.outcomes import Incomplete, Returned, Threw, TraceOutcome
|
|
@@ -165,7 +166,7 @@ class ProseRenderer:
|
|
|
165
166
|
if isinstance(outcome, Returned) and outcome.rendered_value is not None:
|
|
166
167
|
parts.append(f"{indent} Returned {outcome.rendered_value}.\n")
|
|
167
168
|
elif isinstance(outcome, Threw):
|
|
168
|
-
message =
|
|
169
|
+
message = exception_message.text(outcome.exception)
|
|
169
170
|
parts.append(f"{indent} {_exception_type(outcome.exception)}: {message}.\n")
|
|
170
171
|
elif isinstance(outcome, Incomplete):
|
|
171
172
|
parts.append(f"{indent} ⏳ in-flight.\n")
|
|
@@ -176,7 +177,7 @@ class ProseRenderer:
|
|
|
176
177
|
if isinstance(outcome, Returned) and outcome.rendered_value is not None:
|
|
177
178
|
parts.append(f", returning {outcome.rendered_value}")
|
|
178
179
|
elif isinstance(outcome, Threw):
|
|
179
|
-
message =
|
|
180
|
+
message = exception_message.text(outcome.exception)
|
|
180
181
|
parts.append(f" — {_exception_type(outcome.exception)}: {message}")
|
|
181
182
|
if sig.error_context is not None:
|
|
182
183
|
parts.append(f" ({control_sanitize(sig.error_context)})")
|