diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8daa4515..d7f4af87 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -6,7 +6,8 @@ You are working in Newsletter Maker, a Django + DRF + Celery + Qdrant backend wi - Backend runtime code is split across `core/`, `projects/`, `content/`, `entities/`, `ingestion/`, `newsletters/`, `pipeline/`, `trends/`, and `users/`. - Django project settings and top-level URLs live in `newsletter_maker/`. -- Backend tests live primarily in `core/tests/`, app-local `tests/` packages, and `tests/`. +- Backend tests live in app-local `tests/` packages first (`users/tests/`, `projects/tests/`, `ingestion/tests/`, `newsletters/tests/`, `pipeline/tests/`), with `core/tests/` reserved for the remaining cross-cutting coverage. +- The repo-root `tests/` package is for integration coverage only. New unit and app-scoped tests should live in the owning app's `tests/` package. - Frontend application code lives in `frontend/src/app/`, shared UI in `frontend/src/components/`, and shared API/types/helpers in `frontend/src/lib/`. - Operational and architecture docs live in `docs/`. @@ -20,11 +21,12 @@ You are working in Newsletter Maker, a Django + DRF + Celery + Qdrant backend wi ## Backend Conventions - Project scoping is a core invariant. Most API resources are nested under `/api/v1/projects/{project_id}/...`. -- Reuse the established DRF patterns in `core/api.py`, `core/api_urls.py`, and `core/serializers.py`: +- Treat `core/` as the home for genuine cross-cutting concerns only. New app-owned runtime logic should live with its owning app rather than expanding `core/`. +- Reuse the established DRF patterns in `core/api.py`, `core/api_urls.py`, and `core/serializer_mixins.py`: - `ProjectOwnedQuerysetMixin` for nested viewsets - serializer context containing `project` - explicit validation for cross-project foreign keys -- Keep viewsets and views thin. Put operational logic in `core/tasks.py`, `core/pipeline.py`, `core/newsletters.py`, `core/plugins/`, or nearby helpers. +- Keep viewsets and views thin. Put operational logic in `core/tasks.py`, `core/pipeline.py`, `ingestion/plugins/`, `newsletters/intake.py`, or nearby helpers owned by the feature's app. - Preserve existing API field shapes. Backend serializers and frontend types currently use `snake_case`; do not introduce ad hoc `camelCase` transforms. - When API behavior changes, update drf-spectacular schema metadata in `core/api.py`. - When changing ingestion, newsletter intake, AI processing, or embeddings, preserve the handoff between database state, Celery tasks, and Qdrant state. diff --git a/content/admin.py b/content/admin.py index 20d617b4..2080e7d5 100644 --- a/content/admin.py +++ b/content/admin.py @@ -6,12 +6,73 @@ from unfold.admin import ModelAdmin from content.models import Content, UserFeedback -from core.admin import ( - DuplicateStateFilter, - HighValueFilter, - _score_color, - _score_to_percent, -) + + +def _score_to_percent(value): + """Normalize score-like values for display as percentages.""" + + if value is None: + return None + numeric_value = float(value) + if -1.0 <= numeric_value <= 1.0: + return numeric_value * 100 + return numeric_value + + +def _score_color(value) -> str: + """Return the admin display color for a score-like value.""" + + percent_value = _score_to_percent(value) + if percent_value is None: + return "inherit" + if percent_value >= 75: + return "green" + if percent_value >= 40: + return "orange" + return "red" + + +class HighValueFilter(admin.SimpleListFilter): + """Filter content down to high-value reference items.""" + + title = "Content Value" + parameter_name = "value_tier" + + def lookups(self, request, model_admin): + """Return the custom filter options displayed in the admin sidebar.""" + + return (("high_value", "🔥 High Value (Score > 80 & Reference)"),) + + def queryset(self, request, queryset): + """Apply the high-value filter when it is selected.""" + + if self.value() == "high_value": + return queryset.filter(relevance_score__gt=80, is_reference=True) + return queryset + + +class DuplicateStateFilter(admin.SimpleListFilter): + """Filter content by duplicate retention and suppression state.""" + + title = "Duplicate State" + parameter_name = "duplicate_state" + + def lookups(self, request, model_admin): + """Return duplicate-state options displayed in the admin sidebar.""" + + return ( + ("canonical_with_duplicates", "Canonical rows with duplicate signals"), + ("suppressed_duplicates", "Suppressed duplicate rows"), + ) + + def queryset(self, request, queryset): + """Apply the selected duplicate-state filter.""" + + if self.value() == "canonical_with_duplicates": + return queryset.filter(duplicate_signal_count__gt=0) + if self.value() == "suppressed_duplicates": + return queryset.filter(duplicate_of__isnull=False) + return queryset @admin.register(Content) diff --git a/content/apps.py b/content/apps.py index a3c0dbb6..1fd471c9 100644 --- a/content/apps.py +++ b/content/apps.py @@ -8,3 +8,6 @@ class ContentConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "content" + + def ready(self) -> None: + import content.signals # noqa: F401 diff --git a/core/deduplication.py b/content/deduplication.py similarity index 100% rename from core/deduplication.py rename to content/deduplication.py diff --git a/core/signals.py b/content/signals.py similarity index 51% rename from core/signals.py rename to content/signals.py index 4ab7e4b6..6d73eba9 100644 --- a/core/signals.py +++ b/content/signals.py @@ -1,34 +1,15 @@ -"""Signal handlers for cross-cutting core behaviors.""" +"""Signal handlers for content-owned behaviors.""" from __future__ import annotations -from typing import Any - from django.db.models.signals import post_save from django.dispatch import receiver -from core.models import UserFeedback -from newsletters.signals import handle_anymail_inbound as _handle_anymail_inbound +from content.models import UserFeedback from projects.models import ProjectConfig from trends.tasks import queue_topic_centroid_recompute -def handle_anymail_inbound( - sender: Any, - event: Any, - esp_name: str, - **kwargs: Any, -) -> None: - """Preserve the legacy core.signals import path for inbound handling.""" - - _handle_anymail_inbound( - sender=sender, - event=event, - esp_name=esp_name, - **kwargs, - ) - - @receiver(post_save, sender=UserFeedback) def queue_topic_centroid_on_feedback_save(sender, instance, created, **kwargs): """Queue centroid recomputation when feedback changes and config allows it.""" @@ -38,4 +19,4 @@ def queue_topic_centroid_on_feedback_save(sender, instance, created, **kwargs): config, _ = ProjectConfig.objects.get_or_create(project=instance.project) if config.recompute_topic_centroid_on_feedback_save: - queue_topic_centroid_recompute(instance.project_id) + queue_topic_centroid_recompute(instance.project_id) \ No newline at end of file diff --git a/content/tests/test_admin.py b/content/tests/test_admin.py new file mode 100644 index 00000000..c1df4d8c --- /dev/null +++ b/content/tests/test_admin.py @@ -0,0 +1,655 @@ +from types import SimpleNamespace +from typing import Any, cast +from unittest.mock import ANY, Mock + +import pytest +from django.contrib import messages +from django.contrib.admin.sites import AdminSite +from django.db.models import Model +from django.http import HttpRequest +from django.test import RequestFactory +from django.utils import timezone + +from content.admin import ( + ContentAdmin, + DuplicateStateFilter, + HighValueFilter, + UserFeedbackAdmin, +) +from content.models import Content, UserFeedback +from pipeline.models import SkillResult +from projects.model_support import SourcePluginName +from projects.models import Project + +pytestmark = pytest.mark.django_db + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed admin test assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: Any, **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +def _request(query_params: dict[str, str] | None = None) -> HttpRequest: + """Build a typed request object for admin actions and filters.""" + + return RequestFactory().get("/admin/", data=query_params or {}) + + +def _params(**kwargs: str) -> dict[str, list[str]]: + """Build typed admin filter params.""" + + return {key: [value] for key, value in kwargs.items()} + + +def _message_user_mock(admin_instance: Any, mocker: Any) -> Mock: + """Install a mock for ModelAdmin.message_user and return it for assertions.""" + + message_mock = cast(Mock, mocker.Mock()) + admin_instance.message_user = message_mock + return message_mock + + +def _context(response: object) -> dict[str, Any]: + """Cast admin changelist extra_context payloads for typed assertions.""" + + return cast(dict[str, Any], response) + + +def _dashboard_stats(response: object) -> list[dict[str, Any]]: + """Return typed dashboard stats rows from a changelist extra_context payload.""" + + return cast(list[dict[str, Any]], _context(response)["dashboard_stats"]) + + +@pytest.fixture +def source_admin_context(django_user_model): + user = _create_user( + django_user_model, username="admin-owner", password="testpass123" + ) + project = Project.objects.create(name="Admin Project", topic_description="Infra") + return SimpleNamespace(user=user, project=project) + + +def test_content_preview_uses_content_text(source_admin_context): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-preview", + title="Admin Preview", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="A short preview from the content body.", + ) + admin_instance = ContentAdmin(Content, AdminSite()) + + preview = admin_instance.preview_content(content) + + assert 'title="A short preview from the content body."' in preview + + +def test_content_preview_returns_dash_when_content_text_blank(source_admin_context): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-preview-empty", + title="Admin Preview Empty", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text=" ", + ) + admin_instance = ContentAdmin(Content, AdminSite()) + + assert admin_instance.preview_content(content) == "-" + + +def test_content_view_trace_prefers_external_trace_url(source_admin_context): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-trace", + title="Admin Trace", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Trace content.", + ) + SkillResult.objects.create( + content=content, + project=source_admin_context.project, + skill_name="summarization", + status="COMPLETED", + result_data={"trace_url": "https://traces.example/run/123"}, + ) + admin_instance = ContentAdmin(Content, AdminSite()) + + rendered = admin_instance.view_trace(content) + + assert "https://traces.example/run/123" in rendered + assert "📈 Trace" in rendered + + +def test_content_view_trace_falls_back_to_skill_runs_changelist(source_admin_context): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-trace-fallback", + title="Admin Trace Fallback", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Trace fallback content.", + ) + SkillResult.objects.create( + content=content, + project=source_admin_context.project, + skill_name="relevance_scoring", + status="COMPLETED", + result_data={"relevance_score": 0.9}, + ) + admin_instance = ContentAdmin(Content, AdminSite()) + + rendered = admin_instance.view_trace(content) + + assert "🧠 Skill runs" in rendered + assert f"content__id__exact={_require_pk(content)}" in rendered + + +def test_content_changelist_view_builds_dashboard_stats(source_admin_context, mocker): + Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-dashboard-1", + title="Admin Dashboard 1", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Content one.", + relevance_score=0.8, + authority_adjusted_score=0.85, + ) + Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-dashboard-2", + title="Admin Dashboard 2", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Content two.", + relevance_score=0.4, + authority_adjusted_score=0.45, + ) + admin_instance = ContentAdmin(Content, AdminSite()) + mocker.patch.object( + admin_instance, "get_queryset", return_value=Content.objects.all() + ) + super_changelist_view = mocker.patch( + "django.contrib.admin.options.ModelAdmin.changelist_view", + side_effect=lambda request, extra_context=None: extra_context, + ) + + response = admin_instance.changelist_view(request=_request()) + dashboard_stats = _dashboard_stats(response) + + super_changelist_view.assert_called_once() + assert dashboard_stats[0]["value"] == "60.0%" + assert dashboard_stats[1]["value"] == "65.0%" + assert dashboard_stats[2]["value"] == 2 + + +def test_content_admin_score_columns_render_expected_values(source_admin_context): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-scores", + title="Admin Scores", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Score rendering content.", + relevance_score=0.8, + authority_adjusted_score=0.86, + ) + admin_instance = ContentAdmin(Content, AdminSite()) + + rendered_base = admin_instance.display_relevance(content) + rendered_adjusted = admin_instance.display_authority_adjusted_score(content) + + assert "80.0%" in rendered_base + assert "green" in rendered_base + assert "86.0%" in rendered_adjusted + assert "green" in rendered_adjusted + + +def test_generate_newsletter_ideas_queues_selected_content( + source_admin_context, mocker +): + first_content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-queue-1", + title="Admin Queue 1", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Queue one.", + ) + second_content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-queue-2", + title="Admin Queue 2", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Queue two.", + ) + delay_mock = mocker.patch("core.tasks.process_content.delay") + admin_instance = ContentAdmin(Content, AdminSite()) + message_user_mock = _message_user_mock(admin_instance, mocker) + + admin_instance.generate_newsletter_ideas( + request=_request(), + queryset=Content.objects.filter( + id__in=[_require_pk(first_content), _require_pk(second_content)] + ).order_by("id"), + ) + + delay_mock.assert_any_call(_require_pk(first_content)) + delay_mock.assert_any_call(_require_pk(second_content)) + assert delay_mock.call_count == 2 + message_user_mock.assert_called_once_with( + ANY, + "Successfully queued the pipeline for 2 items.", + messages.SUCCESS, + ) + + +def test_content_admin_duplicate_columns_render_expected_values(source_admin_context): + canonical = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-canonical", + canonical_url="https://example.com/admin-canonical", + title="Canonical Story", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Canonical content.", + duplicate_signal_count=2, + ) + duplicate = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-canonical?utm_source=reddit", + canonical_url="https://example.com/admin-canonical", + title="Duplicate Story", + author="Editor", + source_plugin=SourcePluginName.REDDIT, + published_date=timezone.now(), + content_text="Duplicate content.", + duplicate_of=canonical, + is_active=False, + ) + admin_instance = ContentAdmin(Content, AdminSite()) + + assert "Also seen in 2 source(s)" in admin_instance.duplicate_badge(canonical) + assert admin_instance.duplicate_badge(duplicate) == "-" + assert admin_instance.duplicate_parent(canonical) == "-" + assert admin_instance.duplicate_parent(duplicate) == "Canonical Story" + + +def test_high_value_filter_only_returns_high_value_reference_content( + source_admin_context, +): + high_value = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/high-value", + title="High Value", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="High value content.", + relevance_score=81, + is_reference=True, + ) + Content.objects.create( + project=source_admin_context.project, + url="https://example.com/not-high-value", + title="Not High Value", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Not high value content.", + relevance_score=81, + is_reference=False, + ) + filter_instance = HighValueFilter( + request=_request(), + params=_params(value_tier="high_value"), + model=Content, + model_admin=ContentAdmin(Content, AdminSite()), + ) + filter_instance.value = lambda: "high_value" + + filtered = filter_instance.queryset(_request(), Content.objects.all()) + + assert list(filtered) == [high_value] + + +def test_duplicate_state_filter_returns_canonical_rows_with_duplicate_signals( + source_admin_context, +): + canonical = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/filter-canonical", + canonical_url="https://example.com/filter-canonical", + title="Canonical", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Canonical content.", + duplicate_signal_count=2, + ) + Content.objects.create( + project=source_admin_context.project, + url="https://example.com/filter-plain", + canonical_url="https://example.com/filter-plain", + title="Plain", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Plain content.", + ) + filter_instance = DuplicateStateFilter( + request=_request(), + params=_params(duplicate_state="canonical_with_duplicates"), + model=Content, + model_admin=ContentAdmin(Content, AdminSite()), + ) + filter_instance.value = lambda: "canonical_with_duplicates" + + filtered = filter_instance.queryset(_request(), Content.objects.all()) + + assert list(filtered) == [canonical] + + +def test_duplicate_state_filter_returns_suppressed_duplicates( + source_admin_context, +): + canonical = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/filter-parent", + canonical_url="https://example.com/filter-parent", + title="Canonical", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Canonical content.", + ) + duplicate = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/filter-parent?utm_source=reddit", + canonical_url="https://example.com/filter-parent", + title="Duplicate", + author="Editor", + source_plugin=SourcePluginName.REDDIT, + published_date=timezone.now(), + content_text="Duplicate content.", + duplicate_of=canonical, + is_active=False, + ) + filter_instance = DuplicateStateFilter( + request=_request(), + params=_params(duplicate_state="suppressed_duplicates"), + model=Content, + model_admin=ContentAdmin(Content, AdminSite()), + ) + filter_instance.value = lambda: "suppressed_duplicates" + + filtered = filter_instance.queryset(_request(), Content.objects.all()) + + assert list(filtered) == [duplicate] + + +def test_content_view_trace_builds_template_trace_url(source_admin_context, settings): + settings.AI_TRACE_URL_TEMPLATE = "https://trace.example/{project_id}/{skill_name}/{skill_result_id}/{trace_id}/{content_id}/{run_id}" + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-template-trace", + title="Admin Template Trace", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Template trace content.", + ) + skill_result = SkillResult.objects.create( + content=content, + project=source_admin_context.project, + skill_name="summarization", + status="COMPLETED", + result_data={"trace": {"trace_id": "trace-123"}}, + ) + admin_instance = ContentAdmin(Content, AdminSite()) + + rendered = admin_instance.view_trace(content) + + assert ( + f"https://trace.example/{_require_pk(source_admin_context.project)}/summarization/{_require_pk(skill_result)}/trace-123/{_require_pk(content)}/trace-123" + in rendered + ) + + +@pytest.mark.parametrize( + ("score", "expected_color"), + [ + (None, None), + (80, "green"), + (50, "orange"), + (10, "red"), + ], +) +def test_content_display_relevance_uses_expected_output( + source_admin_context, score, expected_color +): + content = Content.objects.create( + project=source_admin_context.project, + url=f"https://example.com/relevance-{score}", + title="Relevance Display", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Content.", + relevance_score=score, + ) + admin_instance = ContentAdmin(Content, AdminSite()) + + rendered = admin_instance.display_relevance(content) + + if score is None: + assert rendered == "-" + else: + assert expected_color in rendered + assert str(score) in rendered + + +def test_user_feedback_admin_helpers_and_dashboard_stats( + source_admin_context, django_user_model, mocker +): + mocker.patch("content.signals.queue_topic_centroid_recompute") + user = _create_user( + django_user_model, username="feedback-user", password="testpass123" + ) + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/feedback", + title="Feedback Title That Is Long Enough To Truncate", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Feedback content.", + relevance_score=85, + ) + upvote = UserFeedback.objects.create( + content=content, + project=source_admin_context.project, + user=user, + feedback_type="upvote", + ) + other_content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/feedback-other", + title="Other Feedback Title", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Other feedback content.", + relevance_score=20, + ) + UserFeedback.objects.create( + content=other_content, + project=source_admin_context.project, + user=_create_user( + django_user_model, username="feedback-user-2", password="testpass123" + ), + feedback_type="downvote", + ) + admin_instance = UserFeedbackAdmin(UserFeedback, AdminSite()) + super_changelist_view = mocker.patch( + "content.admin.ModelAdmin.changelist_view", + side_effect=lambda request, extra_context=None: extra_context, + ) + + response = admin_instance.changelist_view(_request()) + dashboard_stats = _dashboard_stats(response) + + assert "👍" in admin_instance.display_feedback(upvote) + assert admin_instance.get_content_title(upvote).endswith("...") + assert "green" in admin_instance.get_ai_score(upvote) + other_content.relevance_score = None + other_content.save(update_fields=["relevance_score"]) + downvote = UserFeedback.objects.get(content=other_content) + assert admin_instance.get_ai_score(downvote) == "-" + super_changelist_view.assert_called_once() + assert dashboard_stats[0]["value"] == "50.0%" + assert dashboard_stats[1]["value"] == 2 + + +def test_content_view_trace_returns_dash_when_no_skill_results(source_admin_context): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/admin-no-trace", + title="No Trace", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="No trace content.", + ) + admin_instance = ContentAdmin(Content, AdminSite()) + + assert admin_instance.view_trace(content) == "-" + + +def test_high_value_filter_lookups_and_noop_queryset(source_admin_context): + filter_instance = HighValueFilter( + request=_request(), + params={}, + model=Content, + model_admin=ContentAdmin(Content, AdminSite()), + ) + filter_instance.value = lambda: None + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/high-value-noop", + title="Noop", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="noop", + ) + + assert filter_instance.lookups(_request(), ContentAdmin(Content, AdminSite())) == ( + ("high_value", "🔥 High Value (Score > 80 & Reference)"), + ) + assert list(filter_instance.queryset(_request(), Content.objects.all())) == [ + content + ] + + +def test_user_feedback_admin_upvote_and_orange_score_branches( + source_admin_context, mocker +): + mocker.patch("content.signals.queue_topic_centroid_recompute") + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/feedback-orange", + title="Orange Feedback Title", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Feedback content.", + relevance_score=60, + ) + feedback = UserFeedback.objects.create( + content=content, + project=source_admin_context.project, + user=source_admin_context.user, + feedback_type="upvote", + ) + admin_instance = UserFeedbackAdmin(UserFeedback, AdminSite()) + + assert "👍" in admin_instance.display_feedback(feedback) + assert "orange" in admin_instance.get_ai_score(feedback) + + +def test_user_feedback_changelist_view_uses_success_color_for_high_approval( + source_admin_context, django_user_model, mocker +): + mocker.patch("content.signals.queue_topic_centroid_recompute") + first_content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/feedback-success-1", + title="Feedback Success One", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Feedback content one.", + relevance_score=90, + ) + second_content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/feedback-success-2", + title="Feedback Success Two", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Feedback content two.", + relevance_score=90, + ) + UserFeedback.objects.create( + content=first_content, + project=source_admin_context.project, + user=source_admin_context.user, + feedback_type="upvote", + ) + UserFeedback.objects.create( + content=second_content, + project=source_admin_context.project, + user=_create_user( + django_user_model, username="feedback-success-2", password="testpass123" + ), + feedback_type="upvote", + ) + admin_instance = UserFeedbackAdmin(UserFeedback, AdminSite()) + super_changelist_view = mocker.patch( + "content.admin.ModelAdmin.changelist_view", + side_effect=lambda request, extra_context=None: extra_context, + ) + + response = admin_instance.changelist_view(_request()) + dashboard_stats = _dashboard_stats(response) + + super_changelist_view.assert_called_once() + assert dashboard_stats[0]["color"] == "success" + assert dashboard_stats[0]["value"] == "100.0%" diff --git a/content/tests/test_api.py b/content/tests/test_api.py new file mode 100644 index 00000000..184ca9f1 --- /dev/null +++ b/content/tests/test_api.py @@ -0,0 +1,329 @@ +from types import SimpleNamespace +from typing import Any, cast +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient, APITestCase + +from content.models import Content, FeedbackType, UserFeedback +from entities.models import Entity +from pipeline.models import SkillResult, SkillStatus +from projects.model_support import SourcePluginName +from projects.models import Project, ProjectMembership, ProjectRole +from trends.models import ThemeSuggestion, ThemeSuggestionStatus, TopicCluster + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed API test assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _typed_client(client: object) -> APIClient: + """Cast the DRF test client so Pylance sees APIClient helpers.""" + + return cast(APIClient, client) + + +def _create_user(user_model: type[Any], **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +class ContentApiTests(APITestCase): + """Exercise content- and feedback-owned project API endpoints.""" + + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpass123") + self.other_user = _create_user( + user_model, + username="other", + password="testpass123", + ) + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + self.other_project = Project.objects.create( + name="Other Project", + topic_description="Frontend", + ) + ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + ProjectMembership.objects.create( + user=self.other_user, + project=self.other_project, + role=ProjectRole.ADMIN, + ) + self.owner_entity = Entity.objects.create( + project=self.owner_project, + name="Owner Entity", + type="individual", + ) + self.other_entity = Entity.objects.create( + project=self.other_project, + name="Other Entity", + type="vendor", + ) + self.owner_content = Content.objects.create( + project=self.owner_project, + url="https://example.com/owner", + title="Owner Content", + author="Owner Author", + entity=self.owner_entity, + source_plugin="rss", + published_date="2026-04-21T00:00:00Z", + content_text="Owner content text", + ) + self.other_content = Content.objects.create( + project=self.other_project, + url="https://example.com/other", + title="Other Content", + author="Other Author", + entity=self.other_entity, + source_plugin="rss", + published_date="2026-04-21T00:00:00Z", + content_text="Other content text", + ) + _typed_client(self.client).force_authenticate(self.owner) + + def assert_standardized_validation_error( + self, payload: dict[str, object], attr: str + ): + """Assert the repo-standardized validation payload shape.""" + + self.assertEqual(payload["type"], "validation_error") + errors = cast(list[dict[str, object]], payload["errors"]) + self.assertTrue(any(error["attr"] == attr for error in errors)) + + @patch("content.signals.queue_topic_centroid_recompute") + def test_feedback_create_assigns_current_user(self, queue_centroid_mock): + response = self.client.post( + reverse( + "v1:project-feedback-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "content": _require_pk(self.owner_content), + "feedback_type": FeedbackType.UPVOTE, + }, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + feedback = UserFeedback.objects.get() + self.assertEqual(feedback.user, self.owner) + self.assertEqual(feedback.feedback_type, FeedbackType.UPVOTE) + queue_centroid_mock.assert_called_once_with(_require_pk(self.owner_project)) + + def test_feedback_rejects_cross_project_content(self): + response = self.client.post( + reverse( + "v1:project-feedback-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "content": _require_pk(self.other_content), + "feedback_type": FeedbackType.DOWNVOTE, + }, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assert_standardized_validation_error(response.json(), "content") + + def test_content_create_uses_project_from_url(self): + response = self.client.post( + reverse( + "v1:project-content-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "url": "https://example.com/new", + "title": "New Content", + "author": "Owner Author", + "entity": _require_pk(self.owner_entity), + "source_plugin": "rss", + "published_date": "2026-04-22T00:00:00Z", + "content_text": "Nested content text", + "project": _require_pk(self.other_project), + }, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + created_content = Content.objects.get(title="New Content") + self.assertEqual(created_content.project, self.owner_project) + + @patch("core.tasks.run_relevance_scoring_skill.delay") + def test_content_skill_action_queues_relevance_scoring( + self, run_relevance_scoring_delay_mock + ): + response = self.client.post( + f"/api/v1/projects/{_require_pk(self.owner_project)}/contents/{_require_pk(self.owner_content)}/skills/relevance_scoring/", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + pending_result = SkillResult.objects.get( + content=self.owner_content, + skill_name="relevance_scoring", + superseded_by__isnull=True, + ) + run_relevance_scoring_delay_mock.assert_called_once_with( + _require_pk(pending_result) + ) + self.owner_content.refresh_from_db() + self.assertIsNone(self.owner_content.relevance_score) + self.assertEqual(response.json()["skill_name"], "relevance_scoring") + self.assertEqual(response.json()["status"], SkillStatus.PENDING) + + @patch("core.tasks.run_summarization_skill.delay") + def test_content_skill_action_queues_summarization( + self, run_summarization_delay_mock + ): + self.owner_content.relevance_score = 0.25 + self.owner_content.save(update_fields=["relevance_score"]) + + response = self.client.post( + f"/api/v1/projects/{_require_pk(self.owner_project)}/contents/{_require_pk(self.owner_content)}/skills/summarization/", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + pending_result = SkillResult.objects.get( + content=self.owner_content, + skill_name="summarization", + superseded_by__isnull=True, + ) + run_summarization_delay_mock.assert_called_once_with( + _require_pk(pending_result) + ) + self.assertEqual(response.json()["skill_name"], "summarization") + self.assertEqual(response.json()["status"], SkillStatus.PENDING) + + @patch("core.pipeline.search_similar_content") + def test_content_skill_action_runs_find_related(self, search_similar_content_mock): + search_similar_content_mock.return_value = [ + SimpleNamespace( + score=0.91, + payload={ + "content_id": _require_pk(self.other_content), + "title": self.other_content.title, + "url": self.other_content.url, + "published_date": self.other_content.published_date, + "source_plugin": self.other_content.source_plugin, + }, + ) + ] + + response = self.client.post( + f"/api/v1/projects/{_require_pk(self.owner_project)}/contents/{_require_pk(self.owner_content)}/skills/find_related/", + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.json()["skill_name"], "find_related") + self.assertEqual(response.json()["status"], SkillStatus.COMPLETED) + self.assertEqual( + response.json()["result_data"]["related_items"][0]["content_id"], + _require_pk(self.other_content), + ) + + def test_content_detail_includes_newsletter_promotion_state(self): + cluster = TopicCluster.objects.create( + project=self.owner_project, + first_seen_at="2026-04-22T00:00:00Z", + last_seen_at="2026-04-24T00:00:00Z", + is_active=True, + member_count=1, + dominant_entity=self.owner_entity, + ) + suggestion = ThemeSuggestion.objects.create( + project=self.owner_project, + cluster=cluster, + title="Promoted Theme", + pitch="Pitch", + why_it_matters="Why", + suggested_angle="Angle", + velocity_at_creation=0.9, + novelty_score=0.8, + status=ThemeSuggestionStatus.ACCEPTED, + decided_by=self.owner, + ) + self.owner_content.newsletter_promotion_theme = suggestion + self.owner_content.newsletter_promotion_by = self.owner + self.owner_content.newsletter_promotion_at = "2026-04-24T00:00:00Z" + self.owner_content.save( + update_fields=[ + "newsletter_promotion_theme", + "newsletter_promotion_by", + "newsletter_promotion_at", + ] + ) + + response = self.client.get( + reverse( + "v1:project-content-detail", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(self.owner_content), + }, + ) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.json()["newsletter_promotion_theme"], + _require_pk(suggestion), + ) + self.assertEqual( + response.json()["newsletter_promotion_by"], + _require_pk(self.owner), + ) + self.assertIsNotNone(response.json()["newsletter_promotion_at"]) + + def test_content_detail_includes_duplicate_state(self): + canonical = self.owner_content + canonical.canonical_url = "https://example.com/owner" + canonical.duplicate_signal_count = 1 + canonical.save(update_fields=["canonical_url", "duplicate_signal_count"]) + duplicate = Content.objects.create( + project=self.owner_project, + url="https://example.com/owner?utm_source=reddit", + canonical_url="https://example.com/owner", + title="Duplicate Owner Content", + author="Owner Author", + entity=self.owner_entity, + source_plugin="reddit", + published_date="2026-04-22T00:00:00Z", + content_text="Duplicate content text", + duplicate_of=canonical, + is_active=False, + ) + + response = self.client.get( + reverse( + "v1:project-content-detail", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(duplicate), + }, + ) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["canonical_url"], "https://example.com/owner") + self.assertEqual(response.json()["duplicate_of"], _require_pk(canonical)) + self.assertEqual(response.json()["duplicate_signal_count"], 0) diff --git a/content/tests/test_signals.py b/content/tests/test_signals.py new file mode 100644 index 00000000..68eb563d --- /dev/null +++ b/content/tests/test_signals.py @@ -0,0 +1,116 @@ +from types import SimpleNamespace +from typing import Any, cast + +import pytest + +from content.models import Content, FeedbackType, UserFeedback +from entities.models import Entity +from projects.model_support import SourcePluginName +from projects.models import Project, ProjectConfig + +pytestmark = pytest.mark.django_db + + +def _create_user(user_model: Any, **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +@pytest.fixture +def source_plugin_context(django_user_model): + user = _create_user( + django_user_model, username="plugin-owner", password="testpass123" + ) + project = Project.objects.create(name="Plugin Project", topic_description="Infra") + entity = Entity.objects.create( + project=project, + name="Example", + type="vendor", + website_url="https://example.com", + ) + return SimpleNamespace(user=user, project=project, entity=entity) + + +def test_feedback_model_create_queues_topic_centroid_recompute( + source_plugin_context, mocker +): + content = Content.objects.create( + project=source_plugin_context.project, + entity=source_plugin_context.entity, + url="https://example.com/direct-feedback-content", + title="Direct Feedback Content", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-20T12:00:00Z", + content_text="Manual content body", + ) + queue_mock = mocker.patch("content.signals.queue_topic_centroid_recompute") + + UserFeedback.objects.create( + project=source_plugin_context.project, + content=content, + user=source_plugin_context.user, + feedback_type=FeedbackType.UPVOTE, + ) + + queue_mock.assert_called_once_with(source_plugin_context.project.id) + + +def test_feedback_model_update_queues_topic_centroid_recompute( + source_plugin_context, mocker +): + content = Content.objects.create( + project=source_plugin_context.project, + entity=source_plugin_context.entity, + url="https://example.com/direct-feedback-update", + title="Direct Feedback Update", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-20T12:00:00Z", + content_text="Manual content body", + ) + queue_mock = mocker.patch("content.signals.queue_topic_centroid_recompute") + feedback = UserFeedback.objects.create( + project=source_plugin_context.project, + content=content, + user=source_plugin_context.user, + feedback_type=FeedbackType.UPVOTE, + ) + + queue_mock.reset_mock() + feedback.feedback_type = FeedbackType.DOWNVOTE + feedback.save(update_fields=["feedback_type"]) + + queue_mock.assert_called_once_with(source_plugin_context.project.id) + + +def test_feedback_save_skips_topic_centroid_recompute_when_project_config_disables_it( + source_plugin_context, mocker +): + ProjectConfig.objects.create( + project=source_plugin_context.project, + recompute_topic_centroid_on_feedback_save=False, + ) + content = Content.objects.create( + project=source_plugin_context.project, + entity=source_plugin_context.entity, + url="https://example.com/direct-feedback-disabled", + title="Direct Feedback Disabled", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-20T12:00:00Z", + content_text="Manual content body", + ) + queue_mock = mocker.patch("content.signals.queue_topic_centroid_recompute") + + feedback = UserFeedback.objects.create( + project=source_plugin_context.project, + content=content, + user=source_plugin_context.user, + feedback_type=FeedbackType.UPVOTE, + ) + feedback.feedback_type = FeedbackType.DOWNVOTE + feedback.save(update_fields=["feedback_type"]) + + queue_mock.assert_not_called() diff --git a/core/admin.py b/core/admin.py deleted file mode 100644 index ade491bb..00000000 --- a/core/admin.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Django admin configuration for the remaining core cross-cutting workflow.""" - -from django.contrib import admin -from unfold.admin import ModelAdmin as UnfoldModelAdmin - -ModelAdmin = UnfoldModelAdmin - - -def _score_to_percent(value): - """Normalize score-like values for display as percentages.""" - - if value is None: - return None - numeric_value = float(value) - if -1.0 <= numeric_value <= 1.0: - return numeric_value * 100 - return numeric_value - - -def _score_color(value) -> str: - """Return the admin display color for a score-like value.""" - - percent_value = _score_to_percent(value) - if percent_value is None: - return "inherit" - if percent_value >= 75: - return "green" - if percent_value >= 40: - return "orange" - return "red" - - -from trends.admin import TopicCentroidSnapshotAdmin # noqa: E402,F401 - - -class HighValueFilter(admin.SimpleListFilter): - """Filter content down to high-value reference items.""" - - title = "Content Value" - parameter_name = "value_tier" - - def lookups(self, request, model_admin): - """Return the custom filter options displayed in the admin sidebar.""" - - return (("high_value", "🔥 High Value (Score > 80 & Reference)"),) - - def queryset(self, request, queryset): - """Apply the high-value filter when it is selected.""" - - if self.value() == "high_value": - return queryset.filter(relevance_score__gt=80, is_reference=True) - return queryset - - -class DuplicateStateFilter(admin.SimpleListFilter): - """Filter content by duplicate retention and suppression state.""" - - title = "Duplicate State" - parameter_name = "duplicate_state" - - def lookups(self, request, model_admin): - """Return duplicate-state options displayed in the admin sidebar.""" - - return ( - ("canonical_with_duplicates", "Canonical rows with duplicate signals"), - ("suppressed_duplicates", "Suppressed duplicate rows"), - ) - - def queryset(self, request, queryset): - """Apply the selected duplicate-state filter.""" - - if self.value() == "canonical_with_duplicates": - return queryset.filter(duplicate_signal_count__gt=0) - if self.value() == "suppressed_duplicates": - return queryset.filter(duplicate_of__isnull=False) - return queryset - - -from pipeline.admin import ReviewQueueAdmin, SkillResultAdmin # noqa: E402,F401 diff --git a/core/api.py b/core/api.py index 08d8772e..fa1474dc 100644 --- a/core/api.py +++ b/core/api.py @@ -16,41 +16,14 @@ extend_schema_view, inline_serializer, ) -from rest_framework import serializers, status, viewsets -from rest_framework.decorators import action +from rest_framework import serializers from rest_framework.exceptions import NotFound -from rest_framework.response import Response - -from core.models import ( - Content, - IngestionRun, - IntakeAllowlist, - NewsletterIntake, - UserFeedback, -) + from core.permissions import ( - IsProjectAdmin, - IsProjectContributor, - IsProjectFeedbackEditor, - IsProjectMember, - IsProjectMemberWritable, get_visible_projects_queryset, ) -from core.serializers import ( - ContentSerializer, - IngestionRunSerializer, - IntakeAllowlistSerializer, - NewsletterIntakeSerializer, - SkillResultSerializer, - UserFeedbackSerializer, -) from projects.models import Project -CLASSIFICATION_SKILL_NAME = "content_classification" -RELEVANCE_SKILL_NAME = "relevance_scoring" -SUMMARIZATION_SKILL_NAME = "summarization" -RELATED_CONTENT_SKILL_NAME = "find_related" - logger = logging.getLogger(__name__) PROJECT_ID_PARAMETER = OpenApiParameter( @@ -638,199 +611,3 @@ def perform_create(self, serializer): """Ensure nested resources are always created under the current project.""" serializer.save(project=self.get_project()) - - -@document_project_owned_viewset( - resource_plural="content items", - resource_singular="content item", - create_description="Create a new content item for the selected project. Any related entity must belong to the same project.", - tag="Content Library", - action_overrides=build_crud_action_overrides( - ContentSerializer, - resource_plural="content items for the selected project", - resource_singular="content item", - create_examples=[CONTENT_CREATE_REQUEST_EXAMPLE, CONTENT_RESPONSE_EXAMPLE], - create_response_examples=[CONTENT_RESPONSE_EXAMPLE], - retrieve_examples=[CONTENT_RESPONSE_EXAMPLE], - ), -) -class ContentViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Browse project content and trigger ad hoc AI processing for it.""" - - serializer_class = ContentSerializer - queryset = Content.objects.select_related("project", "entity") - - def get_permissions(self): - """Allow all members to read content, contributors to edit, and admins to delete.""" - - if self.action == "destroy": - permission_classes = [IsProjectAdmin] - elif self.action in {"create", "update", "partial_update", "run_skill"}: - permission_classes = [IsProjectMemberWritable] - else: - permission_classes = [IsProjectMember] - return [permission() for permission in permission_classes] - - @extend_schema( - summary="Run content skill", - description=( - "Run one ad hoc skill for the selected content item and persist the outcome as a SkillResult. " - "Supported skill names are content_classification, relevance_scoring, summarization, and find_related." - ), - tags=["AI Processing"], - parameters=[PROJECT_ID_PARAMETER, SKILL_NAME_PARAMETER], - request=None, - responses={ - 201: SkillResultSerializer, - 202: SkillResultSerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - ) - @action(detail=True, methods=["post"], url_path=r"skills/(?P[^/.]+)") - def run_skill(self, request, *args, **kwargs): - """Execute one supported ad hoc skill for a content item. - - Relevant and summarization requests are queued through Celery, while the - other supported skills execute inline and return their ``SkillResult`` - immediately. - """ - - from core.pipeline import execute_ad_hoc_skill - from core.tasks import queue_content_skill - - skill_name = str(kwargs["skill_name"]) - if skill_name not in { - CLASSIFICATION_SKILL_NAME, - RELEVANCE_SKILL_NAME, - SUMMARIZATION_SKILL_NAME, - RELATED_CONTENT_SKILL_NAME, - }: - raise serializers.ValidationError( - { - "skill_name": ( - "Unsupported skill. Choose one of: content_classification, relevance_scoring, " - "summarization, find_related." - ) - } - ) - - content = self.get_object() - if skill_name in {RELEVANCE_SKILL_NAME, SUMMARIZATION_SKILL_NAME}: - skill_result = queue_content_skill(content, skill_name) - serializer = SkillResultSerializer( - skill_result, context=self.get_serializer_context() - ) - return Response(serializer.data, status=status.HTTP_202_ACCEPTED) - - skill_result = execute_ad_hoc_skill(content, skill_name) - serializer = SkillResultSerializer( - skill_result, context=self.get_serializer_context() - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - -@document_project_owned_viewset( - resource_plural="user feedback entries", - resource_singular="user feedback entry", - create_description="Create a new feedback entry for content in the selected project. The authenticated user is recorded automatically.", - tag="Feedback", - action_overrides=build_crud_action_overrides( - UserFeedbackSerializer, - resource_plural="user feedback entries for the selected project", - resource_singular="user feedback entry", - ), -) -class UserFeedbackViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Capture editor feedback on project content items.""" - - serializer_class = UserFeedbackSerializer - queryset = UserFeedback.objects.select_related("content", "project", "user") - - def get_permissions(self): - """Allow all members to read feedback and owners or admins to modify it.""" - - return [IsProjectFeedbackEditor()] - - def perform_create(self, serializer): - """Attach the authenticated user automatically to new feedback rows.""" - - serializer.save(project=self.get_project(), user=self.request.user) - - -@document_project_owned_viewset( - resource_plural="ingestion runs", - resource_singular="ingestion run", - create_description="Create a new ingestion run record for the selected project to track a content ingestion attempt and its status.", - tag="Ingestion", - action_overrides=build_crud_action_overrides( - IngestionRunSerializer, - resource_plural="ingestion runs for the selected project", - resource_singular="ingestion run", - ), -) -class IngestionRunViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Inspect ingestion-run history for a project.""" - - serializer_class = IngestionRunSerializer - queryset = IngestionRun.objects.select_related("project") - - def get_permissions(self): - """Allow all members to read ingestion runs and contributors to manage them.""" - - if self.action in {"create", "update", "partial_update", "destroy"}: - permission_classes = [IsProjectMemberWritable] - else: - permission_classes = [IsProjectMember] - return [permission() for permission in permission_classes] - - -@document_project_owned_viewset( - resource_plural="intake allowlist entries", - resource_singular="intake allowlist entry", - create_description=( - "Create a new confirmed or pending sender allowlist entry for the selected " - "project's newsletter intake workflow." - ), - tag="Ingestion", - action_overrides=build_crud_action_overrides( - IntakeAllowlistSerializer, - resource_plural="intake allowlist entries for the selected project", - resource_singular="intake allowlist entry", - ), -) -class IntakeAllowlistViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Manage newsletter sender allowlist entries for a project.""" - - serializer_class = IntakeAllowlistSerializer - queryset = IntakeAllowlist.objects.select_related("project") - - def get_permissions(self): - """Restrict intake allowlist access to project contributors.""" - - return [IsProjectContributor()] - - -@document_project_owned_viewset( - resource_plural="newsletter intake entries", - resource_singular="newsletter intake entry", - create_description=( - "Newsletter intake entries are created by inbound email processing and are " - "exposed read-only for audit and troubleshooting." - ), - tag="Ingestion", - action_overrides=build_crud_action_overrides( - NewsletterIntakeSerializer, - resource_plural="newsletter intake entries for the selected project", - resource_singular="newsletter intake entry", - ), -) -class NewsletterIntakeViewSet(ProjectOwnedQuerysetMixin, viewsets.ReadOnlyModelViewSet): - """Inspect inbound newsletter history for a project.""" - - serializer_class = NewsletterIntakeSerializer - queryset = NewsletterIntake.objects.select_related("project") - - def get_permissions(self): - """Allow any project member to inspect newsletter intake history.""" - - return [IsProjectMember()] diff --git a/core/apps.py b/core/apps.py index c2c322dd..c0ce093b 100644 --- a/core/apps.py +++ b/core/apps.py @@ -4,6 +4,3 @@ class CoreConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "core" - - def ready(self) -> None: - import core.signals # noqa: F401 diff --git a/core/embeddings.py b/core/embeddings.py index f495f6f2..dc6f3ed6 100644 --- a/core/embeddings.py +++ b/core/embeddings.py @@ -27,7 +27,7 @@ VectorParams, ) -from core.models import Content +from content.models import Content from core.settings_types import CoreSettings from entities.models import Entity diff --git a/core/management/commands/embedding_smoke.py b/core/management/commands/embedding_smoke.py index 2735740f..3065ef3a 100644 --- a/core/management/commands/embedding_smoke.py +++ b/core/management/commands/embedding_smoke.py @@ -1,7 +1,7 @@ from django.core.management.base import BaseCommand, CommandError +from content.models import Content from core.embeddings import embed_text, upsert_content_embedding -from core.models import Content class Command(BaseCommand): diff --git a/core/management/commands/seed_demo.py b/core/management/commands/seed_demo.py index 478077d1..a1df699c 100644 --- a/core/management/commands/seed_demo.py +++ b/core/management/commands/seed_demo.py @@ -11,26 +11,23 @@ from httpx import HTTPError from qdrant_client.http.exceptions import ResponseHandlingException -from core.deduplication import canonicalize_url +from content.deduplication import canonicalize_url +from content.models import Content, FeedbackType, UserFeedback from core.embeddings import upsert_content_embedding -from core.models import ( - Content, - FeedbackType, - IngestionRun, - ReviewQueue, - ReviewReason, - ReviewResolution, - RunStatus, - SkillResult, - SkillStatus, - UserFeedback, -) from core.pipeline import ( CLASSIFICATION_SKILL_NAME, RELEVANCE_SKILL_NAME, SUMMARIZATION_SKILL_NAME, ) from entities.models import Entity, EntityType +from ingestion.models import IngestionRun, RunStatus +from pipeline.models import ( + ReviewQueue, + ReviewReason, + ReviewResolution, + SkillResult, + SkillStatus, +) from projects.model_support import SourcePluginName from projects.models import ( Project, diff --git a/core/management/commands/sync_embeddings.py b/core/management/commands/sync_embeddings.py index 4fbea211..39780a9c 100644 --- a/core/management/commands/sync_embeddings.py +++ b/core/management/commands/sync_embeddings.py @@ -1,7 +1,7 @@ from django.core.management.base import BaseCommand, CommandError +from content.models import Content from core.embeddings import upsert_content_embedding -from core.models import Content class Command(BaseCommand): diff --git a/core/migrations/0002_newsletter_intake.py b/core/migrations/0002_newsletter_intake.py index bb0fda5b..0bdd7f73 100644 --- a/core/migrations/0002_newsletter_intake.py +++ b/core/migrations/0002_newsletter_intake.py @@ -1,7 +1,19 @@ +import secrets + from django.db import migrations, models import django.db.models.deletion -import core.models + +def generate_project_intake_token() -> str: + """Generate the stable intake token used by the historical migration state.""" + + return secrets.token_hex(16) + + +def generate_confirmation_token() -> str: + """Generate the allowlist confirmation token used by the historical migration state.""" + + return secrets.token_urlsafe(24) class Migration(migrations.Migration): @@ -20,7 +32,7 @@ class Migration(migrations.Migration): model_name="project", name="intake_token", field=models.CharField( - default=core.models.generate_project_intake_token, + default=generate_project_intake_token, editable=False, max_length=64, unique=True, @@ -48,7 +60,7 @@ class Migration(migrations.Migration): ( "confirmation_token", models.CharField( - default=core.models.generate_confirmation_token, + default=generate_confirmation_token, max_length=64, unique=True, ), diff --git a/core/models.py b/core/models.py deleted file mode 100644 index 1addf5c4..00000000 --- a/core/models.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Core domain models plus compatibility re-exports during app decomposition. - -The admin, API, Celery tasks, and AI pipeline all revolve around the models in this -module. Adding model-level docstrings here gives Django admindocs a useful summary -of the core entities new contributors interact with first. -""" - -import secrets - -from content.models import Content, FeedbackType, UserFeedback -from entities.models import ( - Entity, - EntityAuthoritySnapshot, - EntityCandidate, - EntityCandidateStatus, - EntityMention, - EntityMentionRole, - EntityMentionSentiment, - EntityType, -) -from ingestion.models import IngestionRun, RunStatus -from newsletters.models import IntakeAllowlist, NewsletterIntake, NewsletterIntakeStatus -from pipeline.models import ReviewQueue as _ReviewQueue -from pipeline.models import ReviewReason as _ReviewReason -from pipeline.models import ReviewResolution as _ReviewResolution -from pipeline.models import SkillResult as _SkillResult -from pipeline.models import SkillStatus as _SkillStatus -from projects.models import Project as _Project -from trends.models import ContentClusterMembership as _ContentClusterMembership -from trends.models import ThemeSuggestion as _ThemeSuggestion -from trends.models import ThemeSuggestionStatus as _ThemeSuggestionStatus -from trends.models import TopicCentroidSnapshot as _TopicCentroidSnapshot -from trends.models import TopicCluster as _TopicCluster -from trends.models import TopicVelocitySnapshot as _TopicVelocitySnapshot - -Project = _Project -ReviewQueue = _ReviewQueue -ReviewReason = _ReviewReason -ReviewResolution = _ReviewResolution -SkillResult = _SkillResult -SkillStatus = _SkillStatus -ContentClusterMembership = _ContentClusterMembership -ThemeSuggestion = _ThemeSuggestion -ThemeSuggestionStatus = _ThemeSuggestionStatus -TopicCluster = _TopicCluster -TopicCentroidSnapshot = _TopicCentroidSnapshot -TopicVelocitySnapshot = _TopicVelocitySnapshot - -__all__ = [ - "Content", - "ContentClusterMembership", - "Entity", - "EntityAuthoritySnapshot", - "EntityCandidate", - "EntityCandidateStatus", - "EntityMention", - "EntityMentionRole", - "EntityMentionSentiment", - "EntityType", - "FeedbackType", - "IngestionRun", - "IntakeAllowlist", - "NewsletterIntake", - "NewsletterIntakeStatus", - "Project", - "RunStatus", - "ThemeSuggestion", - "ThemeSuggestionStatus", - "TopicCluster", - "TopicCentroidSnapshot", - "TopicVelocitySnapshot", - "UserFeedback", -] - - -def generate_project_intake_token() -> str: - """Generate the stable token used in project-specific intake email aliases. - - Returns: - A random hex token that can be embedded in addresses like - ``intake+@...`` to route inbound newsletters to a project. - """ - - from projects.model_support import generate_project_intake_token as _generate_token - - return _generate_token() - - -def generate_confirmation_token() -> str: - """Generate a one-time token for newsletter sender confirmation links. - - Returns: - A URL-safe random token stored on an allowlist entry until the sender - confirms newsletter intake access. - """ - - return secrets.token_urlsafe(24) diff --git a/core/newsletters.py b/core/newsletters.py deleted file mode 100644 index 813222b2..00000000 --- a/core/newsletters.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Compatibility exports for newsletter intake helpers.""" - -from newsletters.intake import ( - build_confirmation_url, - extract_newsletter_items, - normalize_sender_email, - process_inbound_newsletter, - queue_newsletter_intake, - sanitize_newsletter_html, - send_confirmation_email, -) - -__all__ = [ - "build_confirmation_url", - "extract_newsletter_items", - "normalize_sender_email", - "process_inbound_newsletter", - "queue_newsletter_intake", - "sanitize_newsletter_html", - "send_confirmation_email", -] diff --git a/core/pipeline.py b/core/pipeline.py index 97015fc0..ca93ae5e 100644 --- a/core/pipeline.py +++ b/core/pipeline.py @@ -18,8 +18,8 @@ from django.utils import timezone from langgraph.graph import END, StateGraph +from content.deduplication import canonicalize_url from content.models import Content -from core.deduplication import canonicalize_url from core.embeddings import ( build_content_embedding_text, embed_text, @@ -27,8 +27,8 @@ get_topic_centroid_similarity, search_similar_content, ) -from core.entity_extraction import run_entity_extraction from core.llm import build_skill_user_prompt, get_skill_definition, openrouter_chat_json +from entities.extraction import run_entity_extraction from entities.models import EntityMention from pipeline.models import ReviewQueue, ReviewReason, SkillResult, SkillStatus diff --git a/core/plugins/__init__.py b/core/plugins/__init__.py deleted file mode 100644 index cd50803b..00000000 --- a/core/plugins/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Compatibility wrappers for the ingestion plugin registry.""" - -from ingestion.plugins import get_plugin_for_source_config, validate_plugin_config - -__all__ = ["get_plugin_for_source_config", "validate_plugin_config"] diff --git a/core/plugins/base.py b/core/plugins/base.py deleted file mode 100644 index 00801b41..00000000 --- a/core/plugins/base.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Compatibility wrapper for the ingestion plugin base types.""" - -from ingestion.plugins.base import * # noqa: F403 diff --git a/core/plugins/bluesky.py b/core/plugins/bluesky.py deleted file mode 100644 index f402666b..00000000 --- a/core/plugins/bluesky.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Compatibility wrapper for the Bluesky source plugin.""" - -from ingestion.plugins.bluesky import * # noqa: F403 diff --git a/core/plugins/mastodon.py b/core/plugins/mastodon.py deleted file mode 100644 index cdef11d2..00000000 --- a/core/plugins/mastodon.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Compatibility wrapper for the Mastodon source plugin.""" - -from mastodon import Mastodon - -from ingestion.plugins.mastodon import MastodonSourcePlugin - -__all__ = ["Mastodon", "MastodonSourcePlugin"] diff --git a/core/plugins/reddit.py b/core/plugins/reddit.py deleted file mode 100644 index f9cd3f0b..00000000 --- a/core/plugins/reddit.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Compatibility wrapper for the Reddit source plugin.""" - -from ingestion.plugins.reddit import * # noqa: F403 diff --git a/core/plugins/registry.py b/core/plugins/registry.py deleted file mode 100644 index 6b40bc04..00000000 --- a/core/plugins/registry.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Compatibility wrapper for the ingestion plugin registry.""" - -from ingestion.plugins.registry import * # noqa: F403 diff --git a/core/plugins/rss.py b/core/plugins/rss.py deleted file mode 100644 index ca0cdb64..00000000 --- a/core/plugins/rss.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Compatibility wrapper for the RSS source plugin.""" - -from ingestion.plugins.rss import * # noqa: F403 diff --git a/core/serializer_mixins.py b/core/serializer_mixins.py index a9209547..7983a0c0 100644 --- a/core/serializer_mixins.py +++ b/core/serializer_mixins.py @@ -4,9 +4,10 @@ from rest_framework import serializers -from core.models import Content, SkillResult +from content.models import Content from core.permissions import get_visible_projects_queryset from entities.models import Entity +from pipeline.models import SkillResult class ProjectScopedSerializerMixin: diff --git a/core/serializers.py b/core/serializers.py deleted file mode 100644 index 87a91cdf..00000000 --- a/core/serializers.py +++ /dev/null @@ -1,235 +0,0 @@ -"""DRF serializers for project-scoped core models and compatibility exports.""" - -from importlib import import_module -from typing import TYPE_CHECKING, Any - -from rest_framework import serializers - -from core.models import ( - Content, - IngestionRun, - IntakeAllowlist, - NewsletterIntake, - UserFeedback, -) -from core.serializer_mixins import ProjectScopedSerializerMixin - -if TYPE_CHECKING: - from entities.serializers import ( - EntityAuthoritySnapshotSerializer, - EntityCandidateMergeSerializer, - EntityCandidateSerializer, - EntityMentionSummarySerializer, - EntitySerializer, - ) - from pipeline.serializers import ReviewQueueSerializer, SkillResultSerializer - from trends.serializers import ( - TopicCentroidObservabilitySummarySerializer, - TopicCentroidSnapshotSerializer, - ) - -_COMPAT_SERIALIZER_EXPORTS = { - "EntityAuthoritySnapshotSerializer": ( - "entities.serializers", - "EntityAuthoritySnapshotSerializer", - ), - "EntityCandidateMergeSerializer": ( - "entities.serializers", - "EntityCandidateMergeSerializer", - ), - "EntityCandidateSerializer": ( - "entities.serializers", - "EntityCandidateSerializer", - ), - "EntityMentionSummarySerializer": ( - "entities.serializers", - "EntityMentionSummarySerializer", - ), - "EntitySerializer": ("entities.serializers", "EntitySerializer"), - "ReviewQueueSerializer": ("pipeline.serializers", "ReviewQueueSerializer"), - "SkillResultSerializer": ("pipeline.serializers", "SkillResultSerializer"), - "TopicCentroidObservabilitySummarySerializer": ( - "trends.serializers", - "TopicCentroidObservabilitySummarySerializer", - ), - "TopicCentroidSnapshotSerializer": ( - "trends.serializers", - "TopicCentroidSnapshotSerializer", - ), -} - -__all__ = [ - "ProjectScopedSerializerMixin", - "EntityAuthoritySnapshotSerializer", - "EntityCandidateMergeSerializer", - "EntityCandidateSerializer", - "EntityMentionSummarySerializer", - "EntitySerializer", - "ReviewQueueSerializer", - "SkillResultSerializer", - "TopicCentroidObservabilitySummarySerializer", - "TopicCentroidSnapshotSerializer", -] - - -def __getattr__(name: str) -> Any: - """Resolve compatibility serializer re-exports lazily.""" - - try: - module_name, attribute_name = _COMPAT_SERIALIZER_EXPORTS[name] - except KeyError as exc: - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from exc - - value = getattr(import_module(module_name), attribute_name) - globals()[name] = value - return value - - -class ContentSerializer(ProjectScopedSerializerMixin, serializers.ModelSerializer): - """Serialize ingested content items and enforce project/entity consistency.""" - - class Meta: - model = Content - fields = [ - "id", - "project", - "url", - "title", - "author", - "entity", - "source_plugin", - "content_type", - "canonical_url", - "published_date", - "ingested_at", - "content_text", - "relevance_score", - "authority_adjusted_score", - "embedding_id", - "source_metadata", - "duplicate_of", - "duplicate_signal_count", - "is_reference", - "is_active", - ] - read_only_fields = [ - "id", - "project", - "canonical_url", - "ingested_at", - "authority_adjusted_score", - "embedding_id", - "duplicate_of", - "duplicate_signal_count", - ] - - def validate(self, attrs): - """Reject entity assignments that point at a different project.""" - - project = ( - self.context.get("project") - or attrs.get("project") - or getattr(self.instance, "project", None) - ) - entity = attrs.get("entity") or getattr(self.instance, "entity", None) - if project and entity and entity.project_id != project.id: - raise serializers.ValidationError( - {"entity": "Entity must belong to the selected project."} - ) - return attrs - - -class UserFeedbackSerializer(ProjectScopedSerializerMixin, serializers.ModelSerializer): - """Serialize editor feedback attached to a content item.""" - - user = serializers.PrimaryKeyRelatedField(read_only=True) - - class Meta: - model = UserFeedback - fields = ["id", "content", "project", "user", "feedback_type", "created_at"] - read_only_fields = ["id", "project", "user", "created_at"] - - def validate(self, attrs): - """Reject feedback that targets content outside the active project.""" - - project = ( - self.context.get("project") - or attrs.get("project") - or getattr(self.instance, "project", None) - ) - content = attrs.get("content") or getattr(self.instance, "content", None) - if project and content and content.project_id != project.id: - raise serializers.ValidationError( - {"content": "Content must belong to the selected project."} - ) - return attrs - - -class IngestionRunSerializer(ProjectScopedSerializerMixin, serializers.ModelSerializer): - """Serialize ingestion-run audit records.""" - - class Meta: - model = IngestionRun - fields = [ - "id", - "project", - "plugin_name", - "started_at", - "completed_at", - "status", - "items_fetched", - "items_ingested", - "error_message", - ] - read_only_fields = ["id", "project", "started_at"] - - -class IntakeAllowlistSerializer( - ProjectScopedSerializerMixin, serializers.ModelSerializer -): - """Serialize confirmed and pending newsletter sender allowlist entries.""" - - is_confirmed = serializers.BooleanField(read_only=True) - - class Meta: - model = IntakeAllowlist - fields = [ - "id", - "project", - "sender_email", - "is_confirmed", - "confirmed_at", - "confirmation_token", - "created_at", - ] - read_only_fields = ["id", "project", "confirmation_token", "created_at"] - - -class NewsletterIntakeSerializer( - ProjectScopedSerializerMixin, serializers.ModelSerializer -): - """Serialize raw inbound newsletter messages captured for a project.""" - - class Meta: - model = NewsletterIntake - fields = [ - "id", - "project", - "sender_email", - "subject", - "received_at", - "raw_html", - "raw_text", - "message_id", - "status", - "extraction_result", - "error_message", - ] - read_only_fields = [ - "id", - "project", - "received_at", - "status", - "extraction_result", - "error_message", - ] diff --git a/core/tasks.py b/core/tasks.py index cfde1cba..7b792600 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -4,8 +4,7 @@ import math from collections import defaultdict from datetime import timedelta -from importlib import import_module -from typing import TYPE_CHECKING, Any, Protocol, cast +from typing import Protocol, cast from celery import shared_task from django.conf import settings @@ -13,14 +12,8 @@ from django.db.models import Count, Model from django.utils import timezone -from core.embeddings import ( - upsert_content_embedding, -) -from core.models import ( - Content, - FeedbackType, - UserFeedback, -) +from content.models import Content, FeedbackType, UserFeedback +from core.embeddings import upsert_content_embedding from core.pipeline import ( RELEVANCE_SKILL_NAME, SUMMARIZATION_SKILL_NAME, @@ -44,93 +37,16 @@ EntityMentionRole.SUBJECT, ) -if TYPE_CHECKING: - from ingestion.tasks import run_all_ingestions, run_ingestion - from newsletters.tasks import process_newsletter_intake - from trends.tasks import ( - TOPIC_CENTROID_MIN_UPVOTES, - assign_content_to_topic_cluster, - generate_theme_suggestions, - queue_topic_centroid_recompute, - recompute_topic_centroid, - recompute_topic_clusters, - recompute_topic_velocity, - run_all_topic_centroid_recomputations, - run_all_topic_cluster_recomputations, - ) - -_COMPAT_TASK_EXPORTS = { - "process_newsletter_intake": ( - "newsletters.tasks", - "process_newsletter_intake", - ), - "run_all_ingestions": ("ingestion.tasks", "run_all_ingestions"), - "run_ingestion": ("ingestion.tasks", "run_ingestion"), - "assign_content_to_topic_cluster": ( - "trends.tasks", - "assign_content_to_topic_cluster", - ), - "generate_theme_suggestions": ( - "trends.tasks", - "generate_theme_suggestions", - ), - "TOPIC_CENTROID_MIN_UPVOTES": ( - "trends.tasks", - "TOPIC_CENTROID_MIN_UPVOTES", - ), - "recompute_topic_clusters": ("trends.tasks", "recompute_topic_clusters"), - "queue_topic_centroid_recompute": ( - "trends.tasks", - "queue_topic_centroid_recompute", - ), - "recompute_topic_centroid": ("trends.tasks", "recompute_topic_centroid"), - "recompute_topic_velocity": ("trends.tasks", "recompute_topic_velocity"), - "run_all_topic_cluster_recomputations": ( - "trends.tasks", - "run_all_topic_cluster_recomputations", - ), - "run_all_topic_centroid_recomputations": ( - "trends.tasks", - "run_all_topic_centroid_recomputations", - ), -} - __all__ = [ - "process_newsletter_intake", - "run_all_ingestions", - "assign_content_to_topic_cluster", - "generate_theme_suggestions", - "run_ingestion", - "TOPIC_CENTROID_MIN_UPVOTES", - "queue_topic_centroid_recompute", "recompute_authority_scores", - "recompute_topic_clusters", - "recompute_topic_centroid", - "recompute_topic_velocity", "run_all_authority_recomputations", - "run_all_topic_cluster_recomputations", - "run_all_topic_centroid_recomputations", "run_relevance_scoring_skill", "run_summarization_skill", "queue_content_skill", "process_content", - "upsert_content_embedding", ] -def __getattr__(name: str) -> Any: - """Resolve compatibility task re-exports lazily.""" - - try: - module_name, attribute_name = _COMPAT_TASK_EXPORTS[name] - except KeyError as exc: - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from exc - - value = getattr(import_module(module_name), attribute_name) - globals()[name] = value - return value - - class DelayedTask(Protocol): """Protocol for Celery tasks that can run eagerly or via ``delay``.""" diff --git a/core/tests/test_admin.py b/core/tests/test_admin.py deleted file mode 100644 index 595eeab1..00000000 --- a/core/tests/test_admin.py +++ /dev/null @@ -1,1600 +0,0 @@ -from datetime import timedelta -from types import SimpleNamespace -from typing import Any, cast -from unittest.mock import ANY, Mock - -import pytest -from django.contrib import messages -from django.contrib.admin.sites import AdminSite -from django.db.models import Model -from django.http import HttpRequest -from django.test import RequestFactory -from django.utils import timezone - -from content.admin import ContentAdmin, UserFeedbackAdmin -from core.admin import ( - DuplicateStateFilter, - HighValueFilter, - ReviewQueueAdmin, - SkillResultAdmin, - TopicCentroidSnapshotAdmin, -) -from core.models import ( - Content, - Entity, - EntityAuthoritySnapshot, - EntityCandidate, - EntityCandidateStatus, - EntityMention, - IngestionRun, - ReviewQueue, - ReviewReason, - RunStatus, - SkillResult, - TopicCentroidSnapshot, - UserFeedback, -) -from entities.admin import ( - EntityAdmin, - EntityAuthoritySnapshotAdmin, - EntityCandidateAdmin, -) -from ingestion.admin import IngestionRunAdmin -from projects.admin import ( - BlueskyCredentialsAdmin, - BlueskyCredentialsAdminForm, - MastodonCredentialsAdmin, - MastodonCredentialsAdminForm, - ProjectConfigAdmin, - SourceConfigAdmin, -) -from projects.model_support import SourcePluginName -from projects.models import ( - BlueskyCredentials, - MastodonCredentials, - Project, - ProjectConfig, - SourceConfig, -) - -pytestmark = pytest.mark.django_db - - -def _require_pk(instance: Model) -> int: - """Return a saved model primary key for typed admin test assertions.""" - - instance_pk = instance.pk - if instance_pk is None: - raise ValueError(f"{instance.__class__.__name__} must be saved first.") - return int(instance_pk) - - -def _create_user(user_model: Any, **kwargs: object): - """Create a user through the custom manager with a typed escape hatch.""" - - return cast(Any, user_model.objects).create_user(**kwargs) - - -def _request(query_params: dict[str, str] | None = None) -> HttpRequest: - """Build a typed request object for admin actions and filters.""" - - return RequestFactory().get("/admin/", data=query_params or {}) - - -def _params(**kwargs: str) -> dict[str, list[str]]: - """Build typed admin filter params.""" - - return {key: [value] for key, value in kwargs.items()} - - -def _message_user_mock(admin_instance: Any, mocker: Any) -> Mock: - """Install a mock for ModelAdmin.message_user and return it for assertions.""" - - message_mock = cast(Mock, mocker.Mock()) - admin_instance.message_user = message_mock - return message_mock - - -def _context(response: object) -> dict[str, Any]: - """Cast admin changelist extra_context payloads for typed assertions.""" - - return cast(dict[str, Any], response) - - -def _dashboard_stats(response: object) -> list[dict[str, Any]]: - """Return typed dashboard stats rows from a changelist extra_context payload.""" - - return cast(list[dict[str, Any]], _context(response)["dashboard_stats"]) - - -def _drilldowns(response: object) -> list[dict[str, Any]]: - """Return typed centroid drilldowns from a changelist extra_context payload.""" - - return cast(list[dict[str, Any]], _context(response)["centroid_project_drilldowns"]) - - -@pytest.fixture -def source_admin_context(django_user_model): - user = _create_user( - django_user_model, username="admin-owner", password="testpass123" - ) - project = Project.objects.create(name="Admin Project", topic_description="Infra") - return SimpleNamespace(user=user, project=project) - - -def test_test_source_connection_reports_success(source_admin_context, mocker): - source_config = SourceConfig.objects.create( - project=source_admin_context.project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/feed.xml"}, - ) - plugin = mocker.Mock() - plugin.health_check.return_value = True - validate_mock = mocker.patch( - "projects.admin.validate_plugin_config", - return_value={"feed_url": "https://example.com/feed.xml"}, - ) - get_plugin_mock = mocker.patch( - "projects.admin.get_plugin_for_source_config", return_value=plugin - ) - admin_instance = SourceConfigAdmin(SourceConfig, AdminSite()) - message_user_mock = _message_user_mock(admin_instance, mocker) - - admin_instance.test_source_connection( - request=_request(), - queryset=SourceConfig.objects.filter(pk=source_config.pk), - ) - - validate_mock.assert_called_once_with( - SourcePluginName.RSS, {"feed_url": "https://example.com/feed.xml"} - ) - get_plugin_mock.assert_called_once() - plugin.health_check.assert_called_once_with() - message_user_mock.assert_called_once_with( - ANY, - "Connectivity check passed for 1 source(s).", - messages.SUCCESS, - ) - - -def test_project_config_admin_exposes_centroid_toggle_field(source_admin_context): - config = ProjectConfig.objects.create(project=source_admin_context.project) - admin_instance = ProjectConfigAdmin(ProjectConfig, AdminSite()) - - assert "recompute_topic_centroid_on_feedback_save" in admin_instance.list_display - assert "recompute_topic_centroid_on_feedback_save" in admin_instance.list_filter - assert "recompute_topic_centroid_on_feedback_save" in admin_instance.get_fields( - request=_request(), obj=config - ) - - -def test_topic_centroid_snapshot_admin_renders_drift_fields(source_admin_context): - snapshot = TopicCentroidSnapshot.objects.create( - project=source_admin_context.project, - centroid_active=True, - centroid_vector=[1.0, 0.0], - feedback_count=15, - upvote_count=12, - downvote_count=3, - drift_from_previous=0.125, - drift_from_week_ago=0.4, - ) - admin_instance = TopicCentroidSnapshotAdmin(TopicCentroidSnapshot, AdminSite()) - - assert admin_instance.display_drift_from_previous(snapshot) == "12.5%" - assert admin_instance.display_drift_from_week_ago(snapshot) == "40.0%" - - -def test_topic_centroid_snapshot_admin_changelist_view_builds_dashboard_stats( - source_admin_context, mocker -): - second_project = Project.objects.create( - name="Second Admin Project", - topic_description="Analytics", - ) - fixed_now = timezone.now() - recent_snapshot = TopicCentroidSnapshot.objects.create( - project=source_admin_context.project, - centroid_active=True, - centroid_vector=[1.0, 0.0], - feedback_count=18, - upvote_count=14, - downvote_count=4, - drift_from_previous=0.1, - drift_from_week_ago=0.2, - ) - stale_snapshot = TopicCentroidSnapshot.objects.create( - project=second_project, - centroid_active=False, - centroid_vector=[], - feedback_count=2, - upvote_count=1, - downvote_count=1, - ) - TopicCentroidSnapshot.objects.filter(pk=recent_snapshot.pk).update( - computed_at=fixed_now - timedelta(hours=6) - ) - TopicCentroidSnapshot.objects.filter(pk=stale_snapshot.pk).update( - computed_at=fixed_now - timedelta(days=2) - ) - admin_instance = TopicCentroidSnapshotAdmin(TopicCentroidSnapshot, AdminSite()) - mocker.patch.object( - admin_instance, - "get_queryset", - return_value=TopicCentroidSnapshot.objects.all(), - ) - super_changelist_view = mocker.patch( - "django.contrib.admin.options.ModelAdmin.changelist_view", - side_effect=lambda request, extra_context=None: extra_context, - ) - mocker.patch("trends.admin.timezone.now", return_value=fixed_now) - - response = admin_instance.changelist_view(request=_request()) - dashboard_stats = _dashboard_stats(response) - centroid_project_drilldowns = _drilldowns(response) - - super_changelist_view.assert_called_once() - assert ( - admin_instance.list_before_template - == "admin/topic_centroid_snapshot_changelist_widget.html" - ) - assert dashboard_stats[0]["value"] == "1 / 2" - assert dashboard_stats[0]["color"] == "warning" - assert dashboard_stats[1]["value"] == "10.0%" - assert dashboard_stats[1]["color"] == "success" - assert dashboard_stats[2]["value"] == "20.0%" - assert dashboard_stats[2]["color"] == "warning" - assert dashboard_stats[3]["value"] == "6h ago" - assert dashboard_stats[3]["color"] == "success" - assert len(centroid_project_drilldowns) == 2 - assert centroid_project_drilldowns[0]["project_name"] == "Admin Project" - assert centroid_project_drilldowns[0]["href"] == ( - "/admin/trends/topiccentroidsnapshot/?project__id__exact=" - f"{_require_pk(source_admin_context.project)}" - ) - assert centroid_project_drilldowns[0]["drift_from_previous"] == "10.0%" - - -def test_test_source_connection_reports_failures(source_admin_context, mocker): - source_config = SourceConfig.objects.create( - project=source_admin_context.project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/feed.xml"}, - ) - mocker.patch( - "projects.admin.validate_plugin_config", - side_effect=ValueError("Missing required config field: feed_url"), - ) - admin_instance = SourceConfigAdmin(SourceConfig, AdminSite()) - message_user_mock = _message_user_mock(admin_instance, mocker) - - admin_instance.test_source_connection( - request=_request(), - queryset=SourceConfig.objects.filter(pk=source_config.pk), - ) - - message_user_mock.assert_called_once_with( - ANY, - "Connectivity check failed for: rss source for Admin Project: Missing required config field: feed_url", - messages.ERROR, - ) - - -def test_source_config_display_health_renders_without_django6_format_html_error( - source_admin_context, -): - source_config = SourceConfig.objects.create( - project=source_admin_context.project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/feed.xml"}, - is_active=True, - last_fetched_at=timezone.now(), - ) - admin_instance = SourceConfigAdmin(SourceConfig, AdminSite()) - - rendered = admin_instance.display_health(source_config) - - assert "Healthy" in rendered - - -def test_review_queue_changelist_view_builds_dashboard_stats( - source_admin_context, mocker -): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/review-item", - title="Review Item", - author="Reviewer", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Review queue content", - ) - ReviewQueue.objects.create( - project=source_admin_context.project, - content=content, - reason=ReviewReason.BORDERLINE_RELEVANCE, - confidence=0.42, - resolved=False, - ) - admin_instance = ReviewQueueAdmin(ReviewQueue, AdminSite()) - mocker.patch.object( - admin_instance, "get_queryset", return_value=ReviewQueue.objects.all() - ) - super_changelist_view = mocker.patch( - "core.admin.ModelAdmin.changelist_view", - side_effect=lambda request, extra_context=None: extra_context, - ) - - response = admin_instance.changelist_view(request=_request()) - dashboard_stats = _dashboard_stats(response) - - super_changelist_view.assert_called_once() - assert dashboard_stats[0]["value"] == 1 - assert dashboard_stats[1]["value"] == "42%" - - -def test_review_queue_display_confidence_renders_without_django6_format_error( - source_admin_context, -): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/review-confidence", - title="Review Confidence", - author="Reviewer", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Review queue content", - ) - review_item = ReviewQueue.objects.create( - project=source_admin_context.project, - content=content, - reason=ReviewReason.BORDERLINE_RELEVANCE, - confidence=0.42, - resolved=False, - ) - admin_instance = ReviewQueueAdmin(ReviewQueue, AdminSite()) - - rendered = admin_instance.display_confidence(review_item) - - assert "42%" in rendered - - -def test_bluesky_credentials_admin_form_encrypts_app_password(source_admin_context): - form = BlueskyCredentialsAdminForm( - data={ - "project": _require_pk(source_admin_context.project), - "handle": "@Alice.BSKY.social", - "credential_input": "app-password", - "pds_url": "https://pds.example.com/xrpc/", - "is_active": True, - } - ) - - assert form.is_valid(), form.errors - credentials = form.save() - - assert credentials.handle == "alice.bsky.social" - assert credentials.pds_url == "https://pds.example.com" - assert credentials.has_app_password() is True - assert credentials.get_app_password() == "app-password" - - -def test_verify_selected_bluesky_credentials_reports_success( - source_admin_context, mocker -): - credentials = BlueskyCredentials.objects.create( - project=source_admin_context.project, - handle="alice.bsky.social", - app_password_encrypted="ciphertext", - ) - verify_mock = mocker.patch( - "core.plugins.bluesky.BlueskySourcePlugin.verify_credentials" - ) - admin_instance = BlueskyCredentialsAdmin(BlueskyCredentials, AdminSite()) - message_user_mock = _message_user_mock(admin_instance, mocker) - - admin_instance.verify_selected_credentials( - request=_request(), - queryset=BlueskyCredentials.objects.filter(pk=credentials.pk), - ) - - verify_mock.assert_called_once_with(credentials) - message_user_mock.assert_called_once_with( - ANY, - "Credential verification passed for 1 account(s).", - messages.SUCCESS, - ) - - -def test_verify_selected_bluesky_credentials_reports_failures( - source_admin_context, mocker -): - credentials = BlueskyCredentials.objects.create( - project=source_admin_context.project, - handle="alice.bsky.social", - app_password_encrypted="ciphertext", - ) - mocker.patch( - "core.plugins.bluesky.BlueskySourcePlugin.verify_credentials", - side_effect=RuntimeError("bad login"), - ) - admin_instance = BlueskyCredentialsAdmin(BlueskyCredentials, AdminSite()) - message_user_mock = _message_user_mock(admin_instance, mocker) - - admin_instance.verify_selected_credentials( - request=_request(), - queryset=BlueskyCredentials.objects.filter(pk=credentials.pk), - ) - - message_user_mock.assert_called_once_with( - ANY, - "Credential verification failed for: Bluesky credentials for Admin Project: bad login", - messages.ERROR, - ) - - -def test_mastodon_credentials_admin_form_encrypts_access_token(source_admin_context): - form = MastodonCredentialsAdminForm( - data={ - "project": _require_pk(source_admin_context.project), - "instance_url": "https://hachyderm.io/@alice/", - "account_acct": "@Alice", - "credential_input": "access-token", - "is_active": True, - } - ) - - assert form.is_valid(), form.errors - credentials = form.save() - - assert credentials.instance_url == "https://hachyderm.io" - assert credentials.account_acct == "alice@hachyderm.io" - assert credentials.has_access_token() is True - assert credentials.get_access_token() == "access-token" - - -def test_verify_selected_mastodon_credentials_reports_success( - source_admin_context, mocker -): - credentials = MastodonCredentials.objects.create( - project=source_admin_context.project, - instance_url="https://hachyderm.io", - account_acct="alice@hachyderm.io", - access_token_encrypted="ciphertext", - ) - verify_mock = mocker.patch( - "core.plugins.mastodon.MastodonSourcePlugin.verify_credentials" - ) - admin_instance = MastodonCredentialsAdmin(MastodonCredentials, AdminSite()) - message_user_mock = _message_user_mock(admin_instance, mocker) - - admin_instance.verify_selected_credentials( - request=_request(), - queryset=MastodonCredentials.objects.filter(pk=credentials.pk), - ) - - verify_mock.assert_called_once_with(credentials) - message_user_mock.assert_called_once_with( - ANY, - "Credential verification passed for 1 account(s).", - messages.SUCCESS, - ) - - -def test_verify_selected_mastodon_credentials_reports_failures( - source_admin_context, mocker -): - credentials = MastodonCredentials.objects.create( - project=source_admin_context.project, - instance_url="https://hachyderm.io", - account_acct="alice@hachyderm.io", - access_token_encrypted="ciphertext", - ) - mocker.patch( - "core.plugins.mastodon.MastodonSourcePlugin.verify_credentials", - side_effect=RuntimeError("bad token"), - ) - admin_instance = MastodonCredentialsAdmin(MastodonCredentials, AdminSite()) - message_user_mock = _message_user_mock(admin_instance, mocker) - - admin_instance.verify_selected_credentials( - request=_request(), - queryset=MastodonCredentials.objects.filter(pk=credentials.pk), - ) - - message_user_mock.assert_called_once_with( - ANY, - "Credential verification failed for: Mastodon credentials for Admin Project: bad token", - messages.ERROR, - ) - - -def test_ingestion_run_display_efficiency_renders_without_django6_format_error( - source_admin_context, -): - run = IngestionRun.objects.create( - project=source_admin_context.project, - plugin_name=SourcePluginName.RSS, - status=RunStatus.SUCCESS, - items_fetched=12, - items_ingested=9, - ) - admin_instance = IngestionRunAdmin(IngestionRun, AdminSite()) - - rendered = admin_instance.display_efficiency(run) - - assert "75%" in rendered - - -def test_content_preview_uses_content_text(source_admin_context): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-preview", - title="Admin Preview", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="A short preview from the content body.", - ) - admin_instance = ContentAdmin(Content, AdminSite()) - - preview = admin_instance.preview_content(content) - - assert 'title="A short preview from the content body."' in preview - - -def test_content_preview_returns_dash_when_content_text_blank(source_admin_context): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-preview-empty", - title="Admin Preview Empty", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text=" ", - ) - admin_instance = ContentAdmin(Content, AdminSite()) - - assert admin_instance.preview_content(content) == "-" - - -def test_content_view_trace_prefers_external_trace_url(source_admin_context): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-trace", - title="Admin Trace", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Trace content.", - ) - SkillResult.objects.create( - content=content, - project=source_admin_context.project, - skill_name="summarization", - status="COMPLETED", - result_data={"trace_url": "https://traces.example/run/123"}, - ) - admin_instance = ContentAdmin(Content, AdminSite()) - - rendered = admin_instance.view_trace(content) - - assert "https://traces.example/run/123" in rendered - assert "📈 Trace" in rendered - - -def test_content_view_trace_falls_back_to_skill_runs_changelist(source_admin_context): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-trace-fallback", - title="Admin Trace Fallback", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Trace fallback content.", - ) - SkillResult.objects.create( - content=content, - project=source_admin_context.project, - skill_name="relevance_scoring", - status="COMPLETED", - result_data={"relevance_score": 0.9}, - ) - admin_instance = ContentAdmin(Content, AdminSite()) - - rendered = admin_instance.view_trace(content) - - assert "🧠 Skill runs" in rendered - assert f"content__id__exact={_require_pk(content)}" in rendered - - -def test_content_changelist_view_builds_dashboard_stats(source_admin_context, mocker): - Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-dashboard-1", - title="Admin Dashboard 1", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Content one.", - relevance_score=0.8, - authority_adjusted_score=0.85, - ) - Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-dashboard-2", - title="Admin Dashboard 2", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Content two.", - relevance_score=0.4, - authority_adjusted_score=0.45, - ) - admin_instance = ContentAdmin(Content, AdminSite()) - mocker.patch.object( - admin_instance, "get_queryset", return_value=Content.objects.all() - ) - super_changelist_view = mocker.patch( - "django.contrib.admin.options.ModelAdmin.changelist_view", - side_effect=lambda request, extra_context=None: extra_context, - ) - - response = admin_instance.changelist_view(request=_request()) - dashboard_stats = _dashboard_stats(response) - - super_changelist_view.assert_called_once() - assert dashboard_stats[0]["value"] == "60.0%" - assert dashboard_stats[1]["value"] == "65.0%" - assert dashboard_stats[2]["value"] == 2 - - -def test_content_admin_score_columns_render_expected_values(source_admin_context): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-scores", - title="Admin Scores", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Score rendering content.", - relevance_score=0.8, - authority_adjusted_score=0.86, - ) - admin_instance = ContentAdmin(Content, AdminSite()) - - rendered_base = admin_instance.display_relevance(content) - rendered_adjusted = admin_instance.display_authority_adjusted_score(content) - - assert "80.0%" in rendered_base - assert "green" in rendered_base - assert "86.0%" in rendered_adjusted - assert "green" in rendered_adjusted - - -def test_generate_newsletter_ideas_queues_selected_content( - source_admin_context, mocker -): - first_content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-queue-1", - title="Admin Queue 1", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Queue one.", - ) - second_content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-queue-2", - title="Admin Queue 2", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Queue two.", - ) - delay_mock = mocker.patch("core.tasks.process_content.delay") - admin_instance = ContentAdmin(Content, AdminSite()) - message_user_mock = _message_user_mock(admin_instance, mocker) - - admin_instance.generate_newsletter_ideas( - request=_request(), - queryset=Content.objects.filter( - id__in=[_require_pk(first_content), _require_pk(second_content)] - ).order_by("id"), - ) - - delay_mock.assert_any_call(_require_pk(first_content)) - delay_mock.assert_any_call(_require_pk(second_content)) - assert delay_mock.call_count == 2 - message_user_mock.assert_called_once_with( - ANY, - "Successfully queued the pipeline for 2 items.", - messages.SUCCESS, - ) - - -def test_content_admin_duplicate_columns_render_expected_values(source_admin_context): - canonical = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-canonical", - canonical_url="https://example.com/admin-canonical", - title="Canonical Story", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Canonical content.", - duplicate_signal_count=2, - ) - duplicate = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-canonical?utm_source=reddit", - canonical_url="https://example.com/admin-canonical", - title="Duplicate Story", - author="Editor", - source_plugin=SourcePluginName.REDDIT, - published_date=timezone.now(), - content_text="Duplicate content.", - duplicate_of=canonical, - is_active=False, - ) - admin_instance = ContentAdmin(Content, AdminSite()) - - assert "Also seen in 2 source(s)" in admin_instance.duplicate_badge(canonical) - assert admin_instance.duplicate_badge(duplicate) == "-" - assert admin_instance.duplicate_parent(canonical) == "-" - assert admin_instance.duplicate_parent(duplicate) == "Canonical Story" - - -@pytest.mark.parametrize( - ("authority_score", "expected_color", "expected_display"), - [ - (0.9, "green", "90.0%"), - (0.6, "orange", "60.0%"), - (0.2, "red", "20.0%"), - ], -) -def test_entity_colored_score_uses_expected_color( - source_admin_context, authority_score, expected_color, expected_display -): - entity = Entity.objects.create( - project=source_admin_context.project, - name=f"Entity {authority_score}", - type="vendor", - authority_score=authority_score, - website_url=f"https://entity-{authority_score}.example.com", - ) - admin_instance = EntityAdmin(Entity, AdminSite()) - - rendered = admin_instance.colored_score(entity) - - assert expected_color in rendered - assert expected_display in rendered - - -def test_entity_admin_latest_snapshot_summary_renders_components(source_admin_context): - entity = Entity.objects.create( - project=source_admin_context.project, - name="Snapshot Entity", - type="vendor", - authority_score=0.73, - ) - EntityAuthoritySnapshot.objects.create( - entity=entity, - project=source_admin_context.project, - mention_component=0.7, - feedback_component=0.55, - duplicate_component=0.4, - decayed_prior=0.5, - final_score=0.73, - ) - admin_instance = EntityAdmin(Entity, AdminSite()) - - rendered = admin_instance.latest_snapshot_summary(entity) - - assert "M 70.0%" in rendered - assert "F 55.0%" in rendered - assert "D 40.0%" in rendered - assert "Carry 50.0%" in rendered - - -def test_entity_authority_snapshot_admin_helpers_render_expected_values( - source_admin_context, -): - entity = Entity.objects.create( - project=source_admin_context.project, - name="Snapshot Admin Entity", - type="vendor", - authority_score=0.81, - ) - snapshot = EntityAuthoritySnapshot.objects.create( - entity=entity, - project=source_admin_context.project, - mention_component=0.8, - feedback_component=0.6, - duplicate_component=0.4, - decayed_prior=0.5, - final_score=0.81, - ) - admin_instance = EntityAuthoritySnapshotAdmin(EntityAuthoritySnapshot, AdminSite()) - - rendered_score = admin_instance.display_final_score(snapshot) - rendered_components = admin_instance.display_components(snapshot) - - assert "81.0%" in rendered_score - assert "green" in rendered_score - assert "M 80.0%" in rendered_components - assert "F 60.0%" in rendered_components - - -def test_accept_selected_entity_candidates_creates_entity_and_backfills_mentions( - source_admin_context, mocker -): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/river-labs-launch", - title="River Labs ships a new platform release", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="River Labs announced a new hosted control plane.", - ) - candidate = EntityCandidate.objects.create( - project=source_admin_context.project, - name="River Labs", - suggested_type="vendor", - first_seen_in=content, - occurrence_count=2, - ) - admin_instance = EntityCandidateAdmin(EntityCandidate, AdminSite()) - _message_user_mock(admin_instance, mocker) - - admin_instance.accept_selected_candidates( - request=_request(), - queryset=EntityCandidate.objects.filter(pk=candidate.pk), - ) - - candidate.refresh_from_db() - content.refresh_from_db() - entity = Entity.objects.get( - project=source_admin_context.project, - name="River Labs", - ) - mention = EntityMention.objects.get(content=content, entity=entity) - - assert candidate.status == EntityCandidateStatus.ACCEPTED - assert candidate.merged_into == entity - assert mention.role == "subject" - assert content.entity == entity - - -def test_reject_selected_entity_candidates_marks_candidates_rejected( - source_admin_context, mocker -): - candidate = EntityCandidate.objects.create( - project=source_admin_context.project, - name="Rejected Vendor", - suggested_type="vendor", - ) - admin_instance = EntityCandidateAdmin(EntityCandidate, AdminSite()) - _message_user_mock(admin_instance, mocker) - - admin_instance.reject_selected_candidates( - request=_request(), - queryset=EntityCandidate.objects.filter(pk=candidate.pk), - ) - - candidate.refresh_from_db() - - assert candidate.status == EntityCandidateStatus.REJECTED - - -def test_merge_selected_entity_candidates_uses_existing_same_name_entity( - source_admin_context, mocker -): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/acme-merge", - title="Acme ships a new platform feature", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Acme expanded its hosted platform product.", - ) - entity = Entity.objects.create( - project=source_admin_context.project, - name="Acme", - type="vendor", - ) - candidate = EntityCandidate.objects.create( - project=source_admin_context.project, - name="Acme", - suggested_type="vendor", - first_seen_in=content, - ) - admin_instance = EntityCandidateAdmin(EntityCandidate, AdminSite()) - _message_user_mock(admin_instance, mocker) - - admin_instance.merge_into_existing_entities( - request=_request(), - queryset=EntityCandidate.objects.filter(pk=candidate.pk), - ) - - candidate.refresh_from_db() - - assert candidate.status == EntityCandidateStatus.MERGED - assert candidate.merged_into == entity - - -def test_high_value_filter_only_returns_high_value_reference_content( - source_admin_context, -): - high_value = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/high-value", - title="High Value", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="High value content.", - relevance_score=81, - is_reference=True, - ) - Content.objects.create( - project=source_admin_context.project, - url="https://example.com/not-high-value", - title="Not High Value", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Not high value content.", - relevance_score=81, - is_reference=False, - ) - filter_instance = HighValueFilter( - request=_request(), - params=_params(value_tier="high_value"), - model=Content, - model_admin=ContentAdmin(Content, AdminSite()), - ) - filter_instance.value = lambda: "high_value" - - filtered = filter_instance.queryset(_request(), Content.objects.all()) - - assert list(filtered) == [high_value] - - -def test_duplicate_state_filter_returns_canonical_rows_with_duplicate_signals( - source_admin_context, -): - canonical = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/filter-canonical", - canonical_url="https://example.com/filter-canonical", - title="Canonical", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Canonical content.", - duplicate_signal_count=2, - ) - Content.objects.create( - project=source_admin_context.project, - url="https://example.com/filter-plain", - canonical_url="https://example.com/filter-plain", - title="Plain", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Plain content.", - ) - filter_instance = DuplicateStateFilter( - request=_request(), - params=_params(duplicate_state="canonical_with_duplicates"), - model=Content, - model_admin=ContentAdmin(Content, AdminSite()), - ) - filter_instance.value = lambda: "canonical_with_duplicates" - - filtered = filter_instance.queryset(_request(), Content.objects.all()) - - assert list(filtered) == [canonical] - - -def test_duplicate_state_filter_returns_suppressed_duplicates( - source_admin_context, -): - canonical = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/filter-parent", - canonical_url="https://example.com/filter-parent", - title="Canonical", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Canonical content.", - ) - duplicate = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/filter-parent?utm_source=reddit", - canonical_url="https://example.com/filter-parent", - title="Duplicate", - author="Editor", - source_plugin=SourcePluginName.REDDIT, - published_date=timezone.now(), - content_text="Duplicate content.", - duplicate_of=canonical, - is_active=False, - ) - filter_instance = DuplicateStateFilter( - request=_request(), - params=_params(duplicate_state="suppressed_duplicates"), - model=Content, - model_admin=ContentAdmin(Content, AdminSite()), - ) - filter_instance.value = lambda: "suppressed_duplicates" - - filtered = filter_instance.queryset(_request(), Content.objects.all()) - - assert list(filtered) == [duplicate] - - -def test_content_view_trace_builds_template_trace_url(source_admin_context, settings): - settings.AI_TRACE_URL_TEMPLATE = "https://trace.example/{project_id}/{skill_name}/{skill_result_id}/{trace_id}/{content_id}/{run_id}" - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-template-trace", - title="Admin Template Trace", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Template trace content.", - ) - skill_result = SkillResult.objects.create( - content=content, - project=source_admin_context.project, - skill_name="summarization", - status="COMPLETED", - result_data={"trace": {"trace_id": "trace-123"}}, - ) - admin_instance = ContentAdmin(Content, AdminSite()) - - rendered = admin_instance.view_trace(content) - - assert ( - f"https://trace.example/{_require_pk(source_admin_context.project)}/summarization/{_require_pk(skill_result)}/trace-123/{_require_pk(content)}/trace-123" - in rendered - ) - - -@pytest.mark.parametrize( - ("score", "expected_color"), - [ - (None, None), - (80, "green"), - (50, "orange"), - (10, "red"), - ], -) -def test_content_display_relevance_uses_expected_output( - source_admin_context, score, expected_color -): - content = Content.objects.create( - project=source_admin_context.project, - url=f"https://example.com/relevance-{score}", - title="Relevance Display", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Content.", - relevance_score=score, - ) - admin_instance = ContentAdmin(Content, AdminSite()) - - rendered = admin_instance.display_relevance(content) - - if score is None: - assert rendered == "-" - else: - assert expected_color in rendered - assert str(score) in rendered - - -def test_skill_result_admin_helpers_and_dashboard_stats(source_admin_context, mocker): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/skill-result", - title="Skill Result Title For Preview", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Skill result content.", - ) - current_result = SkillResult.objects.create( - content=content, - project=source_admin_context.project, - skill_name="summarization", - status="FAILED", - result_data={"summary": "Draft summary"}, - error_message="boom", - latency_ms=1250, - confidence=0.42, - ) - superseded_result = SkillResult.objects.create( - content=content, - project=source_admin_context.project, - skill_name="relevance_scoring", - status="COMPLETED", - result_data=None, - latency_ms=250, - confidence=0.91, - superseded_by=current_result, - ) - admin_instance = SkillResultAdmin(SkillResult, AdminSite()) - message_user_mock = _message_user_mock(admin_instance, mocker) - super_changelist_view = mocker.patch( - "core.admin.ModelAdmin.changelist_view", - side_effect=lambda request, extra_context=None: extra_context, - ) - - admin_instance.retry_selected_skills( - _request(), SkillResult.objects.filter(pk=current_result.pk) - ) - current_result.refresh_from_db() - response = admin_instance.changelist_view(_request()) - dashboard_stats = _dashboard_stats(response) - - assert current_result.status == "pending" - assert current_result.error_message == "" - message_user_mock.assert_called_once_with( - ANY, - "Successfully reset 1 skills to PENDING for retry.", - messages.SUCCESS, - ) - assert ( - admin_instance.preview_json(current_result) - == f'🔍 Preview' - ) - assert admin_instance.preview_json(superseded_result) == "-" - assert admin_instance.get_content_link(current_result).endswith("...") - assert "● PENDING" in admin_instance.display_status(current_result) - assert admin_instance.display_performance(current_result) == "1250ms / 42%" - assert admin_instance.is_current(current_result) is True - assert admin_instance.is_current(superseded_result) is False - assert "Draft summary" in admin_instance.pretty_result_data(current_result) - assert admin_instance.pretty_result_data(superseded_result) == "No data available" - super_changelist_view.assert_called_once() - assert dashboard_stats[0]["value"] == "750ms" - assert dashboard_stats[1]["value"] == "0.0%" - - -def test_user_feedback_admin_helpers_and_dashboard_stats( - source_admin_context, django_user_model, mocker -): - mocker.patch("core.signals.queue_topic_centroid_recompute") - user = _create_user( - django_user_model, username="feedback-user", password="testpass123" - ) - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/feedback", - title="Feedback Title That Is Long Enough To Truncate", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Feedback content.", - relevance_score=85, - ) - upvote = UserFeedback.objects.create( - content=content, - project=source_admin_context.project, - user=user, - feedback_type="upvote", - ) - other_content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/feedback-other", - title="Other Feedback Title", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Other feedback content.", - relevance_score=20, - ) - UserFeedback.objects.create( - content=other_content, - project=source_admin_context.project, - user=_create_user( - django_user_model, username="feedback-user-2", password="testpass123" - ), - feedback_type="downvote", - ) - admin_instance = UserFeedbackAdmin(UserFeedback, AdminSite()) - super_changelist_view = mocker.patch( - "core.admin.ModelAdmin.changelist_view", - side_effect=lambda request, extra_context=None: extra_context, - ) - - response = admin_instance.changelist_view(_request()) - dashboard_stats = _dashboard_stats(response) - - assert "👍" in admin_instance.display_feedback(upvote) - assert admin_instance.get_content_title(upvote).endswith("...") - assert "green" in admin_instance.get_ai_score(upvote) - other_content.relevance_score = None - other_content.save(update_fields=["relevance_score"]) - downvote = UserFeedback.objects.get(content=other_content) - assert admin_instance.get_ai_score(downvote) == "-" - super_changelist_view.assert_called_once() - assert dashboard_stats[0]["value"] == "50.0%" - assert dashboard_stats[1]["value"] == 2 - - -def test_ingestion_run_display_duration_handles_running_and_completed( - source_admin_context, -): - running_run = IngestionRun.objects.create( - project=source_admin_context.project, - plugin_name=SourcePluginName.RSS, - status=RunStatus.RUNNING, - items_fetched=0, - items_ingested=0, - ) - completed_run = IngestionRun.objects.create( - project=source_admin_context.project, - plugin_name=SourcePluginName.RSS, - status=RunStatus.SUCCESS, - items_fetched=10, - items_ingested=10, - ) - completed_run.started_at = timezone.now() - timedelta(minutes=3, seconds=5) - completed_run.completed_at = completed_run.started_at + timedelta( - minutes=3, seconds=5 - ) - completed_run.save(update_fields=["started_at", "completed_at"]) - admin_instance = IngestionRunAdmin(IngestionRun, AdminSite()) - - assert admin_instance.display_duration(running_run) == "In Progress..." - assert admin_instance.display_duration(completed_run) == "3m 5s" - - -def test_review_queue_actions_update_resolution_and_emit_message( - source_admin_context, mocker -): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/review-action", - title="Review Action", - author="Reviewer", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Review action content.", - ) - approve_item = ReviewQueue.objects.create( - project=source_admin_context.project, - content=content, - reason=ReviewReason.BORDERLINE_RELEVANCE, - confidence=0.5, - resolved=False, - ) - reject_item = ReviewQueue.objects.create( - project=source_admin_context.project, - content=content, - reason=ReviewReason.LOW_CONFIDENCE_CLASSIFICATION, - confidence=0.2, - resolved=False, - ) - admin_instance = ReviewQueueAdmin(ReviewQueue, AdminSite()) - message_user_mock = _message_user_mock(admin_instance, mocker) - - admin_instance.mark_as_approved( - _request(), ReviewQueue.objects.filter(pk=approve_item.pk) - ) - admin_instance.mark_as_rejected( - _request(), ReviewQueue.objects.filter(pk=reject_item.pk) - ) - - approve_item.refresh_from_db() - reject_item.refresh_from_db() - assert approve_item.resolved is True - assert approve_item.resolution == "APPROVED" - assert reject_item.resolved is True - assert reject_item.resolution == "REJECTED" - assert message_user_mock.call_count == 2 - - -def test_high_value_filter_lookups_and_noop_queryset(source_admin_context): - filter_instance = HighValueFilter( - request=_request(), - params={}, - model=Content, - model_admin=ContentAdmin(Content, AdminSite()), - ) - filter_instance.value = lambda: None - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/high-value-noop", - title="Noop", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="noop", - ) - - assert filter_instance.lookups(_request(), ContentAdmin(Content, AdminSite())) == ( - ("high_value", "🔥 High Value (Score > 80 & Reference)"), - ) - assert list(filter_instance.queryset(_request(), Content.objects.all())) == [ - content - ] - - -def test_content_view_trace_returns_dash_when_no_skill_results(source_admin_context): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/admin-no-trace", - title="No Trace", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="No trace content.", - ) - admin_instance = ContentAdmin(Content, AdminSite()) - - assert admin_instance.view_trace(content) == "-" - - -def test_skill_result_admin_handles_unknown_status_and_empty_performance( - source_admin_context, -): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/skill-result-empty", - title="", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Skill result content.", - ) - skill_result = SkillResult.objects.create( - content=content, - project=source_admin_context.project, - skill_name="summarization", - status="QUEUED", - result_data={"summary": "Queued summary"}, - latency_ms=None, - confidence=None, - ) - admin_instance = SkillResultAdmin(SkillResult, AdminSite()) - - assert admin_instance.get_content_link(skill_result) == "Untitled" - assert "gray" in admin_instance.display_status(skill_result) - assert admin_instance.display_performance(skill_result) == "- / -" - - -def test_skill_result_changelist_view_uses_warning_and_danger_colors( - source_admin_context, mocker -): - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/skill-result-slow", - title="Slow Skill Result", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Slow skill result content.", - ) - SkillResult.objects.create( - content=content, - project=source_admin_context.project, - skill_name="summarization", - status="failed", - latency_ms=3001, - ) - admin_instance = SkillResultAdmin(SkillResult, AdminSite()) - super_changelist_view = mocker.patch( - "core.admin.ModelAdmin.changelist_view", - side_effect=lambda request, extra_context=None: extra_context, - ) - - response = admin_instance.changelist_view(_request()) - dashboard_stats = _dashboard_stats(response) - - super_changelist_view.assert_called_once() - assert dashboard_stats[0]["color"] == "warning" - assert dashboard_stats[1]["color"] == "danger" - - -def test_user_feedback_admin_upvote_and_orange_score_branches( - source_admin_context, mocker -): - mocker.patch("core.signals.queue_topic_centroid_recompute") - content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/feedback-orange", - title="Orange Feedback Title", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Feedback content.", - relevance_score=60, - ) - feedback = UserFeedback.objects.create( - content=content, - project=source_admin_context.project, - user=source_admin_context.user, - feedback_type="upvote", - ) - admin_instance = UserFeedbackAdmin(UserFeedback, AdminSite()) - - assert "👍" in admin_instance.display_feedback(feedback) - assert "orange" in admin_instance.get_ai_score(feedback) - - -def test_user_feedback_changelist_view_uses_success_color_for_high_approval( - source_admin_context, django_user_model, mocker -): - mocker.patch("core.signals.queue_topic_centroid_recompute") - first_content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/feedback-success-1", - title="Feedback Success One", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Feedback content one.", - relevance_score=90, - ) - second_content = Content.objects.create( - project=source_admin_context.project, - url="https://example.com/feedback-success-2", - title="Feedback Success Two", - author="Editor", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Feedback content two.", - relevance_score=90, - ) - UserFeedback.objects.create( - content=first_content, - project=source_admin_context.project, - user=source_admin_context.user, - feedback_type="upvote", - ) - UserFeedback.objects.create( - content=second_content, - project=source_admin_context.project, - user=_create_user( - django_user_model, username="feedback-success-2", password="testpass123" - ), - feedback_type="upvote", - ) - admin_instance = UserFeedbackAdmin(UserFeedback, AdminSite()) - super_changelist_view = mocker.patch( - "core.admin.ModelAdmin.changelist_view", - side_effect=lambda request, extra_context=None: extra_context, - ) - - response = admin_instance.changelist_view(_request()) - dashboard_stats = _dashboard_stats(response) - - super_changelist_view.assert_called_once() - assert dashboard_stats[0]["color"] == "success" - assert dashboard_stats[0]["value"] == "100.0%" - - -def test_ingestion_run_admin_status_efficiency_and_dashboard_branches( - source_admin_context, mocker -): - IngestionRun.objects.create( - project=source_admin_context.project, - plugin_name=SourcePluginName.RSS, - status="failed", - items_fetched=0, - items_ingested=0, - ) - running_run = IngestionRun.objects.create( - project=source_admin_context.project, - plugin_name=SourcePluginName.RSS, - status=RunStatus.RUNNING, - items_fetched=5, - items_ingested=5, - ) - admin_instance = IngestionRunAdmin(IngestionRun, AdminSite()) - super_changelist_view = mocker.patch( - "core.admin.ModelAdmin.changelist_view", - side_effect=lambda request, extra_context=None: extra_context, - ) - - response = admin_instance.changelist_view(_request()) - dashboard_stats = _dashboard_stats(response) - - assert "danger" in admin_instance.display_status( - IngestionRun.objects.filter(status="failed").first() - ) - assert ( - admin_instance.display_efficiency( - IngestionRun.objects.filter(status="failed").first() - ) - == "0/0" - ) - assert "info" in admin_instance.display_status(running_run) - super_changelist_view.assert_called_once() - assert dashboard_stats[0]["value"] == "5" - assert dashboard_stats[1]["color"] == "warning" - - -def test_source_config_admin_health_pretty_config_and_dashboard_branches( - source_admin_context, mocker -): - stale_config = SourceConfig.objects.create( - project=source_admin_context.project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/stale.xml"}, - is_active=True, - last_fetched_at=timezone.now() - timedelta(days=2), - ) - paused_config = SourceConfig.objects.create( - project=source_admin_context.project, - plugin_name=SourcePluginName.REDDIT, - config={}, - is_active=False, - ) - never_run_config = SourceConfig.objects.create( - project=source_admin_context.project, - plugin_name=SourcePluginName.RSS, - config={}, - is_active=True, - last_fetched_at=None, - ) - admin_instance = SourceConfigAdmin(SourceConfig, AdminSite()) - super_changelist_view = mocker.patch( - "core.admin.ModelAdmin.changelist_view", - side_effect=lambda request, extra_context=None: extra_context, - ) - - response = admin_instance.changelist_view(_request()) - dashboard_stats = _dashboard_stats(response) - - assert "Stale" in admin_instance.display_health(stale_config) - assert "Paused" in admin_instance.display_health(paused_config) - assert "Never Run" in admin_instance.display_health(never_run_config) - assert admin_instance.pretty_config(paused_config) == "Empty" - super_changelist_view.assert_called_once() - assert dashboard_stats[0]["color"] == "warning" - assert dashboard_stats[1]["value"] == 2 - - -@pytest.mark.parametrize( - ("confidence", "expected_color"), - [ - (0.2, "red"), - (0.9, "green"), - ], -) -def test_review_queue_display_confidence_remaining_color_branches( - source_admin_context, - confidence, - expected_color, -): - content = Content.objects.create( - project=source_admin_context.project, - url=f"https://example.com/review-confidence-{confidence}", - title="Review Confidence Remaining", - author="Reviewer", - source_plugin=SourcePluginName.RSS, - published_date=timezone.now(), - content_text="Review queue content", - ) - review_item = ReviewQueue.objects.create( - project=source_admin_context.project, - content=content, - reason=ReviewReason.BORDERLINE_RELEVANCE, - confidence=confidence, - resolved=False, - ) - admin_instance = ReviewQueueAdmin(ReviewQueue, AdminSite()) - - rendered = admin_instance.display_confidence(review_item) - - assert expected_color in rendered diff --git a/core/tests/test_api.py b/core/tests/test_api.py index fd4f155e..58201423 100644 --- a/core/tests/test_api.py +++ b/core/tests/test_api.py @@ -1,4 +1,3 @@ -from types import SimpleNamespace from typing import Any, cast from unittest.mock import patch @@ -8,41 +7,21 @@ from rest_framework import status from rest_framework.test import APIClient, APITestCase -from core.models import ( - Content, - ContentClusterMembership, - Entity, - EntityAuthoritySnapshot, - EntityCandidate, - EntityCandidateStatus, - EntityMention, - FeedbackType, - IngestionRun, - IntakeAllowlist, - NewsletterIntake, - NewsletterIntakeStatus, - ReviewQueue, - ReviewReason, - RunStatus, - SkillResult, - SkillStatus, - ThemeSuggestion, - ThemeSuggestionStatus, - TopicCentroidSnapshot, - TopicCluster, - TopicVelocitySnapshot, - UserFeedback, -) +from content.models import Content, FeedbackType, UserFeedback +from entities.models import Entity, EntityCandidate +from ingestion.models import IngestionRun, RunStatus +from newsletters.models import IntakeAllowlist, NewsletterIntake, NewsletterIntakeStatus +from pipeline.models import ReviewQueue, ReviewReason, SkillResult, SkillStatus from projects.model_support import SourcePluginName from projects.models import ( BlueskyCredentials, - MastodonCredentials, Project, ProjectConfig, ProjectMembership, ProjectRole, SourceConfig, ) +from trends.models import TopicCentroidSnapshot def _require_pk(instance: Model) -> int: @@ -186,1152 +165,6 @@ def assert_standardized_validation_error(self, payload, attr): self.assertEqual(payload["type"], "validation_error") self.assertTrue(any(error["attr"] == attr for error in payload["errors"])) - def test_project_list_requires_authentication(self): - _typed_client(self.client).force_authenticate(user=None) - - response = self.client.get(reverse("v1:project-list"), HTTP_HOST="localhost") - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual( - response.json(), - { - "type": "client_error", - "errors": [ - { - "code": "not_authenticated", - "detail": "Authentication credentials were not provided.", - "attr": None, - } - ], - }, - ) - - def test_project_list_is_scoped_to_request_user_memberships(self): - BlueskyCredentials.objects.create( - project=self.owner_project, - handle="owner-project.bsky.social", - is_active=True, - last_error="", - ) - - response = self.client.get(reverse("v1:project-list")) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], _require_pk(self.owner_project)) - self.assertEqual(response.json()[0]["user_role"], ProjectRole.ADMIN) - self.assertEqual( - response.json()[0]["intake_token"], self.owner_project.intake_token - ) - self.assertFalse(response.json()[0]["intake_enabled"]) - self.assertTrue(response.json()[0]["has_bluesky_credentials"]) - self.assertEqual( - response.json()[0]["bluesky_handle"], "owner-project.bsky.social" - ) - self.assertTrue(response.json()[0]["bluesky_is_active"]) - self.assertEqual(response.json()[0]["bluesky_last_error"], "") - - def test_project_rotate_intake_token_returns_updated_project(self): - original_token = self.owner_project.intake_token - - response = self.client.post( - reverse( - "v1:project-rotate-intake-token", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.owner_project.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertNotEqual(self.owner_project.intake_token, original_token) - self.assertEqual( - response.json()["intake_token"], self.owner_project.intake_token - ) - - def test_entity_list_is_scoped_to_request_user_project(self): - response = self.client.get( - reverse( - "v1:project-entity-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], _require_pk(self.owner_entity)) - - def test_entity_list_includes_recent_mentions(self): - mention = EntityMention.objects.create( - project=self.owner_project, - content=self.owner_content, - entity=self.owner_entity, - role="subject", - sentiment="neutral", - span="Owner Entity", - confidence=0.88, - ) - - response = self.client.get( - reverse( - "v1:project-entity-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()[0]["mention_count"], 1) - self.assertEqual( - response.json()[0]["latest_mentions"][0]["id"], _require_pk(mention) - ) - self.assertEqual( - response.json()[0]["latest_mentions"][0]["content_title"], - self.owner_content.title, - ) - - def test_entity_mentions_action_returns_full_mention_history(self): - first_mention = EntityMention.objects.create( - project=self.owner_project, - content=self.owner_content, - entity=self.owner_entity, - role="subject", - sentiment="neutral", - span="Owner Entity", - confidence=0.88, - ) - second_content = Content.objects.create( - project=self.owner_project, - url="https://example.com/owner-second", - title="Second Owner Content", - author="Owner Author", - entity=self.owner_entity, - source_plugin="rss", - published_date="2026-04-22T00:00:00Z", - content_text="Another owner content text", - ) - second_mention = EntityMention.objects.create( - project=self.owner_project, - content=second_content, - entity=self.owner_entity, - role="mentioned", - sentiment="positive", - span="Owner Entity", - confidence=0.67, - ) - - response = self.client.get( - reverse( - "v1:project-entity-mentions", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_entity), - }, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 2) - self.assertEqual(response.json()[0]["id"], _require_pk(second_mention)) - self.assertEqual(response.json()[1]["id"], _require_pk(first_mention)) - self.assertEqual(response.json()[0]["content_title"], second_content.title) - - def test_intake_allowlist_list_is_scoped_to_request_user_project(self): - other_allowlist = IntakeAllowlist.objects.create( - project=self.other_project, - sender_email="other@example.com", - ) - - response = self.client.get( - reverse( - "v1:project-intake-allowlist-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual( - response.json()[0]["id"], _require_pk(self.owner_intake_allowlist) - ) - self.assertFalse(response.json()[0]["is_confirmed"]) - self.assertNotEqual(response.json()[0]["id"], _require_pk(other_allowlist)) - - def test_intake_allowlist_create_and_delete_manage_project_senders(self): - create_response = self.client.post( - reverse( - "v1:project-intake-allowlist-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - {"sender_email": "new-sender@example.com"}, - format="json", - ) - - self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) - created_allowlist = IntakeAllowlist.objects.get( - project=self.owner_project, - sender_email="new-sender@example.com", - ) - self.assertEqual( - create_response.json()["project"], _require_pk(self.owner_project) - ) - self.assertFalse(create_response.json()["is_confirmed"]) - - delete_response = self.client.delete( - reverse( - "v1:project-intake-allowlist-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(created_allowlist), - }, - ) - ) - - self.assertEqual(delete_response.status_code, status.HTTP_204_NO_CONTENT) - self.assertFalse( - IntakeAllowlist.objects.filter(pk=_require_pk(created_allowlist)).exists() - ) - - def test_bluesky_credentials_list_create_and_update_hide_stored_password(self): - list_response = self.client.get( - reverse( - "v1:project-bluesky-credentials-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(list_response.status_code, status.HTTP_200_OK) - self.assertEqual(list_response.json(), []) - - create_response = self.client.post( - reverse( - "v1:project-bluesky-credentials-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - { - "handle": "@Owner.Project.BSKY.social", - "pds_url": "https://pds.example.com/xrpc/", - "is_active": True, - "app_password": "app-password", - }, - format="json", - ) - - self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) - credentials = BlueskyCredentials.objects.get(project=self.owner_project) - self.assertEqual(credentials.handle, "owner.project.bsky.social") - self.assertEqual(credentials.pds_url, "https://pds.example.com") - self.assertEqual(credentials.get_app_password(), "app-password") - self.assertTrue(create_response.json()["has_stored_credential"]) - self.assertNotIn("app_password", create_response.json()) - - update_response = self.client.patch( - reverse( - "v1:project-bluesky-credentials-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(credentials), - }, - ), - { - "handle": "updated.bsky.social", - "pds_url": "", - "is_active": False, - }, - format="json", - ) - - credentials.refresh_from_db() - self.assertEqual(update_response.status_code, status.HTTP_200_OK) - self.assertEqual(credentials.handle, "updated.bsky.social") - self.assertFalse(credentials.is_active) - self.assertEqual(credentials.get_app_password(), "app-password") - - def test_bluesky_credentials_create_requires_app_password(self): - response = self.client.post( - reverse( - "v1:project-bluesky-credentials-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - { - "handle": "owner.bsky.social", - "pds_url": "", - "is_active": True, - "app_password": "", - }, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error(response.json(), "app_password") - - def test_newsletter_intake_list_returns_recent_project_history(self): - other_intake = NewsletterIntake.objects.create( - project=self.other_project, - sender_email="other@example.com", - subject="Other Digest", - raw_text="Another item", - message_id="other-intake-1", - ) - - response = self.client.get( - reverse( - "v1:project-newsletter-intake-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual( - response.json()[0]["id"], _require_pk(self.owner_newsletter_intake) - ) - self.assertEqual(response.json()[0]["status"], NewsletterIntakeStatus.EXTRACTED) - self.assertEqual( - response.json()[0]["extraction_result"]["items"][0]["title"], - "Example Post", - ) - self.assertNotEqual(response.json()[0]["id"], _require_pk(other_intake)) - - def test_entity_list_supports_authority_score_ordering(self): - second_entity = Entity.objects.create( - project=self.owner_project, - name="Second Entity", - type="vendor", - authority_score=0.9, - ) - self.owner_entity.authority_score = 0.4 - self.owner_entity.save(update_fields=["authority_score"]) - - response = self.client.get( - reverse( - "v1:project-entity-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - {"ordering": "-authority_score"}, - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()[0]["id"], _require_pk(second_entity)) - self.assertEqual(response.json()[1]["id"], _require_pk(self.owner_entity)) - - def test_entity_authority_history_action_returns_recent_snapshots(self): - first_snapshot = EntityAuthoritySnapshot.objects.create( - entity=self.owner_entity, - project=self.owner_project, - mention_component=0.6, - feedback_component=0.5, - duplicate_component=0.5, - decayed_prior=0.5, - final_score=0.53, - ) - second_snapshot = EntityAuthoritySnapshot.objects.create( - entity=self.owner_entity, - project=self.owner_project, - mention_component=0.8, - feedback_component=0.7, - duplicate_component=0.6, - decayed_prior=0.53, - final_score=0.66, - ) - - response = self.client.get( - reverse( - "v1:project-entity-authority-history", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_entity), - }, - ), - {"limit": 1}, - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], _require_pk(second_snapshot)) - self.assertNotEqual(response.json()[0]["id"], _require_pk(first_snapshot)) - - def test_topic_centroid_summary_action_returns_latest_snapshot_and_averages(self): - latest_snapshot = TopicCentroidSnapshot.objects.create( - project=self.owner_project, - centroid_active=True, - centroid_vector=[0.0, 1.0], - feedback_count=14, - upvote_count=11, - downvote_count=3, - drift_from_previous=0.3, - drift_from_week_ago=0.4, - ) - - response = self.client.get( - reverse( - "v1:project-topic-centroid-snapshot-summary", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["project"], _require_pk(self.owner_project)) - self.assertEqual(response.json()["snapshot_count"], 2) - self.assertEqual(response.json()["active_snapshot_count"], 2) - self.assertEqual( - response.json()["latest_snapshot"]["id"], _require_pk(latest_snapshot) - ) - self.assertAlmostEqual(response.json()["avg_drift_from_previous"], 0.2) - self.assertAlmostEqual(response.json()["avg_drift_from_week_ago"], 0.3) - - def test_topic_cluster_list_returns_current_velocity_annotation(self): - cluster = TopicCluster.objects.create( - project=self.owner_project, - first_seen_at="2026-04-22T00:00:00Z", - last_seen_at="2026-04-24T00:00:00Z", - is_active=True, - member_count=3, - dominant_entity=self.owner_entity, - ) - TopicVelocitySnapshot.objects.create( - cluster=cluster, - project=self.owner_project, - window_count=4, - trailing_mean=1.5, - trailing_stddev=0.5, - z_score=3.0, - velocity_score=1.0, - ) - - response = self.client.get( - reverse( - "v1:project-topic-cluster-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - {"ordering": "-velocity_score"}, - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], _require_pk(cluster)) - self.assertEqual(response.json()[0]["member_count"], 3) - self.assertEqual( - response.json()[0]["dominant_entity"]["id"], _require_pk(self.owner_entity) - ) - self.assertAlmostEqual(response.json()[0]["velocity_score"], 1.0) - self.assertAlmostEqual(response.json()[0]["z_score"], 3.0) - self.assertEqual(response.json()[0]["window_count"], 4) - - def test_topic_cluster_detail_and_velocity_history_action_return_memberships(self): - cluster = TopicCluster.objects.create( - project=self.owner_project, - first_seen_at="2026-04-22T00:00:00Z", - last_seen_at="2026-04-24T00:00:00Z", - is_active=True, - member_count=1, - dominant_entity=self.owner_entity, - ) - ContentClusterMembership.objects.create( - content=self.owner_content, - cluster=cluster, - project=self.owner_project, - similarity=0.92, - ) - first_snapshot = TopicVelocitySnapshot.objects.create( - cluster=cluster, - project=self.owner_project, - window_count=2, - trailing_mean=1.0, - trailing_stddev=0.2, - z_score=1.5, - velocity_score=0.75, - ) - second_snapshot = TopicVelocitySnapshot.objects.create( - cluster=cluster, - project=self.owner_project, - window_count=3, - trailing_mean=1.0, - trailing_stddev=0.3, - z_score=3.0, - velocity_score=1.0, - ) - TopicVelocitySnapshot.objects.filter(pk=first_snapshot.pk).update( - computed_at="2026-04-23T00:00:00Z" - ) - TopicVelocitySnapshot.objects.filter(pk=second_snapshot.pk).update( - computed_at="2026-04-24T00:00:00Z" - ) - - detail_response = self.client.get( - reverse( - "v1:project-topic-cluster-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(cluster), - }, - ) - ) - history_response = self.client.get( - reverse( - "v1:project-topic-cluster-velocity-history", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(cluster), - }, - ), - {"limit": 1}, - ) - - self.assertEqual(detail_response.status_code, status.HTTP_200_OK) - self.assertEqual(detail_response.json()["id"], _require_pk(cluster)) - self.assertEqual(len(detail_response.json()["memberships"]), 1) - self.assertEqual( - detail_response.json()["memberships"][0]["content"]["id"], - _require_pk(self.owner_content), - ) - self.assertEqual(len(detail_response.json()["velocity_history"]), 2) - self.assertEqual( - detail_response.json()["velocity_history"][0]["id"], - _require_pk(second_snapshot), - ) - - self.assertEqual(history_response.status_code, status.HTTP_200_OK) - self.assertEqual(len(history_response.json()), 1) - self.assertEqual(history_response.json()[0]["id"], _require_pk(second_snapshot)) - - def test_theme_suggestion_list_is_scoped_to_project(self): - cluster = TopicCluster.objects.create( - project=self.owner_project, - first_seen_at="2026-04-22T00:00:00Z", - last_seen_at="2026-04-24T00:00:00Z", - is_active=True, - member_count=3, - dominant_entity=self.owner_entity, - ) - suggestion = ThemeSuggestion.objects.create( - project=self.owner_project, - cluster=cluster, - title="Owner Theme", - pitch="Owner pitch", - why_it_matters="Owner why", - suggested_angle="Owner angle", - velocity_at_creation=0.9, - novelty_score=0.8, - ) - other_cluster = TopicCluster.objects.create( - project=self.other_project, - first_seen_at="2026-04-22T00:00:00Z", - last_seen_at="2026-04-24T00:00:00Z", - is_active=True, - member_count=3, - dominant_entity=self.other_entity, - ) - ThemeSuggestion.objects.create( - project=self.other_project, - cluster=other_cluster, - title="Other Theme", - pitch="Other pitch", - why_it_matters="Other why", - suggested_angle="Other angle", - velocity_at_creation=0.8, - novelty_score=0.7, - ) - - response = self.client.get( - reverse( - "v1:project-theme-suggestion-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], _require_pk(suggestion)) - self.assertEqual(response.json()[0]["status"], ThemeSuggestionStatus.PENDING) - - def test_theme_suggestion_accept_and_dismiss_actions_update_workflow_fields(self): - cluster = TopicCluster.objects.create( - project=self.owner_project, - first_seen_at="2026-04-22T00:00:00Z", - last_seen_at="2026-04-24T00:00:00Z", - is_active=True, - member_count=3, - dominant_entity=self.owner_entity, - ) - promoted_content = Content.objects.create( - project=self.owner_project, - url="https://example.com/promoted-by-theme", - title="Promoted by Theme", - author="Owner Author", - entity=self.owner_entity, - source_plugin=SourcePluginName.RSS, - published_date="2026-04-24T00:00:00Z", - content_text="Promoted content text", - ) - ContentClusterMembership.objects.create( - content=promoted_content, - cluster=cluster, - project=self.owner_project, - similarity=0.94, - ) - accept_suggestion = ThemeSuggestion.objects.create( - project=self.owner_project, - cluster=cluster, - title="Accept Theme", - pitch="Pitch", - why_it_matters="Why", - suggested_angle="Angle", - velocity_at_creation=0.9, - novelty_score=0.8, - ) - dismiss_suggestion = ThemeSuggestion.objects.create( - project=self.owner_project, - cluster=cluster, - title="Dismiss Theme", - pitch="Pitch", - why_it_matters="Why", - suggested_angle="Angle", - velocity_at_creation=0.7, - novelty_score=0.75, - ) - - accept_response = self.client.post( - reverse( - "v1:project-theme-suggestion-accept", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(accept_suggestion), - }, - ), - format="json", - ) - dismiss_response = self.client.post( - reverse( - "v1:project-theme-suggestion-dismiss", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(dismiss_suggestion), - }, - ), - {"reason": "already covered"}, - format="json", - ) - - accept_suggestion.refresh_from_db() - dismiss_suggestion.refresh_from_db() - promoted_content.refresh_from_db() - self.assertEqual(accept_response.status_code, status.HTTP_200_OK) - self.assertEqual(accept_suggestion.status, ThemeSuggestionStatus.ACCEPTED) - self.assertEqual(accept_suggestion.decided_by, self.owner) - self.assertIsNotNone(accept_suggestion.decided_at) - self.assertEqual(promoted_content.newsletter_promotion_theme, accept_suggestion) - self.assertEqual(promoted_content.newsletter_promotion_by, self.owner) - self.assertIsNotNone(promoted_content.newsletter_promotion_at) - self.assertEqual(len(accept_response.json()["promoted_contents"]), 1) - self.assertEqual( - accept_response.json()["promoted_contents"][0]["id"], - _require_pk(promoted_content), - ) - - self.assertEqual(dismiss_response.status_code, status.HTTP_200_OK) - self.assertEqual(dismiss_suggestion.status, ThemeSuggestionStatus.DISMISSED) - self.assertEqual(dismiss_suggestion.dismissal_reason, "already covered") - self.assertEqual(dismiss_suggestion.decided_by, self.owner) - - def test_content_detail_includes_newsletter_promotion_state(self): - cluster = TopicCluster.objects.create( - project=self.owner_project, - first_seen_at="2026-04-22T00:00:00Z", - last_seen_at="2026-04-24T00:00:00Z", - is_active=True, - member_count=1, - dominant_entity=self.owner_entity, - ) - suggestion = ThemeSuggestion.objects.create( - project=self.owner_project, - cluster=cluster, - title="Promoted Theme", - pitch="Pitch", - why_it_matters="Why", - suggested_angle="Angle", - velocity_at_creation=0.9, - novelty_score=0.8, - status=ThemeSuggestionStatus.ACCEPTED, - decided_by=self.owner, - ) - self.owner_content.newsletter_promotion_theme = suggestion - self.owner_content.newsletter_promotion_by = self.owner - self.owner_content.newsletter_promotion_at = "2026-04-24T00:00:00Z" - self.owner_content.save( - update_fields=[ - "newsletter_promotion_theme", - "newsletter_promotion_by", - "newsletter_promotion_at", - ] - ) - - response = self.client.get( - reverse( - "v1:project-content-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_content), - }, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - response.json()["newsletter_promotion_theme"], - _require_pk(suggestion), - ) - self.assertEqual( - response.json()["newsletter_promotion_by"], _require_pk(self.owner) - ) - self.assertIsNotNone(response.json()["newsletter_promotion_at"]) - - def test_content_detail_includes_duplicate_state(self): - canonical = self.owner_content - canonical.canonical_url = "https://example.com/owner" - canonical.duplicate_signal_count = 1 - canonical.save(update_fields=["canonical_url", "duplicate_signal_count"]) - duplicate = Content.objects.create( - project=self.owner_project, - url="https://example.com/owner?utm_source=reddit", - canonical_url="https://example.com/owner", - title="Duplicate Owner Content", - author="Owner Author", - entity=self.owner_entity, - source_plugin="reddit", - published_date="2026-04-22T00:00:00Z", - content_text="Duplicate content text", - duplicate_of=canonical, - is_active=False, - ) - - response = self.client.get( - reverse( - "v1:project-content-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(duplicate), - }, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["canonical_url"], "https://example.com/owner") - self.assertEqual(response.json()["duplicate_of"], _require_pk(canonical)) - self.assertEqual(response.json()["duplicate_signal_count"], 0) - - def test_nested_entity_list_rejects_other_users_project(self): - response = self.client.get( - reverse( - "v1:project-entity-list", - kwargs={"project_id": _require_pk(self.other_project)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_entity_candidate_list_is_scoped_to_request_user_project(self): - owner_candidate = EntityCandidate.objects.create( - project=self.owner_project, - name="Owner Candidate", - suggested_type="vendor", - first_seen_in=self.owner_content, - ) - EntityCandidate.objects.create( - project=self.other_project, - name="Other Candidate", - suggested_type="organization", - first_seen_in=self.other_content, - ) - - response = self.client.get( - reverse( - "v1:project-entity-candidate-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], _require_pk(owner_candidate)) - - def test_entity_candidate_accept_action_returns_updated_candidate(self): - candidate = EntityCandidate.objects.create( - project=self.owner_project, - name="River Labs", - suggested_type="vendor", - first_seen_in=self.owner_content, - ) - - response = self.client.post( - reverse( - "v1:project-entity-candidate-accept", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(candidate), - }, - ), - format="json", - ) - - candidate.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(candidate.status, EntityCandidateStatus.ACCEPTED) - self.assertIsNotNone(candidate.merged_into) - self.assertEqual(response.json()["status"], EntityCandidateStatus.ACCEPTED) - - def test_entity_candidate_reject_action_returns_updated_candidate(self): - candidate = EntityCandidate.objects.create( - project=self.owner_project, - name="Rejected Candidate", - suggested_type="organization", - first_seen_in=self.owner_content, - ) - - response = self.client.post( - reverse( - "v1:project-entity-candidate-reject", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(candidate), - }, - ), - format="json", - ) - - candidate.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(candidate.status, EntityCandidateStatus.REJECTED) - self.assertEqual(response.json()["status"], EntityCandidateStatus.REJECTED) - - def test_entity_candidate_merge_rejects_cross_project_entity(self): - candidate = EntityCandidate.objects.create( - project=self.owner_project, - name="Merge Candidate", - suggested_type="vendor", - first_seen_in=self.owner_content, - ) - - response = self.client.post( - reverse( - "v1:project-entity-candidate-merge", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(candidate), - }, - ), - {"merged_into": _require_pk(self.other_entity)}, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error(response.json(), "merged_into") - - def test_entity_candidate_merge_action_returns_updated_candidate(self): - candidate = EntityCandidate.objects.create( - project=self.owner_project, - name="Owner Entity Alias", - suggested_type="vendor", - first_seen_in=self.owner_content, - ) - - response = self.client.post( - reverse( - "v1:project-entity-candidate-merge", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(candidate), - }, - ), - {"merged_into": _require_pk(self.owner_entity)}, - format="json", - ) - - candidate.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(candidate.status, EntityCandidateStatus.MERGED) - self.assertEqual(candidate.merged_into, self.owner_entity) - self.assertEqual(response.json()["merged_into"], _require_pk(self.owner_entity)) - - def test_verify_bluesky_credentials_requires_project_credentials(self): - response = self.client.post( - reverse( - "v1:project-verify-bluesky-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error( - response.json(), "bluesky_credentials" - ) - - @patch("core.plugins.bluesky.BlueskySourcePlugin.verify_credentials") - def test_verify_bluesky_credentials_verifies_project_account(self, verify_mock): - credentials = BlueskyCredentials( - project=self.owner_project, handle="project.bsky.social" - ) - credentials.set_app_password("app-password") - credentials.save() - - response = self.client.post( - reverse( - "v1:project-verify-bluesky-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - verify_mock.assert_called_once() - verified_credentials = verify_mock.call_args.args[0] - self.assertEqual(verified_credentials, credentials) - self.assertEqual(response.json()["status"], "verified") - self.assertEqual(response.json()["handle"], "project.bsky.social") - self.assertEqual(response.json()["last_error"], "") - - @patch("core.api.logger.exception") - @patch( - "core.plugins.bluesky.BlueskySourcePlugin.verify_credentials", - side_effect=RuntimeError("bad login"), - ) - def test_verify_bluesky_credentials_surfaces_verification_errors( - self, _verify_mock, logger_exception_mock - ): - credentials = BlueskyCredentials( - project=self.owner_project, handle="project.bsky.social" - ) - credentials.set_app_password("app-password") - credentials.save() - - response = self.client.post( - reverse( - "v1:project-verify-bluesky-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error( - response.json(), "bluesky_credentials" - ) - self.assertNotIn("bad login", str(response.json())) - logger_exception_mock.assert_called_once_with( - "Bluesky credential verification failed for project id=%s", - _require_pk(self.owner_project), - ) - - def test_verify_mastodon_credentials_requires_configured_project_credentials(self): - response = self.client.post( - reverse( - "v1:project-verify-mastodon-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error( - response.json(), "mastodon_credentials" - ) - - @patch("core.plugins.mastodon.MastodonSourcePlugin.verify_credentials") - def test_verify_mastodon_credentials_verifies_project_account(self, verify_mock): - credentials = MastodonCredentials( - project=self.owner_project, - instance_url="https://hachyderm.io", - account_acct="alice@hachyderm.io", - ) - credentials.set_access_token("access-token") - credentials.save() - - response = self.client.post( - reverse( - "v1:project-verify-mastodon-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - verify_mock.assert_called_once() - verified_credentials = verify_mock.call_args.args[0] - self.assertEqual(verified_credentials, credentials) - self.assertEqual(response.json()["status"], "verified") - self.assertEqual(response.json()["account_acct"], "alice@hachyderm.io") - self.assertEqual(response.json()["instance_url"], "https://hachyderm.io") - self.assertEqual(response.json()["last_error"], "") - - @patch("core.api.logger.exception") - @patch( - "core.plugins.mastodon.MastodonSourcePlugin.verify_credentials", - side_effect=RuntimeError("bad token"), - ) - def test_verify_mastodon_credentials_surfaces_verification_errors( - self, _verify_mock, logger_exception_mock - ): - credentials = MastodonCredentials( - project=self.owner_project, - instance_url="https://hachyderm.io", - account_acct="alice@hachyderm.io", - ) - credentials.set_access_token("access-token") - credentials.save() - - response = self.client.post( - reverse( - "v1:project-verify-mastodon-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error( - response.json(), "mastodon_credentials" - ) - self.assertNotIn("bad token", str(response.json())) - logger_exception_mock.assert_called_once_with( - "Mastodon credential verification failed for project id=%s", - _require_pk(self.owner_project), - ) - - @patch("core.signals.queue_topic_centroid_recompute") - def test_feedback_create_assigns_current_user(self, queue_centroid_mock): - response = self.client.post( - reverse( - "v1:project-feedback-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - { - "content": _require_pk(self.owner_content), - "feedback_type": FeedbackType.UPVOTE, - }, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - feedback = UserFeedback.objects.get() - self.assertEqual(feedback.user, self.owner) - self.assertEqual(feedback.feedback_type, FeedbackType.UPVOTE) - queue_centroid_mock.assert_called_once_with(_require_pk(self.owner_project)) - - def test_feedback_rejects_cross_project_content(self): - response = self.client.post( - reverse( - "v1:project-feedback-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - { - "content": _require_pk(self.other_content), - "feedback_type": FeedbackType.DOWNVOTE, - }, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error(response.json(), "content") - - def test_content_create_uses_project_from_url(self): - response = self.client.post( - reverse( - "v1:project-content-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - { - "url": "https://example.com/new", - "title": "New Content", - "author": "Owner Author", - "entity": _require_pk(self.owner_entity), - "source_plugin": "rss", - "published_date": "2026-04-22T00:00:00Z", - "content_text": "Nested content text", - "project": _require_pk(self.other_project), - }, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - created_content = Content.objects.get(title="New Content") - self.assertEqual(created_content.project, self.owner_project) - - @patch("core.tasks.run_relevance_scoring_skill.delay") - def test_content_skill_action_queues_relevance_scoring( - self, run_relevance_scoring_delay_mock - ): - - response = self.client.post( - f"/api/v1/projects/{_require_pk(self.owner_project)}/contents/{_require_pk(self.owner_content)}/skills/relevance_scoring/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) - pending_result = SkillResult.objects.get( - content=self.owner_content, - skill_name="relevance_scoring", - superseded_by__isnull=True, - ) - run_relevance_scoring_delay_mock.assert_called_once_with( - _require_pk(pending_result) - ) - self.owner_content.refresh_from_db() - self.assertIsNone(self.owner_content.relevance_score) - self.assertEqual(response.json()["skill_name"], "relevance_scoring") - self.assertEqual(response.json()["status"], SkillStatus.PENDING) - - @patch("core.tasks.run_summarization_skill.delay") - def test_content_skill_action_queues_summarization( - self, run_summarization_delay_mock - ): - self.owner_content.relevance_score = 0.25 - self.owner_content.save(update_fields=["relevance_score"]) - - response = self.client.post( - f"/api/v1/projects/{_require_pk(self.owner_project)}/contents/{_require_pk(self.owner_content)}/skills/summarization/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) - pending_result = SkillResult.objects.get( - content=self.owner_content, - skill_name="summarization", - superseded_by__isnull=True, - ) - run_summarization_delay_mock.assert_called_once_with( - _require_pk(pending_result) - ) - self.assertEqual(response.json()["skill_name"], "summarization") - self.assertEqual(response.json()["status"], SkillStatus.PENDING) - - @patch("core.pipeline.search_similar_content") - def test_content_skill_action_runs_find_related(self, search_similar_content_mock): - search_similar_content_mock.return_value = [ - SimpleNamespace( - score=0.91, - payload={ - "content_id": _require_pk(self.other_content), - "title": self.other_content.title, - "url": self.other_content.url, - "published_date": self.other_content.published_date, - "source_plugin": self.other_content.source_plugin, - }, - ) - ] - - response = self.client.post( - f"/api/v1/projects/{_require_pk(self.owner_project)}/contents/{_require_pk(self.owner_content)}/skills/find_related/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.json()["skill_name"], "find_related") - self.assertEqual(response.json()["status"], SkillStatus.COMPLETED) - self.assertEqual( - response.json()["result_data"]["related_items"][0]["content_id"], - _require_pk(self.other_content), - ) - def test_authenticated_nested_list_endpoints_smoke(self): list_endpoints = [ reverse( @@ -1397,7 +230,7 @@ def test_authenticated_nested_list_endpoints_smoke(self): response = self.client.get(endpoint) self.assertEqual(response.status_code, status.HTTP_200_OK) - @patch("core.signals.queue_topic_centroid_recompute") + @patch("content.signals.queue_topic_centroid_recompute") def test_authenticated_nested_detail_endpoints_smoke(self, queue_centroid_mock): detail_endpoints = [ reverse( @@ -1522,16 +355,3 @@ def test_authenticated_nested_detail_endpoints_smoke(self, queue_centroid_mock): with self.subTest(endpoint=endpoint): response = self.client.get(endpoint) self.assertEqual(response.status_code, status.HTTP_200_OK) - - def test_source_config_create_validates_plugin_config(self): - response = self.client.post( - reverse( - "v1:project-source-config-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - {"plugin_name": SourcePluginName.RSS, "config": {}}, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error(response.json(), "config") diff --git a/core/tests/test_embeddings.py b/core/tests/test_embeddings.py index 09c2a71c..d534586f 100644 --- a/core/tests/test_embeddings.py +++ b/core/tests/test_embeddings.py @@ -10,6 +10,7 @@ from qdrant_client.http.exceptions import ResponseHandlingException from qdrant_client.http.models import FieldCondition, Filter, MatchValue +from content.models import Content, UserFeedback from core import embeddings from core.embeddings import ( build_content_embedding_text, @@ -24,19 +25,14 @@ upsert_content_embedding, upsert_topic_centroid, ) -from core.models import ( - Content, - Entity, - IngestionRun, - ReviewQueue, - SkillResult, - UserFeedback, -) from core.pipeline import ( CLASSIFICATION_SKILL_NAME, RELEVANCE_SKILL_NAME, SUMMARIZATION_SKILL_NAME, ) +from entities.models import Entity +from ingestion.models import IngestionRun +from pipeline.models import ReviewQueue, SkillResult from projects.model_support import SourcePluginName from projects.models import Project, SourceConfig @@ -379,7 +375,7 @@ def test_embedding_smoke_command_can_upsert_content(embedding_context, mocker, c def test_seed_demo_creates_reference_corpus_and_embeds_demo_content(mocker, capsys): - mocker.patch("core.signals.queue_topic_centroid_recompute") + mocker.patch("content.signals.queue_topic_centroid_recompute") upsert_mock = mocker.patch( "core.management.commands.seed_demo.upsert_content_embedding" ) @@ -426,7 +422,7 @@ def test_seed_demo_creates_reference_corpus_and_embeds_demo_content(mocker, caps def test_seed_demo_is_stable_on_rerun(mocker): - mocker.patch("core.signals.queue_topic_centroid_recompute") + mocker.patch("content.signals.queue_topic_centroid_recompute") mocker.patch("core.management.commands.seed_demo.upsert_content_embedding") call_command("seed_demo") @@ -444,7 +440,7 @@ def test_seed_demo_is_stable_on_rerun(mocker): def test_seed_demo_skips_embeddings_when_vector_stack_is_unavailable(mocker, capsys): - mocker.patch("core.signals.queue_topic_centroid_recompute") + mocker.patch("content.signals.queue_topic_centroid_recompute") upsert_mock = mocker.patch( "core.management.commands.seed_demo.upsert_content_embedding", side_effect=ResponseHandlingException(httpx.ConnectError("connection refused")), diff --git a/core/tests/test_permissions.py b/core/tests/test_permissions.py index 37c28dd9..2b9b8fd7 100644 --- a/core/tests/test_permissions.py +++ b/core/tests/test_permissions.py @@ -7,20 +7,12 @@ from rest_framework import status from rest_framework.test import APIClient, APITestCase -from core.models import ( - Content, - Entity, - EntityCandidate, - FeedbackType, - ReviewQueue, - ReviewReason, - ThemeSuggestion, - TopicCentroidSnapshot, - TopicCluster, - UserFeedback, -) +from content.models import Content, FeedbackType, UserFeedback +from entities.models import Entity, EntityCandidate +from pipeline.models import ReviewQueue, ReviewReason from projects.model_support import SourcePluginName from projects.models import BlueskyCredentials, Project, ProjectMembership, ProjectRole +from trends.models import ThemeSuggestion, TopicCentroidSnapshot, TopicCluster def _require_pk(instance: Model) -> int: @@ -46,7 +38,7 @@ def _create_user(user_model: type[Any], **kwargs: object): class ProjectRolePermissionTests(APITestCase): def setUp(self): - queue_centroid_patcher = patch("core.signals.queue_topic_centroid_recompute") + queue_centroid_patcher = patch("content.signals.queue_topic_centroid_recompute") queue_centroid_patcher.start() self.addCleanup(queue_centroid_patcher.stop) diff --git a/core/tests/test_serializers.py b/core/tests/test_serializers.py index 64e371be..4adae77b 100644 --- a/core/tests/test_serializers.py +++ b/core/tests/test_serializers.py @@ -6,20 +6,13 @@ from django.db.models import Model from rest_framework import serializers as drf_serializers -from core.models import ( - Content, - Entity, - ReviewReason, - SkillResult, -) -from core.serializers import ( - ContentSerializer, - EntitySerializer, - IngestionRunSerializer, - ReviewQueueSerializer, - SkillResultSerializer, - UserFeedbackSerializer, -) +from content.models import Content +from content.serializers import ContentSerializer, UserFeedbackSerializer +from entities.models import Entity +from entities.serializers import EntitySerializer +from ingestion.serializers import IngestionRunSerializer +from pipeline.models import ReviewReason, SkillResult +from pipeline.serializers import ReviewQueueSerializer, SkillResultSerializer from projects.model_support import SourcePluginName from projects.models import ( MastodonCredentials, diff --git a/core/tests/test_tasks.py b/core/tests/test_tasks.py index f2007c10..3af7edbe 100644 --- a/core/tests/test_tasks.py +++ b/core/tests/test_tasks.py @@ -1,27 +1,9 @@ -from datetime import datetime, timedelta, timezone from types import SimpleNamespace import pytest from django.db.models import Model -from core.models import ( - Content, - ContentClusterMembership, - Entity, - EntityAuthoritySnapshot, - EntityMention, - EntityMentionRole, - FeedbackType, - IngestionRun, - RunStatus, - SkillStatus, - ThemeSuggestion, - ThemeSuggestionStatus, - TopicCentroidSnapshot, - TopicCluster, - TopicVelocitySnapshot, - UserFeedback, -) +from content.models import Content, FeedbackType, UserFeedback from core.pipeline import RELEVANCE_SKILL_NAME, SUMMARIZATION_SKILL_NAME from core.tasks import ( queue_content_skill, @@ -30,21 +12,15 @@ run_relevance_scoring_skill, run_summarization_skill, ) -from ingestion.tasks import _ingest_source_config, run_all_ingestions, run_ingestion -from projects.model_support import SourcePluginName -from projects.models import Project, ProjectConfig, SourceConfig -from trends.tasks import ( - TOPIC_CENTROID_MIN_UPVOTES, - accept_theme_suggestion, - assign_content_to_topic_cluster, - generate_theme_suggestions, - queue_topic_centroid_recompute, - recompute_topic_centroid, - recompute_topic_clusters, - recompute_topic_velocity, - run_all_topic_centroid_recomputations, - run_all_topic_cluster_recomputations, +from entities.models import ( + Entity, + EntityAuthoritySnapshot, + EntityMention, + EntityMentionRole, ) +from pipeline.models import SkillStatus +from projects.model_support import SourcePluginName +from projects.models import Project, ProjectConfig pytestmark = pytest.mark.django_db @@ -73,328 +49,6 @@ def source_plugin_context(django_user_model): return SimpleNamespace(user=user, project=project, entity=entity) -def test_run_ingestion_creates_content_from_rss_entries(source_plugin_context, mocker): - upsert_embedding_mock = mocker.patch("core.tasks.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") - parse_mock = mocker.patch("core.plugins.rss.feedparser.parse") - source_config = SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/feed.xml"}, - ) - parse_mock.return_value = SimpleNamespace( - entries=[ - SimpleNamespace( - link="https://example.com/post-1", - title="Example Post", - author="Author", - summary="Summary", - published_parsed=datetime( - 2026, 4, 20, 12, 0, tzinfo=timezone.utc - ).timetuple(), - ) - ] - ) - - result = run_ingestion(_require_pk(source_config)) - - assert result["items_fetched"] == 1 - assert result["items_ingested"] == 1 - content = Content.objects.get(url="https://example.com/post-1") - assert content.project == source_plugin_context.project - assert content.entity == source_plugin_context.entity - upsert_embedding_mock.assert_called_once_with(content) - process_content_delay_mock.assert_called_once_with(_require_pk(content)) - assert ( - SourceConfig.objects.get(pk=_require_pk(source_config)).last_fetched_at - is not None - ) - ingestion_run = IngestionRun.objects.get( - project=source_plugin_context.project, plugin_name=SourcePluginName.RSS - ) - assert ingestion_run.status == RunStatus.SUCCESS - - -def test_run_ingestion_skips_same_source_duplicate_urls(source_plugin_context, mocker): - upsert_embedding_mock = mocker.patch("core.tasks.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") - parse_mock = mocker.patch("core.plugins.rss.feedparser.parse") - source_config = SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/feed.xml"}, - ) - Content.objects.create( - project=source_plugin_context.project, - entity=source_plugin_context.entity, - url="https://example.com/post-1", - title="Existing", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-20T12:00:00Z", - content_text="Existing content", - ) - parse_mock.return_value = SimpleNamespace( - entries=[ - SimpleNamespace( - link="https://example.com/post-1", - title="Duplicate Post", - author="Author", - summary="Summary", - published_parsed=datetime( - 2026, 4, 20, 12, 0, tzinfo=timezone.utc - ).timetuple(), - ) - ] - ) - - result = run_ingestion(_require_pk(source_config)) - - assert result["items_fetched"] == 1 - assert result["items_ingested"] == 0 - upsert_embedding_mock.assert_not_called() - process_content_delay_mock.assert_not_called() - assert Content.objects.filter(url="https://example.com/post-1").count() == 1 - - -def test_ingest_source_config_allows_cross_plugin_duplicate_urls_for_pipeline_dedup( - source_plugin_context, mocker -): - upsert_embedding_mock = mocker.patch("core.tasks.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") - source_config = SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.REDDIT, - config={"subreddit": "python", "listing": "new", "limit": 5}, - ) - Content.objects.create( - project=source_plugin_context.project, - entity=source_plugin_context.entity, - url="https://example.com/post-1", - canonical_url="https://example.com/post-1", - title="Existing RSS Item", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-20T12:00:00Z", - content_text="Existing content", - ) - plugin = SimpleNamespace( - fetch_new_content=lambda since: [ - SimpleNamespace( - url="https://example.com/post-1", - title="Reddit duplicate that should still enter the pipeline", - author="redditor", - published_date=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), - content_text="A community post linking to the same article.", - source_plugin=SourcePluginName.REDDIT, - source_metadata={}, - ) - ], - match_entity_for_url=lambda url: None, - ) - mocker.patch("ingestion.tasks.get_plugin_for_source_config", return_value=plugin) - - items_fetched, items_ingested = _ingest_source_config(source_config) - - assert items_fetched == 1 - assert items_ingested == 1 - assert Content.objects.filter(project=source_plugin_context.project).count() == 2 - upsert_embedding_mock.assert_called_once() - process_content_delay_mock.assert_called_once() - - -def test_run_ingestion_creates_content_from_reddit_posts(source_plugin_context, mocker): - upsert_embedding_mock = mocker.patch("core.tasks.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") - reddit_mock = mocker.patch("core.plugins.reddit.praw.Reddit") - source_config = SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.REDDIT, - config={"subreddit": "python", "listing": "new", "limit": 5}, - ) - submission = SimpleNamespace( - id="abc123", - url="https://reddit.com/r/python/comments/abc123/test", - permalink="/r/python/comments/abc123/test", - title="Reddit Post", - selftext="Post body", - author="redditor", - created_utc=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc).timestamp(), - ) - subreddit = SimpleNamespace( - new=lambda limit: iter([submission]), hot=lambda limit: iter([]) - ) - reddit_mock.return_value.subreddit.return_value = subreddit - - result = run_ingestion(_require_pk(source_config)) - - assert result["items_fetched"] == 1 - assert result["items_ingested"] == 1 - content = Content.objects.get(title="Reddit Post") - upsert_embedding_mock.assert_called_once_with(content) - process_content_delay_mock.assert_called_once_with(_require_pk(content)) - assert content.source_plugin == SourcePluginName.REDDIT - assert content.entity is None - - -def test_ingest_source_config_deduplicates_bluesky_posts_by_post_uri( - source_plugin_context, mocker -): - upsert_embedding_mock = mocker.patch("core.tasks.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") - source_config = SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.BLUESKY, - config={"author_handle": "example.bsky.social"}, - ) - Content.objects.create( - project=source_plugin_context.project, - entity=source_plugin_context.entity, - url="https://example.com/existing-article", - title="Existing Bluesky Post", - author="example.bsky.social", - source_plugin=SourcePluginName.BLUESKY, - published_date="2026-04-20T12:00:00Z", - content_text="Existing content", - source_metadata={"post_uri": "at://did:plc:author/app.bsky.feed.post/abc123"}, - ) - plugin = SimpleNamespace( - fetch_new_content=lambda since: [ - SimpleNamespace( - url="https://example.com/new-canonical-url", - title="Duplicate Bluesky Post", - author="example.bsky.social", - published_date=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), - content_text="Duplicate content", - source_plugin=SourcePluginName.BLUESKY, - source_metadata={ - "author_handle": "example.bsky.social", - "post_uri": "at://did:plc:author/app.bsky.feed.post/abc123", - }, - ) - ], - match_entity_for_item=lambda item: source_plugin_context.entity, - ) - mocker.patch("ingestion.tasks.get_plugin_for_source_config", return_value=plugin) - - items_fetched, items_ingested = _ingest_source_config(source_config) - - assert items_fetched == 1 - assert items_ingested == 0 - assert Content.objects.filter(project=source_plugin_context.project).count() == 1 - upsert_embedding_mock.assert_not_called() - process_content_delay_mock.assert_not_called() - - -def test_ingest_source_config_deduplicates_mastodon_statuses_by_status_uri( - source_plugin_context, mocker -): - upsert_embedding_mock = mocker.patch("core.tasks.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") - source_config = SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.MASTODON, - config={ - "instance_url": "https://hachyderm.io", - "hashtag": "platformengineering", - }, - ) - Content.objects.create( - project=source_plugin_context.project, - entity=source_plugin_context.entity, - url="https://example.com/existing-article", - title="Existing Mastodon Status", - author="Alice Example", - source_plugin=SourcePluginName.MASTODON, - published_date="2026-04-20T12:00:00Z", - content_text="Existing content", - source_metadata={ - "status_uri": "https://hachyderm.io/users/alice/statuses/abc123" - }, - ) - plugin = SimpleNamespace( - fetch_new_content=lambda since: [ - SimpleNamespace( - url="https://example.com/new-canonical-url", - title="Duplicate Mastodon Status", - author="Alice Example", - published_date=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), - content_text="Duplicate content", - source_plugin=SourcePluginName.MASTODON, - source_metadata={ - "author_acct": "alice@hachyderm.io", - "status_uri": "https://hachyderm.io/users/alice/statuses/abc123", - }, - ) - ], - match_entity_for_item=lambda item: source_plugin_context.entity, - ) - mocker.patch("ingestion.tasks.get_plugin_for_source_config", return_value=plugin) - - items_fetched, items_ingested = _ingest_source_config(source_config) - - assert items_fetched == 1 - assert items_ingested == 0 - assert Content.objects.filter(project=source_plugin_context.project).count() == 1 - upsert_embedding_mock.assert_not_called() - process_content_delay_mock.assert_not_called() - - -def test_run_all_ingestions_enqueues_active_source_configs( - source_plugin_context, mocker -): - delay_mock = mocker.patch("ingestion.tasks.run_ingestion.delay") - active_one = SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/feed.xml"}, - ) - active_two = SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.REDDIT, - config={"subreddit": "python"}, - ) - SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/inactive.xml"}, - is_active=False, - ) - - enqueued_count = run_all_ingestions() - - assert enqueued_count == 2 - delay_mock.assert_any_call(_require_pk(active_one)) - delay_mock.assert_any_call(_require_pk(active_two)) - assert delay_mock.call_count == 2 - - -def test_run_all_ingestions_executes_inline_when_eager( - source_plugin_context, settings, mocker -): - settings.CELERY_TASK_ALWAYS_EAGER = True - run_ingestion_mock = mocker.patch("ingestion.tasks.run_ingestion") - delay_mock = mocker.patch("ingestion.tasks.run_ingestion.delay") - active_one = SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/feed.xml"}, - ) - active_two = SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.REDDIT, - config={"subreddit": "python"}, - ) - - enqueued_count = run_all_ingestions() - - assert enqueued_count == 2 - run_ingestion_mock.assert_any_call(_require_pk(active_one)) - run_ingestion_mock.assert_any_call(_require_pk(active_two)) - assert run_ingestion_mock.call_count == 2 - delay_mock.assert_not_called() - - def test_run_all_authority_recomputations_enqueues_all_projects( source_plugin_context, mocker ): @@ -432,64 +86,10 @@ def test_run_all_authority_recomputations_executes_inline_when_eager( delay_mock.assert_not_called() -def test_run_all_topic_centroid_recomputations_enqueues_all_projects( - source_plugin_context, mocker -): - delay_mock = mocker.patch("trends.tasks.recompute_topic_centroid.delay") - other_project = Project.objects.create( - name="Other Centroid Project", - topic_description="Security", - ) - - enqueued_count = run_all_topic_centroid_recomputations() - - assert enqueued_count == 2 - delay_mock.assert_any_call(source_plugin_context.project.id) - delay_mock.assert_any_call(_require_pk(other_project)) - assert delay_mock.call_count == 2 - - -def test_run_all_topic_centroid_recomputations_executes_inline_when_eager( - source_plugin_context, settings, mocker -): - settings.CELERY_TASK_ALWAYS_EAGER = True - recompute_mock = mocker.patch("trends.tasks.recompute_topic_centroid") - delay_mock = mocker.patch("trends.tasks.recompute_topic_centroid.delay") - other_project = Project.objects.create( - name="Inline Centroid Project", - topic_description="Platform", - ) - - enqueued_count = run_all_topic_centroid_recomputations() - - assert enqueued_count == 2 - recompute_mock.assert_any_call(source_plugin_context.project.id) - recompute_mock.assert_any_call(_require_pk(other_project)) - assert recompute_mock.call_count == 2 - delay_mock.assert_not_called() - - -def test_run_all_topic_cluster_recomputations_enqueues_all_projects( - source_plugin_context, mocker -): - delay_mock = mocker.patch("trends.tasks.recompute_topic_clusters.delay") - other_project = Project.objects.create( - name="Other Cluster Project", - topic_description="Security", - ) - - enqueued_count = run_all_topic_cluster_recomputations() - - assert enqueued_count == 2 - delay_mock.assert_any_call(source_plugin_context.project.id) - delay_mock.assert_any_call(_require_pk(other_project)) - assert delay_mock.call_count == 2 - - def test_recompute_authority_scores_updates_entities_and_creates_snapshots( source_plugin_context, mocker ): - mocker.patch("core.signals.queue_topic_centroid_recompute") + mocker.patch("content.signals.queue_topic_centroid_recompute") project = source_plugin_context.project config = ProjectConfig.objects.create( project=project, @@ -570,593 +170,6 @@ def test_recompute_authority_scores_updates_entities_and_creates_snapshots( ) -def test_recompute_topic_centroid_upserts_weighted_normalized_centroid( - source_plugin_context, mocker -): - project = source_plugin_context.project - mocker.patch("core.signals.queue_topic_centroid_recompute") - upsert_mock = mocker.patch("trends.tasks.upsert_topic_centroid") - delete_mock = mocker.patch("trends.tasks.delete_topic_centroid") - vector_lookup = { - **{ - f"Upvote {index}": [1.0, 0.0] for index in range(TOPIC_CENTROID_MIN_UPVOTES) - }, - "Downvote": [0.0, 1.0], - } - mocker.patch( - "trends.tasks.embed_text", - side_effect=lambda text: vector_lookup[text.split("\n\n", 1)[0]], - ) - - upvote_contents = [] - for index in range(TOPIC_CENTROID_MIN_UPVOTES): - upvote_contents.append( - Content.objects.create( - project=project, - entity=source_plugin_context.entity, - url=f"https://example.com/upvote-{index}", - title=f"Upvote {index}", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-20T12:00:00Z", - content_text="Manual content body", - ) - ) - downvote_content = Content.objects.create( - project=project, - entity=source_plugin_context.entity, - url="https://example.com/downvote", - title="Downvote", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-20T12:00:00Z", - content_text="Manual content body", - ) - for content in upvote_contents: - UserFeedback.objects.create( - project=project, - content=content, - user=source_plugin_context.user, - feedback_type=FeedbackType.UPVOTE, - ) - second_user = source_plugin_context.user.__class__.objects.create_user( - username="downvote-owner", password="testpass123" - ) - UserFeedback.objects.create( - project=project, - content=downvote_content, - user=second_user, - feedback_type=FeedbackType.DOWNVOTE, - ) - - result = recompute_topic_centroid(project.id) - snapshot = TopicCentroidSnapshot.objects.get(project=project) - - assert result["centroid_active"] is True - delete_mock.assert_not_called() - upsert_mock.assert_called_once() - centroid_vector = upsert_mock.call_args.args[1] - assert centroid_vector[0] > 0.9 - assert centroid_vector[1] < 0.0 - assert snapshot.centroid_active is True - assert snapshot.feedback_count == TOPIC_CENTROID_MIN_UPVOTES + 1 - assert snapshot.upvote_count == TOPIC_CENTROID_MIN_UPVOTES - assert snapshot.downvote_count == 1 - assert snapshot.centroid_vector == pytest.approx(centroid_vector) - assert snapshot.drift_from_previous is None - assert snapshot.drift_from_week_ago is None - - -def test_recompute_topic_centroid_persists_drift_from_previous_and_week_old_snapshot( - source_plugin_context, mocker -): - project = source_plugin_context.project - mocker.patch("core.signals.queue_topic_centroid_recompute") - upsert_mock = mocker.patch("trends.tasks.upsert_topic_centroid") - delete_mock = mocker.patch("trends.tasks.delete_topic_centroid") - mocker.patch("trends.tasks.embed_text", return_value=[1.0, 0.0]) - - recent_snapshot = TopicCentroidSnapshot.objects.create( - project=project, - centroid_active=True, - centroid_vector=[1.0, 0.0], - feedback_count=12, - upvote_count=12, - downvote_count=0, - ) - older_snapshot = TopicCentroidSnapshot.objects.create( - project=project, - centroid_active=True, - centroid_vector=[0.0, 1.0], - feedback_count=12, - upvote_count=12, - downvote_count=0, - ) - TopicCentroidSnapshot.objects.filter(pk=recent_snapshot.pk).update( - computed_at=datetime(2026, 4, 28, 12, 0, tzinfo=timezone.utc) - ) - TopicCentroidSnapshot.objects.filter(pk=older_snapshot.pk).update( - computed_at=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc) - ) - - for index in range(TOPIC_CENTROID_MIN_UPVOTES): - content = Content.objects.create( - project=project, - entity=source_plugin_context.entity, - url=f"https://example.com/drift-upvote-{index}", - title=f"Drift Upvote {index}", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-20T12:00:00Z", - content_text="Manual content body", - ) - UserFeedback.objects.create( - project=project, - content=content, - user=source_plugin_context.user, - feedback_type=FeedbackType.UPVOTE, - ) - - result = recompute_topic_centroid(project.id) - snapshot = TopicCentroidSnapshot.objects.filter(project=project).latest( - "computed_at" - ) - - assert result["centroid_active"] is True - delete_mock.assert_not_called() - upsert_mock.assert_called_once() - assert snapshot.centroid_active is True - assert snapshot.drift_from_previous == pytest.approx(0.0) - assert snapshot.drift_from_week_ago == pytest.approx(1.0) - - -def test_recompute_topic_centroid_disables_centroid_below_minimum_upvotes( - source_plugin_context, mocker -): - project = source_plugin_context.project - mocker.patch("core.signals.queue_topic_centroid_recompute") - upsert_mock = mocker.patch("trends.tasks.upsert_topic_centroid") - delete_mock = mocker.patch("trends.tasks.delete_topic_centroid") - for index in range(TOPIC_CENTROID_MIN_UPVOTES - 1): - content = Content.objects.create( - project=project, - entity=source_plugin_context.entity, - url=f"https://example.com/too-few-{index}", - title=f"Too Few {index}", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-20T12:00:00Z", - content_text="Manual content body", - ) - UserFeedback.objects.create( - project=project, - content=content, - user=source_plugin_context.user, - feedback_type=FeedbackType.UPVOTE, - ) - - result = recompute_topic_centroid(project.id) - snapshot = TopicCentroidSnapshot.objects.get(project=project) - - assert result["centroid_active"] is False - delete_mock.assert_called_once_with(project.id) - upsert_mock.assert_not_called() - assert snapshot.centroid_active is False - assert snapshot.centroid_vector == [] - assert snapshot.upvote_count == TOPIC_CENTROID_MIN_UPVOTES - 1 - assert snapshot.drift_from_previous is None - - -def test_recompute_topic_clusters_groups_recent_similar_content( - source_plugin_context, mocker -): - project = source_plugin_context.project - second_entity = Entity.objects.create( - project=project, - name="Secondary Entity", - type="vendor", - ) - vector_lookup = { - "Trend 1": [1.0, 0.0], - "Trend 2": [0.99, 0.01], - "Trend 3": [0.98, 0.02], - "Trend 4": [0.97, 0.03], - "Outlier": [0.0, 1.0], - } - mocker.patch( - "trends.tasks.embed_text", - side_effect=lambda text: vector_lookup[text.split("\n\n", 1)[0]], - ) - delay_mock = mocker.patch("trends.tasks.recompute_topic_velocity.delay") - - clustered_contents = [] - for index in range(4): - content = Content.objects.create( - project=project, - entity=source_plugin_context.entity, - url=f"https://example.com/trend-{index}", - title=f"Trend {index + 1}", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date=f"2026-04-2{index}T12:00:00Z", - content_text="Clusterable trend content", - ) - clustered_contents.append(content) - EntityMention.objects.create( - project=project, - content=content, - entity=source_plugin_context.entity, - role=EntityMentionRole.SUBJECT, - ) - outlier = Content.objects.create( - project=project, - entity=second_entity, - url="https://example.com/outlier", - title="Outlier", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-24T12:00:00Z", - content_text="Outlier trend content", - ) - - result = recompute_topic_clusters(project.id) - - cluster = TopicCluster.objects.get(project=project, is_active=True) - memberships = list( - ContentClusterMembership.objects.filter(cluster=cluster).values_list( - "content_id", flat=True - ) - ) - - assert result["contents_considered"] == 5 - assert result["clusters_updated"] == 1 - assert cluster.member_count == 4 - assert cluster.dominant_entity == source_plugin_context.entity - assert set(memberships) == {_require_pk(content) for content in clustered_contents} - assert _require_pk(outlier) not in memberships - delay_mock.assert_called_once_with(project.id) - - -def test_assign_content_to_topic_cluster_adds_similar_content_to_existing_cluster( - source_plugin_context, mocker -): - project = source_plugin_context.project - vector_lookup = { - "Cluster 1": [1.0, 0.0], - "Cluster 2": [0.99, 0.01], - "Cluster 3": [0.98, 0.02], - "Candidate": [0.97, 0.03], - } - mocker.patch( - "trends.tasks.embed_text", - side_effect=lambda text: vector_lookup[text.split("\n\n", 1)[0]], - ) - - existing_contents = [] - for index in range(3): - content = Content.objects.create( - project=project, - entity=source_plugin_context.entity, - url=f"https://example.com/cluster-{index}", - title=f"Cluster {index + 1}", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date=f"2026-04-2{index}T12:00:00Z", - content_text="Existing cluster content", - ) - existing_contents.append(content) - cluster = TopicCluster.objects.create( - project=project, - first_seen_at=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), - last_seen_at=datetime(2026, 4, 22, 12, 0, tzinfo=timezone.utc), - is_active=True, - member_count=3, - dominant_entity=source_plugin_context.entity, - ) - ContentClusterMembership.objects.bulk_create( - [ - ContentClusterMembership( - content=content, - cluster=cluster, - project=project, - similarity=0.9, - ) - for content in existing_contents - ] - ) - candidate = Content.objects.create( - project=project, - entity=source_plugin_context.entity, - url="https://example.com/candidate", - title="Candidate", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-24T12:00:00Z", - content_text="New similar cluster content", - ) - - result = assign_content_to_topic_cluster(_require_pk(candidate)) - - cluster.refresh_from_db() - membership = ContentClusterMembership.objects.get(content=candidate) - assert result["assigned"] is True - assert result["cluster_id"] == _require_pk(cluster) - assert membership.cluster == cluster - assert cluster.member_count == 4 - assert cluster.is_active is True - - -def test_recompute_topic_velocity_detects_synthetic_burst( - source_plugin_context, mocker -): - project = source_plugin_context.project - fixed_now = datetime(2026, 4, 30, 12, 0, tzinfo=timezone.utc) - mocker.patch("trends.tasks.timezone.now", return_value=fixed_now) - cluster = TopicCluster.objects.create( - project=project, - first_seen_at=fixed_now - timedelta(days=8), - last_seen_at=fixed_now, - is_active=True, - member_count=11, - dominant_entity=source_plugin_context.entity, - ) - - membership_rows = [] - for offset in range(1, 8): - content = Content.objects.create( - project=project, - entity=source_plugin_context.entity, - url=f"https://example.com/baseline-{offset}", - title=f"Baseline {offset}", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date=fixed_now - timedelta(days=offset, hours=1), - content_text="Baseline trend content", - ) - membership_rows.append( - ContentClusterMembership( - content=content, - cluster=cluster, - project=project, - similarity=0.9, - ) - ) - for index in range(4): - content = Content.objects.create( - project=project, - entity=source_plugin_context.entity, - url=f"https://example.com/burst-{index}", - title=f"Burst {index}", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date=fixed_now - timedelta(hours=index + 1), - content_text="Burst trend content", - ) - membership_rows.append( - ContentClusterMembership( - content=content, - cluster=cluster, - project=project, - similarity=0.95, - ) - ) - ContentClusterMembership.objects.bulk_create(membership_rows) - - result = recompute_topic_velocity(project.id) - - snapshot = TopicVelocitySnapshot.objects.get(cluster=cluster) - assert result["clusters_evaluated"] == 1 - assert result["snapshots_created"] == 1 - assert snapshot.window_count == 4 - assert snapshot.trailing_mean == pytest.approx(1.0) - assert snapshot.trailing_stddev == pytest.approx(0.0) - assert snapshot.z_score == pytest.approx(3.0) - assert snapshot.velocity_score == pytest.approx(1.0) - - -def test_generate_theme_suggestions_creates_pending_suggestion( - source_plugin_context, settings, mocker -): - settings.OPENROUTER_API_KEY = "test-key" - project = source_plugin_context.project - cluster = TopicCluster.objects.create( - project=project, - first_seen_at=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), - last_seen_at=datetime(2026, 4, 24, 12, 0, tzinfo=timezone.utc), - is_active=True, - member_count=3, - dominant_entity=source_plugin_context.entity, - ) - content = Content.objects.create( - project=project, - entity=source_plugin_context.entity, - url="https://example.com/theme-source", - title="Theme Source", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-24T12:00:00Z", - content_text="Theme source content", - ) - ContentClusterMembership.objects.create( - content=content, - cluster=cluster, - project=project, - similarity=0.95, - ) - TopicVelocitySnapshot.objects.create( - cluster=cluster, - project=project, - window_count=4, - trailing_mean=1.0, - trailing_stddev=0.0, - z_score=3.0, - velocity_score=1.0, - ) - llm_mock = mocker.patch( - "trends.tasks.openrouter_chat_json", - side_effect=[ - SimpleNamespace( - payload={ - "title": "Platform teams are consolidating around one workflow", - "one_sentence_pitch": "A burst of similar coverage suggests a coherent newsletter theme.", - "why_it_matters": "Editors can turn the cluster into a timely section.", - "suggested_angle": "Explain what changed this week.", - }, - model=settings.AI_SUMMARIZATION_MODEL, - latency_ms=123, - ), - SimpleNamespace( - payload={"novelty_score": 0.91, "explanation": "Novel enough."}, - model=settings.AI_RELEVANCE_MODEL, - latency_ms=98, - ), - ], - ) - - result = generate_theme_suggestions(project.id) - - suggestion = ThemeSuggestion.objects.get(project=project, cluster=cluster) - assert result["created"] == 1 - assert suggestion.status == ThemeSuggestionStatus.PENDING - assert suggestion.title == "Platform teams are consolidating around one workflow" - assert suggestion.novelty_score == pytest.approx(0.91) - assert suggestion.velocity_at_creation == pytest.approx(1.0) - assert llm_mock.call_count == 2 - - -def test_generate_theme_suggestions_updates_existing_pending_for_same_cluster( - source_plugin_context, -): - project = source_plugin_context.project - cluster = TopicCluster.objects.create( - project=project, - first_seen_at=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), - last_seen_at=datetime(2026, 4, 24, 12, 0, tzinfo=timezone.utc), - is_active=True, - member_count=3, - dominant_entity=source_plugin_context.entity, - ) - TopicVelocitySnapshot.objects.create( - cluster=cluster, - project=project, - window_count=4, - trailing_mean=1.0, - trailing_stddev=0.0, - z_score=3.0, - velocity_score=0.88, - ) - suggestion = ThemeSuggestion.objects.create( - project=project, - cluster=cluster, - title="Existing pending theme", - pitch="Pitch", - why_it_matters="Why", - suggested_angle="Angle", - velocity_at_creation=0.2, - novelty_score=0.8, - ) - - result = generate_theme_suggestions(project.id) - - suggestion.refresh_from_db() - assert result["created"] == 0 - assert result["updated"] == 1 - assert ThemeSuggestion.objects.filter(project=project, cluster=cluster).count() == 1 - assert suggestion.velocity_at_creation == pytest.approx(0.88) - - -def test_accept_theme_suggestion_marks_cluster_members_for_newsletter_promotion( - source_plugin_context, -): - project = source_plugin_context.project - cluster = TopicCluster.objects.create( - project=project, - first_seen_at=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), - last_seen_at=datetime(2026, 4, 24, 12, 0, tzinfo=timezone.utc), - is_active=True, - member_count=2, - dominant_entity=source_plugin_context.entity, - ) - primary_content = Content.objects.create( - project=project, - entity=source_plugin_context.entity, - url="https://example.com/promote-1", - title="Promoted One", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-24T12:00:00Z", - content_text="Primary theme content", - ) - secondary_content = Content.objects.create( - project=project, - entity=source_plugin_context.entity, - url="https://example.com/promote-2", - title="Promoted Two", - author="Author", - source_plugin=SourcePluginName.REDDIT, - published_date="2026-04-24T13:00:00Z", - content_text="Secondary theme content", - ) - ContentClusterMembership.objects.bulk_create( - [ - ContentClusterMembership( - content=primary_content, - cluster=cluster, - project=project, - similarity=0.95, - ), - ContentClusterMembership( - content=secondary_content, - cluster=cluster, - project=project, - similarity=0.9, - ), - ] - ) - suggestion = ThemeSuggestion.objects.create( - project=project, - cluster=cluster, - title="Accepted Theme", - pitch="Pitch", - why_it_matters="Why", - suggested_angle="Angle", - velocity_at_creation=0.7, - novelty_score=0.8, - ) - - accept_theme_suggestion(suggestion, user_id=source_plugin_context.user.id) - - suggestion.refresh_from_db() - primary_content.refresh_from_db() - secondary_content.refresh_from_db() - assert suggestion.status == ThemeSuggestionStatus.ACCEPTED - assert primary_content.newsletter_promotion_theme == suggestion - assert secondary_content.newsletter_promotion_theme == suggestion - assert primary_content.newsletter_promotion_by == source_plugin_context.user - assert secondary_content.newsletter_promotion_by == source_plugin_context.user - assert primary_content.newsletter_promotion_at is not None - assert secondary_content.newsletter_promotion_at is not None - - -def test_run_ingestion_marks_failure_when_plugin_errors(source_plugin_context, mocker): - parse_mock = mocker.patch("core.plugins.rss.feedparser.parse") - source_config = SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/feed.xml"}, - ) - parse_mock.side_effect = RuntimeError("feed unavailable") - - with pytest.raises(RuntimeError, match="feed unavailable"): - run_ingestion(_require_pk(source_config)) - - ingestion_run = IngestionRun.objects.get( - project=source_plugin_context.project, plugin_name=SourcePluginName.RSS - ) - assert ingestion_run.status == RunStatus.FAILED - assert ingestion_run.error_message == "feed unavailable" - - def test_queue_content_skill_enqueues_relevance_task(source_plugin_context, mocker): content = Content.objects.create( project=source_plugin_context.project, @@ -1225,115 +238,6 @@ def test_queue_content_skill_executes_summary_inline_when_eager( delay_mock.assert_not_called() -def test_queue_topic_centroid_recompute_enqueues_background_task( - source_plugin_context, mocker -): - cache_add_mock = mocker.patch("trends.tasks.cache.add", return_value=True) - delay_mock = mocker.patch("trends.tasks.recompute_topic_centroid.delay") - - queued = queue_topic_centroid_recompute(source_plugin_context.project.id) - - assert queued is True - cache_add_mock.assert_called_once() - delay_mock.assert_called_once_with(source_plugin_context.project.id) - - -def test_queue_topic_centroid_recompute_skips_duplicate_queue_attempts( - source_plugin_context, mocker -): - mocker.patch("trends.tasks.cache.add", return_value=False) - delay_mock = mocker.patch("trends.tasks.recompute_topic_centroid.delay") - - queued = queue_topic_centroid_recompute(source_plugin_context.project.id) - - assert queued is False - delay_mock.assert_not_called() - - -def test_feedback_model_create_queues_topic_centroid_recompute( - source_plugin_context, mocker -): - content = Content.objects.create( - project=source_plugin_context.project, - entity=source_plugin_context.entity, - url="https://example.com/direct-feedback-content", - title="Direct Feedback Content", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-20T12:00:00Z", - content_text="Manual content body", - ) - queue_mock = mocker.patch("core.signals.queue_topic_centroid_recompute") - - UserFeedback.objects.create( - project=source_plugin_context.project, - content=content, - user=source_plugin_context.user, - feedback_type=FeedbackType.UPVOTE, - ) - - queue_mock.assert_called_once_with(source_plugin_context.project.id) - - -def test_feedback_model_update_queues_topic_centroid_recompute( - source_plugin_context, mocker -): - content = Content.objects.create( - project=source_plugin_context.project, - entity=source_plugin_context.entity, - url="https://example.com/direct-feedback-update", - title="Direct Feedback Update", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-20T12:00:00Z", - content_text="Manual content body", - ) - queue_mock = mocker.patch("core.signals.queue_topic_centroid_recompute") - feedback = UserFeedback.objects.create( - project=source_plugin_context.project, - content=content, - user=source_plugin_context.user, - feedback_type=FeedbackType.UPVOTE, - ) - - queue_mock.reset_mock() - feedback.feedback_type = FeedbackType.DOWNVOTE - feedback.save(update_fields=["feedback_type"]) - - queue_mock.assert_called_once_with(source_plugin_context.project.id) - - -def test_feedback_save_skips_topic_centroid_recompute_when_project_config_disables_it( - source_plugin_context, mocker -): - ProjectConfig.objects.create( - project=source_plugin_context.project, - recompute_topic_centroid_on_feedback_save=False, - ) - content = Content.objects.create( - project=source_plugin_context.project, - entity=source_plugin_context.entity, - url="https://example.com/direct-feedback-disabled", - title="Direct Feedback Disabled", - author="Author", - source_plugin=SourcePluginName.RSS, - published_date="2026-04-20T12:00:00Z", - content_text="Manual content body", - ) - queue_mock = mocker.patch("core.signals.queue_topic_centroid_recompute") - - feedback = UserFeedback.objects.create( - project=source_plugin_context.project, - content=content, - user=source_plugin_context.user, - feedback_type=FeedbackType.UPVOTE, - ) - feedback.feedback_type = FeedbackType.DOWNVOTE - feedback.save(update_fields=["feedback_type"]) - - queue_mock.assert_not_called() - - def test_run_relevance_scoring_skill_updates_pending_result( source_plugin_context, mocker ): @@ -1397,42 +301,3 @@ def test_run_summarization_skill_marks_result_failed_when_relevance_is_too_low( assert result.status == SkillStatus.FAILED assert pending_result.status == SkillStatus.FAILED assert "Summarization requires relevance_score" in pending_result.error_message - - -def test_ingest_source_config_truncates_fields_and_processes_inline( - source_plugin_context, settings, mocker -): - settings.CELERY_TASK_ALWAYS_EAGER = True - plugin = mocker.Mock() - plugin.fetch_new_content.return_value = [ - SimpleNamespace( - url="https://example.com/post-long", - title="T" * 600, - author="A" * 300, - source_plugin=SourcePluginName.RSS, - published_date=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), - content_text="Summary", - ) - ] - plugin.match_entity_for_url.return_value = source_plugin_context.entity - source_config = SourceConfig.objects.create( - project=source_plugin_context.project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/feed.xml"}, - ) - mocker.patch("ingestion.tasks.get_plugin_for_source_config", return_value=plugin) - upsert_mock = mocker.patch("core.tasks.upsert_content_embedding") - process_mock = mocker.patch("core.tasks.process_content") - delay_mock = mocker.patch("core.tasks.process_content.delay") - - items_fetched, items_ingested = _ingest_source_config(source_config) - - created = Content.objects.get(url="https://example.com/post-long") - assert items_fetched == 1 - assert items_ingested == 1 - assert created.entity == source_plugin_context.entity - assert len(created.title) == 512 - assert len(created.author) == 255 - upsert_mock.assert_called_once_with(created) - process_mock.assert_called_once_with(_require_pk(created)) - delay_mock.assert_not_called() diff --git a/entities/admin.py b/entities/admin.py index 32b7fb4f..a461bcfb 100644 --- a/entities/admin.py +++ b/entities/admin.py @@ -5,7 +5,7 @@ from django.http import HttpRequest from django.utils.html import format_html -from core.entity_extraction import ( +from entities.extraction import ( accept_entity_candidate, merge_entity_candidate, reject_entity_candidate, diff --git a/entities/api.py b/entities/api.py index c54c1d8f..34202bdf 100644 --- a/entities/api.py +++ b/entities/api.py @@ -13,7 +13,7 @@ build_crud_action_overrides, document_project_owned_viewset, ) -from core.entity_extraction import ( +from entities.extraction import ( accept_entity_candidate, merge_entity_candidate, reject_entity_candidate, diff --git a/core/entity_extraction.py b/entities/extraction.py similarity index 99% rename from core/entity_extraction.py rename to entities/extraction.py index 1e2426c0..486839fe 100644 --- a/core/entity_extraction.py +++ b/entities/extraction.py @@ -12,13 +12,9 @@ from django.db.models import Model from django.utils import timezone +from content.models import Content from core.embeddings import search_similar_entities_for_content from core.llm import build_skill_user_prompt, get_skill_definition, openrouter_chat_json -from core.models import ( - Content, - SkillResult, - SkillStatus, -) from entities.models import ( Entity, EntityCandidate, @@ -28,6 +24,7 @@ EntityMentionSentiment, EntityType, ) +from pipeline.models import SkillResult, SkillStatus ENTITY_EXTRACTION_SKILL_NAME = "entity_extraction" ENTITY_RETRIEVAL_LIMIT = 8 diff --git a/entities/tests/test_admin.py b/entities/tests/test_admin.py new file mode 100644 index 00000000..5769c7ed --- /dev/null +++ b/entities/tests/test_admin.py @@ -0,0 +1,246 @@ +from types import SimpleNamespace +from typing import Any, cast +from unittest.mock import Mock + +import pytest +from django.contrib.admin.sites import AdminSite +from django.db.models import Model +from django.http import HttpRequest +from django.test import RequestFactory +from django.utils import timezone + +from content.models import Content +from entities.admin import ( + EntityAdmin, + EntityAuthoritySnapshotAdmin, + EntityCandidateAdmin, +) +from entities.models import ( + Entity, + EntityAuthoritySnapshot, + EntityCandidate, + EntityCandidateStatus, + EntityMention, +) +from projects.model_support import SourcePluginName +from projects.models import Project + +pytestmark = pytest.mark.django_db + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed admin test assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: Any, **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +def _request(query_params: dict[str, str] | None = None) -> HttpRequest: + """Build a typed request object for admin actions and filters.""" + + return RequestFactory().get("/admin/", data=query_params or {}) + + +def _message_user_mock(admin_instance: Any, mocker: Any) -> Mock: + """Install a mock for ModelAdmin.message_user and return it for assertions.""" + + message_mock = cast(Mock, mocker.Mock()) + admin_instance.message_user = message_mock + return message_mock + + +@pytest.fixture +def source_admin_context(django_user_model): + user = _create_user( + django_user_model, username="admin-owner", password="testpass123" + ) + project = Project.objects.create(name="Admin Project", topic_description="Infra") + return SimpleNamespace(user=user, project=project) + + +@pytest.mark.parametrize( + ("authority_score", "expected_color", "expected_display"), + [ + (0.9, "green", "90.0%"), + (0.6, "orange", "60.0%"), + (0.2, "red", "20.0%"), + ], +) +def test_entity_colored_score_uses_expected_color( + source_admin_context, authority_score, expected_color, expected_display +): + entity = Entity.objects.create( + project=source_admin_context.project, + name=f"Entity {authority_score}", + type="vendor", + authority_score=authority_score, + website_url=f"https://entity-{authority_score}.example.com", + ) + admin_instance = EntityAdmin(Entity, AdminSite()) + + rendered = admin_instance.colored_score(entity) + + assert expected_color in rendered + assert expected_display in rendered + + +def test_entity_admin_latest_snapshot_summary_renders_components(source_admin_context): + entity = Entity.objects.create( + project=source_admin_context.project, + name="Snapshot Entity", + type="vendor", + authority_score=0.73, + ) + EntityAuthoritySnapshot.objects.create( + entity=entity, + project=source_admin_context.project, + mention_component=0.7, + feedback_component=0.55, + duplicate_component=0.4, + decayed_prior=0.5, + final_score=0.73, + ) + admin_instance = EntityAdmin(Entity, AdminSite()) + + rendered = admin_instance.latest_snapshot_summary(entity) + + assert "M 70.0%" in rendered + assert "F 55.0%" in rendered + assert "D 40.0%" in rendered + assert "Carry 50.0%" in rendered + + +def test_entity_authority_snapshot_admin_helpers_render_expected_values( + source_admin_context, +): + entity = Entity.objects.create( + project=source_admin_context.project, + name="Snapshot Admin Entity", + type="vendor", + authority_score=0.81, + ) + snapshot = EntityAuthoritySnapshot.objects.create( + entity=entity, + project=source_admin_context.project, + mention_component=0.8, + feedback_component=0.6, + duplicate_component=0.4, + decayed_prior=0.5, + final_score=0.81, + ) + admin_instance = EntityAuthoritySnapshotAdmin(EntityAuthoritySnapshot, AdminSite()) + + rendered_score = admin_instance.display_final_score(snapshot) + rendered_components = admin_instance.display_components(snapshot) + + assert "81.0%" in rendered_score + assert "green" in rendered_score + assert "M 80.0%" in rendered_components + assert "F 60.0%" in rendered_components + + +def test_accept_selected_entity_candidates_creates_entity_and_backfills_mentions( + source_admin_context, mocker +): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/river-labs-launch", + title="River Labs ships a new platform release", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="River Labs announced a new hosted control plane.", + ) + candidate = EntityCandidate.objects.create( + project=source_admin_context.project, + name="River Labs", + suggested_type="vendor", + first_seen_in=content, + occurrence_count=2, + ) + admin_instance = EntityCandidateAdmin(EntityCandidate, AdminSite()) + _message_user_mock(admin_instance, mocker) + + admin_instance.accept_selected_candidates( + request=_request(), + queryset=EntityCandidate.objects.filter(pk=candidate.pk), + ) + + candidate.refresh_from_db() + content.refresh_from_db() + entity = Entity.objects.get( + project=source_admin_context.project, + name="River Labs", + ) + mention = EntityMention.objects.get(content=content, entity=entity) + + assert candidate.status == EntityCandidateStatus.ACCEPTED + assert candidate.merged_into == entity + assert mention.role == "subject" + assert content.entity == entity + + +def test_reject_selected_entity_candidates_marks_candidates_rejected( + source_admin_context, mocker +): + candidate = EntityCandidate.objects.create( + project=source_admin_context.project, + name="Rejected Vendor", + suggested_type="vendor", + ) + admin_instance = EntityCandidateAdmin(EntityCandidate, AdminSite()) + _message_user_mock(admin_instance, mocker) + + admin_instance.reject_selected_candidates( + request=_request(), + queryset=EntityCandidate.objects.filter(pk=candidate.pk), + ) + + candidate.refresh_from_db() + + assert candidate.status == EntityCandidateStatus.REJECTED + + +def test_merge_selected_entity_candidates_uses_existing_same_name_entity( + source_admin_context, mocker +): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/acme-merge", + title="Acme ships a new platform feature", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Acme expanded its hosted platform product.", + ) + entity = Entity.objects.create( + project=source_admin_context.project, + name="Acme", + type="vendor", + ) + candidate = EntityCandidate.objects.create( + project=source_admin_context.project, + name="Acme", + suggested_type="vendor", + first_seen_in=content, + ) + admin_instance = EntityCandidateAdmin(EntityCandidate, AdminSite()) + _message_user_mock(admin_instance, mocker) + + admin_instance.merge_into_existing_entities( + request=_request(), + queryset=EntityCandidate.objects.filter(pk=candidate.pk), + ) + + candidate.refresh_from_db() + + assert candidate.status == EntityCandidateStatus.MERGED + assert candidate.merged_into == entity diff --git a/entities/tests/test_api.py b/entities/tests/test_api.py new file mode 100644 index 00000000..3ed85f3b --- /dev/null +++ b/entities/tests/test_api.py @@ -0,0 +1,386 @@ +from typing import Any, cast + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient, APITestCase + +from content.models import Content +from entities.models import ( + Entity, + EntityAuthoritySnapshot, + EntityCandidate, + EntityCandidateStatus, + EntityMention, +) +from projects.models import Project, ProjectMembership, ProjectRole + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed API test assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _typed_client(client: object) -> APIClient: + """Cast the DRF test client so Pylance sees APIClient helpers.""" + + return cast(APIClient, client) + + +def _create_user(user_model: type[Any], **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +class EntityApiTests(APITestCase): + """Exercise entity-owned project API endpoints.""" + + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpass123") + self.other_user = _create_user( + user_model, username="other", password="testpass123" + ) + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + self.other_project = Project.objects.create( + name="Other Project", + topic_description="Frontend", + ) + ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + ProjectMembership.objects.create( + user=self.other_user, + project=self.other_project, + role=ProjectRole.ADMIN, + ) + self.owner_entity = Entity.objects.create( + project=self.owner_project, + name="Owner Entity", + type="individual", + ) + self.other_entity = Entity.objects.create( + project=self.other_project, + name="Other Entity", + type="vendor", + ) + self.owner_content = Content.objects.create( + project=self.owner_project, + url="https://example.com/owner", + title="Owner Content", + author="Owner Author", + entity=self.owner_entity, + source_plugin="rss", + published_date="2026-04-21T00:00:00Z", + content_text="Owner content text", + ) + self.other_content = Content.objects.create( + project=self.other_project, + url="https://example.com/other", + title="Other Content", + author="Other Author", + entity=self.other_entity, + source_plugin="rss", + published_date="2026-04-21T00:00:00Z", + content_text="Other content text", + ) + _typed_client(self.client).force_authenticate(self.owner) + + def assert_standardized_validation_error( + self, payload: dict[str, object], attr: str + ): + """Assert the repo-standardized validation payload shape.""" + + self.assertEqual(payload["type"], "validation_error") + errors = cast(list[dict[str, object]], payload["errors"]) + self.assertTrue(any(error["attr"] == attr for error in errors)) + + def test_entity_list_is_scoped_to_request_user_project(self): + response = self.client.get( + reverse( + "v1:project-entity-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]["id"], _require_pk(self.owner_entity)) + + def test_entity_list_includes_recent_mentions(self): + mention = EntityMention.objects.create( + project=self.owner_project, + content=self.owner_content, + entity=self.owner_entity, + role="subject", + sentiment="neutral", + span="Owner Entity", + confidence=0.88, + ) + + response = self.client.get( + reverse( + "v1:project-entity-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()[0]["mention_count"], 1) + self.assertEqual( + response.json()[0]["latest_mentions"][0]["id"], _require_pk(mention) + ) + self.assertEqual( + response.json()[0]["latest_mentions"][0]["content_title"], + self.owner_content.title, + ) + + def test_entity_mentions_action_returns_full_mention_history(self): + first_mention = EntityMention.objects.create( + project=self.owner_project, + content=self.owner_content, + entity=self.owner_entity, + role="subject", + sentiment="neutral", + span="Owner Entity", + confidence=0.88, + ) + second_content = Content.objects.create( + project=self.owner_project, + url="https://example.com/owner-second", + title="Second Owner Content", + author="Owner Author", + entity=self.owner_entity, + source_plugin="rss", + published_date="2026-04-22T00:00:00Z", + content_text="Another owner content text", + ) + second_mention = EntityMention.objects.create( + project=self.owner_project, + content=second_content, + entity=self.owner_entity, + role="mentioned", + sentiment="positive", + span="Owner Entity", + confidence=0.67, + ) + + response = self.client.get( + reverse( + "v1:project-entity-mentions", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(self.owner_entity), + }, + ) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()), 2) + self.assertEqual(response.json()[0]["id"], _require_pk(second_mention)) + self.assertEqual(response.json()[1]["id"], _require_pk(first_mention)) + self.assertEqual(response.json()[0]["content_title"], second_content.title) + + def test_entity_list_supports_authority_score_ordering(self): + second_entity = Entity.objects.create( + project=self.owner_project, + name="Second Entity", + type="vendor", + authority_score=0.9, + ) + self.owner_entity.authority_score = 0.4 + self.owner_entity.save(update_fields=["authority_score"]) + + response = self.client.get( + reverse( + "v1:project-entity-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + {"ordering": "-authority_score"}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()[0]["id"], _require_pk(second_entity)) + self.assertEqual(response.json()[1]["id"], _require_pk(self.owner_entity)) + + def test_entity_authority_history_action_returns_recent_snapshots(self): + first_snapshot = EntityAuthoritySnapshot.objects.create( + entity=self.owner_entity, + project=self.owner_project, + mention_component=0.6, + feedback_component=0.5, + duplicate_component=0.5, + decayed_prior=0.5, + final_score=0.53, + ) + second_snapshot = EntityAuthoritySnapshot.objects.create( + entity=self.owner_entity, + project=self.owner_project, + mention_component=0.8, + feedback_component=0.7, + duplicate_component=0.6, + decayed_prior=0.53, + final_score=0.66, + ) + + response = self.client.get( + reverse( + "v1:project-entity-authority-history", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(self.owner_entity), + }, + ), + {"limit": 1}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]["id"], _require_pk(second_snapshot)) + self.assertNotEqual(response.json()[0]["id"], _require_pk(first_snapshot)) + + def test_nested_entity_list_rejects_other_users_project(self): + response = self.client.get( + reverse( + "v1:project-entity-list", + kwargs={"project_id": _require_pk(self.other_project)}, + ) + ) + + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_entity_candidate_list_is_scoped_to_request_user_project(self): + owner_candidate = EntityCandidate.objects.create( + project=self.owner_project, + name="Owner Candidate", + suggested_type="vendor", + first_seen_in=self.owner_content, + ) + EntityCandidate.objects.create( + project=self.other_project, + name="Other Candidate", + suggested_type="organization", + first_seen_in=self.other_content, + ) + + response = self.client.get( + reverse( + "v1:project-entity-candidate-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]["id"], _require_pk(owner_candidate)) + + def test_entity_candidate_accept_action_returns_updated_candidate(self): + candidate = EntityCandidate.objects.create( + project=self.owner_project, + name="River Labs", + suggested_type="vendor", + first_seen_in=self.owner_content, + ) + + response = self.client.post( + reverse( + "v1:project-entity-candidate-accept", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(candidate), + }, + ), + format="json", + ) + + candidate.refresh_from_db() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(candidate.status, EntityCandidateStatus.ACCEPTED) + self.assertIsNotNone(candidate.merged_into) + self.assertEqual(response.json()["status"], EntityCandidateStatus.ACCEPTED) + + def test_entity_candidate_reject_action_returns_updated_candidate(self): + candidate = EntityCandidate.objects.create( + project=self.owner_project, + name="Rejected Candidate", + suggested_type="organization", + first_seen_in=self.owner_content, + ) + + response = self.client.post( + reverse( + "v1:project-entity-candidate-reject", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(candidate), + }, + ), + format="json", + ) + + candidate.refresh_from_db() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(candidate.status, EntityCandidateStatus.REJECTED) + self.assertEqual(response.json()["status"], EntityCandidateStatus.REJECTED) + + def test_entity_candidate_merge_rejects_cross_project_entity(self): + candidate = EntityCandidate.objects.create( + project=self.owner_project, + name="Merge Candidate", + suggested_type="vendor", + first_seen_in=self.owner_content, + ) + + response = self.client.post( + reverse( + "v1:project-entity-candidate-merge", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(candidate), + }, + ), + {"merged_into": _require_pk(self.other_entity)}, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assert_standardized_validation_error(response.json(), "merged_into") + + def test_entity_candidate_merge_action_returns_updated_candidate(self): + candidate = EntityCandidate.objects.create( + project=self.owner_project, + name="Owner Entity Alias", + suggested_type="vendor", + first_seen_in=self.owner_content, + ) + + response = self.client.post( + reverse( + "v1:project-entity-candidate-merge", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(candidate), + }, + ), + {"merged_into": _require_pk(self.owner_entity)}, + format="json", + ) + + candidate.refresh_from_db() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(candidate.status, EntityCandidateStatus.MERGED) + self.assertEqual(candidate.merged_into, self.owner_entity) + self.assertEqual( + response.json()["merged_into"], _require_pk(self.owner_entity) + ) \ No newline at end of file diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index cc902b62..cd51df15 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2024.d.ts","./node_modules/typescript/lib/lib.es2025.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2024.collection.d.ts","./node_modules/typescript/lib/lib.es2024.object.d.ts","./node_modules/typescript/lib/lib.es2024.promise.d.ts","./node_modules/typescript/lib/lib.es2024.regexp.d.ts","./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2024.string.d.ts","./node_modules/typescript/lib/lib.es2025.collection.d.ts","./node_modules/typescript/lib/lib.es2025.float16.d.ts","./node_modules/typescript/lib/lib.es2025.intl.d.ts","./node_modules/typescript/lib/lib.es2025.iterator.d.ts","./node_modules/typescript/lib/lib.es2025.promise.d.ts","./node_modules/typescript/lib/lib.es2025.regexp.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.date.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.error.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","./node_modules/typescript/lib/lib.esnext.temporal.d.ts","./node_modules/typescript/lib/lib.esnext.typedarrays.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/react/global.d.ts","./node_modules/csstype/index.d.ts","./node_modules/@types/react/index.d.ts","./node_modules/next/dist/styled-jsx/types/css.d.ts","./node_modules/next/dist/styled-jsx/types/macro.d.ts","./node_modules/next/dist/styled-jsx/types/style.d.ts","./node_modules/next/dist/styled-jsx/types/global.d.ts","./node_modules/next/dist/styled-jsx/types/index.d.ts","./node_modules/next/dist/server/get-page-files.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/blob.d.ts","./node_modules/@types/node/web-globals/console.d.ts","./node_modules/@types/node/web-globals/crypto.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/encoding.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/utility.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client-stats.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/round-robin-pool.d.ts","./node_modules/undici-types/h2c-client.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-call-history.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/snapshot-agent.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/cache-interceptor.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/importmeta.d.ts","./node_modules/@types/node/web-globals/messaging.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/performance.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/web-globals/streams.d.ts","./node_modules/@types/node/web-globals/timers.d.ts","./node_modules/@types/node/web-globals/url.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/inspector/promises.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/path/posix.d.ts","./node_modules/@types/node/path/win32.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/quic.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/test/reporters.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/util/types.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/react/canary.d.ts","./node_modules/@types/react/experimental.d.ts","./node_modules/@types/react-dom/index.d.ts","./node_modules/@types/react-dom/canary.d.ts","./node_modules/@types/react-dom/experimental.d.ts","./node_modules/next/dist/lib/fallback.d.ts","./node_modules/next/dist/compiled/webpack/webpack.d.ts","./node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","./node_modules/next/dist/shared/lib/entry-constants.d.ts","./node_modules/next/dist/shared/lib/constants.d.ts","./node_modules/next/dist/lib/bundler.d.ts","./node_modules/next/dist/server/config.d.ts","./node_modules/next/dist/lib/load-custom-routes.d.ts","./node_modules/next/dist/shared/lib/image-config.d.ts","./node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","./node_modules/next/dist/server/body-streams.d.ts","./node_modules/next/dist/server/request/search-params.d.ts","./node_modules/next/dist/shared/lib/segment-cache/vary-params-decoding.d.ts","./node_modules/next/dist/server/app-render/vary-params.d.ts","./node_modules/next/dist/server/request/params.d.ts","./node_modules/next/dist/server/route-kind.d.ts","./node_modules/next/dist/server/route-definitions/route-definition.d.ts","./node_modules/next/dist/server/route-matches/route-match.d.ts","./node_modules/next/dist/client/components/app-router-headers.d.ts","./node_modules/next/dist/server/lib/cache-control.d.ts","./node_modules/next/dist/shared/lib/app-router-types.d.ts","./node_modules/next/dist/server/lib/cache-handlers/types.d.ts","./node_modules/next/dist/server/use-cache/use-cache-wrapper.d.ts","./node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","./node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","./node_modules/next/dist/lib/constants.d.ts","./node_modules/next/dist/server/render-result.d.ts","./node_modules/next/dist/server/response-cache/types.d.ts","./node_modules/next/dist/server/response-cache/index.d.ts","./node_modules/@types/react/jsx-runtime.d.ts","./node_modules/next/dist/next-devtools/userspace/pages/pages-dev-overlay-setup.d.ts","./node_modules/next/dist/build/static-paths/types.d.ts","./node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","./node_modules/next/dist/build/adapter/setup-node-env.external.d.ts","./node_modules/next/dist/server/instrumentation/types.d.ts","./node_modules/next/dist/lib/setup-exception-listeners.d.ts","./node_modules/next/dist/lib/worker.d.ts","./node_modules/next/dist/server/lib/experimental/ppr.d.ts","./node_modules/next/dist/lib/page-types.d.ts","./node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","./node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","./node_modules/next/dist/build/analysis/get-page-static-info.d.ts","./node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","./node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","./node_modules/next/dist/server/require-hook.d.ts","./node_modules/next/dist/server/node-polyfill-crypto.d.ts","./node_modules/next/dist/server/node-environment-baseline.d.ts","./node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","./node_modules/next/dist/server/node-environment-extensions/console-file.d.ts","./node_modules/next/dist/server/node-environment-extensions/console-exit.d.ts","./node_modules/next/dist/server/node-environment-extensions/console-dim.external.d.ts","./node_modules/next/dist/server/node-environment-extensions/unhandled-rejection.external.d.ts","./node_modules/next/dist/server/node-environment-extensions/random.d.ts","./node_modules/next/dist/server/node-environment-extensions/date.d.ts","./node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","./node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","./node_modules/next/dist/server/node-environment-extensions/fast-set-immediate.external.d.ts","./node_modules/next/dist/server/node-environment.d.ts","./node_modules/next/dist/build/page-extensions-type.d.ts","./node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","./node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","./node_modules/next/dist/server/lib/i18n-provider.d.ts","./node_modules/next/dist/server/web/next-url.d.ts","./node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","./node_modules/next/dist/server/web/spec-extension/cookies.d.ts","./node_modules/next/dist/server/web/spec-extension/request.d.ts","./node_modules/next/dist/shared/lib/deep-readonly.d.ts","./node_modules/next/dist/server/lib/incremental-cache/index.d.ts","./node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","./node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","./node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","./node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","./node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","./node_modules/next/dist/shared/lib/mitt.d.ts","./node_modules/next/dist/client/with-router.d.ts","./node_modules/next/dist/client/router.d.ts","./node_modules/next/dist/client/route-loader.d.ts","./node_modules/next/dist/client/page-loader.d.ts","./node_modules/next/dist/shared/lib/bloom-filter.d.ts","./node_modules/next/dist/shared/lib/router/router.d.ts","./node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","./node_modules/next/dist/client/components/readonly-url-search-params.d.ts","./node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","./node_modules/next/dist/client/flight-data-helpers.d.ts","./node_modules/next/dist/client/components/segment-cache/cache-key.d.ts","./node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","./node_modules/next/dist/client/components/segment-cache/types.d.ts","./node_modules/next/dist/shared/lib/segment-cache/segment-value-encoding.d.ts","./node_modules/next/dist/client/components/segment-cache/scheduler.d.ts","./node_modules/next/dist/client/components/segment-cache/cache-map.d.ts","./node_modules/next/dist/client/components/segment-cache/vary-path.d.ts","./node_modules/next/dist/client/components/segment-cache/cache.d.ts","./node_modules/next/dist/client/components/router-reducer/ppr-navigations.d.ts","./node_modules/next/dist/client/components/segment-cache/navigation.d.ts","./node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","./node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","./node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","./node_modules/next/dist/build/templates/pages.d.ts","./node_modules/next/dist/server/route-modules/pages/module.d.ts","./node_modules/next/dist/server/render.d.ts","./node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","./node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","./node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","./node_modules/next/dist/server/route-matchers/route-matcher.d.ts","./node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","./node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","./node_modules/next/dist/server/normalizers/normalizer.d.ts","./node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","./node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","./node_modules/next/dist/server/normalizers/request/suffix.d.ts","./node_modules/next/dist/server/normalizers/request/rsc.d.ts","./node_modules/next/dist/server/normalizers/request/next-data.d.ts","./node_modules/next/dist/server/after/builtin-request-context.d.ts","./node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.d.ts","./node_modules/next/dist/server/route-modules/pages/builtin/_error.d.ts","./node_modules/next/dist/server/load-default-error-components.d.ts","./node_modules/next/dist/server/base-server.d.ts","./node_modules/next/dist/server/after/after.d.ts","./node_modules/next/dist/server/after/after-context.d.ts","./node_modules/next/dist/server/use-cache/cache-life.d.ts","./node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","./node_modules/next/dist/server/lib/lazy-result.d.ts","./node_modules/next/dist/server/app-render/create-error-handler.d.ts","./node_modules/next/dist/shared/lib/action-revalidation-kind.d.ts","./node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","./node_modules/next/dist/server/async-storage/work-store.d.ts","./node_modules/next/dist/server/web/http.d.ts","./node_modules/next/dist/client/components/hooks-server-context.d.ts","./node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","./node_modules/next/dist/client/components/redirect-status-code.d.ts","./node_modules/next/dist/client/components/redirect-error.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","./node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","./node_modules/next/dist/server/app-render/cache-signal.d.ts","./node_modules/next/dist/server/app-render/instant-validation/boundary-tracking.d.ts","./node_modules/next/dist/server/app-render/instant-validation/instant-validation-error.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","./node_modules/next/dist/server/app-render/instant-validation/instant-samples.d.ts","./node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","./node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","./node_modules/next/dist/server/lib/implicit-tags.d.ts","./node_modules/next/dist/server/app-render/staged-rendering.d.ts","./node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","./node_modules/next/dist/build/templates/app-route.d.ts","./node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","./node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","./node_modules/next/dist/server/route-modules/app-route/module.d.ts","./node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","./node_modules/next/dist/build/segment-config/app/app-segments.d.ts","./node_modules/next/dist/build/get-supported-browsers.d.ts","./node_modules/next/dist/build/utils.d.ts","./node_modules/next/dist/build/rendering-mode.d.ts","./node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.d.ts","./node_modules/next/dist/server/lib/cpu-profile.d.ts","./node_modules/next/dist/build/turborepo-access-trace/types.d.ts","./node_modules/next/dist/build/turborepo-access-trace/result.d.ts","./node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","./node_modules/next/dist/build/turborepo-access-trace/index.d.ts","./node_modules/next/dist/export/routes/types.d.ts","./node_modules/next/dist/export/types.d.ts","./node_modules/next/dist/export/worker.d.ts","./node_modules/next/dist/build/worker.d.ts","./node_modules/next/dist/build/index.d.ts","./node_modules/next/dist/lib/coalesced-function.d.ts","./node_modules/next/dist/server/lib/router-utils/types.d.ts","./node_modules/next/dist/trace/types.d.ts","./node_modules/next/dist/trace/trace.d.ts","./node_modules/next/dist/trace/shared.d.ts","./node_modules/next/dist/trace/index.d.ts","./node_modules/next/dist/build/load-jsconfig.d.ts","./node_modules/@next/env/dist/index.d.ts","./node_modules/next/dist/build/webpack/plugins/telemetry-plugin/use-cache-tracker-utils.d.ts","./node_modules/next/dist/build/webpack/plugins/telemetry-plugin/telemetry-plugin.d.ts","./node_modules/next/dist/telemetry/storage.d.ts","./node_modules/next/dist/build/build-context.d.ts","./node_modules/next/dist/build/webpack-config.d.ts","./node_modules/next/dist/build/swc/generated-native.d.ts","./node_modules/next/dist/build/define-env.d.ts","./node_modules/next/dist/build/swc/index.d.ts","./node_modules/next/dist/build/swc/types.d.ts","./node_modules/next/dist/server/dev/parse-version-info.d.ts","./node_modules/next/dist/next-devtools/shared/types.d.ts","./node_modules/next/dist/server/dev/dev-indicator-server-state.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/cache-indicator.d.ts","./node_modules/next/dist/server/lib/parse-stack.d.ts","./node_modules/next/dist/next-devtools/server/shared.d.ts","./node_modules/next/dist/next-devtools/shared/stack-frame.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/utils/get-error-by-type.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/container/runtime-error/render-error.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/shared.d.ts","./node_modules/next/dist/server/dev/debug-channel.d.ts","./node_modules/next/dist/server/dev/hot-reloader-types.d.ts","./node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","./node_modules/next/dist/server/web/spec-extension/response.d.ts","./node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","./node_modules/next/dist/server/web/types.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","./node_modules/next/dist/server/base-http/node.d.ts","./node_modules/next/dist/server/lib/async-callback-set.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","./node_modules/sharp/lib/index.d.ts","./node_modules/next/dist/server/image-optimizer.d.ts","./node_modules/next/dist/server/next-server.d.ts","./node_modules/next/dist/server/lib/types.d.ts","./node_modules/next/dist/server/lib/lru-cache.d.ts","./node_modules/next/dist/server/lib/dev-bundler-service.d.ts","./node_modules/next/dist/server/dev/static-paths-worker.d.ts","./node_modules/next/dist/server/dev/next-dev-server.d.ts","./node_modules/next/dist/server/next.d.ts","./node_modules/next/dist/server/lib/render-server.d.ts","./node_modules/next/dist/server/lib/router-server.d.ts","./node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","./node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","./node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","./node_modules/next/dist/server/lib/router-utils/router-server-context.d.ts","./node_modules/next/dist/server/route-modules/route-module.d.ts","./node_modules/next/dist/server/load-components.d.ts","./node_modules/next/dist/server/web/adapter.d.ts","./node_modules/next/dist/server/app-render/types.d.ts","./node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","./node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","./node_modules/next/dist/server/lib/app-dir-module.d.ts","./node_modules/next/dist/server/app-render/app-render.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/client/components/error-boundary.d.ts","./node_modules/next/dist/client/components/layout-router.d.ts","./node_modules/next/dist/client/components/render-from-template-context.d.ts","./node_modules/next/dist/client/components/client-page.d.ts","./node_modules/next/dist/client/components/client-segment.d.ts","./node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","./node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","./node_modules/next/dist/lib/metadata/types/extra-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","./node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","./node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","./node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","./node_modules/next/dist/lib/metadata/types/resolvers.d.ts","./node_modules/next/dist/lib/metadata/types/icons.d.ts","./node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","./node_modules/next/dist/lib/metadata/metadata.d.ts","./node_modules/next/dist/lib/framework/boundary-components.d.ts","./node_modules/next/dist/server/app-render/rsc/preloads.d.ts","./node_modules/next/dist/server/app-render/rsc/postpone.d.ts","./node_modules/next/dist/server/app-render/rsc/taint.d.ts","./node_modules/next/dist/server/app-render/collect-segment-data.d.ts","./node_modules/next/dist/server/app-render/instant-validation/instant-validation.d.ts","./node_modules/next/dist/next-devtools/userspace/app/segment-explorer-node.d.ts","./node_modules/next/dist/server/app-render/entry-base.d.ts","./node_modules/next/dist/build/templates/app-page.d.ts","./node_modules/next/dist/server/route-modules/app-page/helpers/prerender-manifest-matcher.d.ts","./node_modules/@types/react/jsx-dev-runtime.d.ts","./node_modules/@types/react/compiler-runtime.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.d.ts","./node_modules/@types/react-dom/client.d.ts","./node_modules/@types/react-dom/static.d.ts","./node_modules/@types/react-dom/server.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/app-page/module.d.ts","./node_modules/next/dist/server/request/fallback-params.d.ts","./node_modules/next/dist/server/web/spec-extension/image-response.d.ts","./node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","./node_modules/next/dist/server/web/spec-extension/url-pattern.d.ts","./node_modules/next/dist/server/after/index.d.ts","./node_modules/next/dist/server/request/connection.d.ts","./node_modules/next/dist/server/web/exports/index.d.ts","./node_modules/next/dist/server/request-meta.d.ts","./node_modules/next/dist/cli/next-test.d.ts","./node_modules/next/dist/shared/lib/size-limit.d.ts","./node_modules/next/dist/server/config-shared.d.ts","./node_modules/next/dist/server/base-http/index.d.ts","./node_modules/next/dist/server/api-utils/index.d.ts","./node_modules/next/dist/build/adapter/build-complete.d.ts","./node_modules/next/dist/types.d.ts","./node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/utils.d.ts","./node_modules/next/dist/pages/_app.d.ts","./node_modules/next/app.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","./node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","./node_modules/next/dist/server/use-cache/cache-tag.d.ts","./node_modules/next/cache.d.ts","./node_modules/next/dist/pages/_document.d.ts","./node_modules/next/document.d.ts","./node_modules/next/dist/shared/lib/dynamic.d.ts","./node_modules/next/dynamic.d.ts","./node_modules/next/dist/pages/_error.d.ts","./node_modules/next/dist/client/components/catch-error.d.ts","./node_modules/next/dist/api/error.d.ts","./node_modules/next/error.d.ts","./node_modules/next/dist/shared/lib/head.d.ts","./node_modules/next/head.d.ts","./node_modules/next/dist/server/request/cookies.d.ts","./node_modules/next/dist/server/request/headers.d.ts","./node_modules/next/dist/server/request/draft-mode.d.ts","./node_modules/next/headers.d.ts","./node_modules/next/dist/shared/lib/get-img-props.d.ts","./node_modules/next/dist/client/image-component.d.ts","./node_modules/next/dist/shared/lib/image-external.d.ts","./node_modules/next/image.d.ts","./node_modules/next/dist/client/link.d.ts","./node_modules/next/link.d.ts","./node_modules/next/dist/client/components/unrecognized-action-error.d.ts","./node_modules/next/dist/client/components/redirect.d.ts","./node_modules/next/dist/client/components/not-found.d.ts","./node_modules/next/dist/client/components/forbidden.d.ts","./node_modules/next/dist/client/components/unauthorized.d.ts","./node_modules/next/dist/client/components/unstable-rethrow.server.d.ts","./node_modules/next/dist/client/components/unstable-rethrow.d.ts","./node_modules/next/dist/client/components/navigation.react-server.d.ts","./node_modules/next/dist/client/components/navigation.d.ts","./node_modules/next/navigation.d.ts","./node_modules/next/router.d.ts","./node_modules/next/dist/client/script.d.ts","./node_modules/next/script.d.ts","./node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","./node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","./node_modules/next/dist/compiled/@vercel/og/types.d.ts","./node_modules/next/server.d.ts","./node_modules/next/types/global.d.ts","./node_modules/next/types/compiled.d.ts","./node_modules/next/types.d.ts","./node_modules/next/index.d.ts","./node_modules/next/image-types/global.d.ts","./.next/dev/types/routes.d.ts","./next-env.d.ts","./next.config.ts","./node_modules/vite/types/hmrPayload.d.ts","./node_modules/vite/dist/node/chunks/moduleRunnerTransport.d.ts","./node_modules/vite/types/customEvent.d.ts","./node_modules/rolldown/dist/shared/logging-C6h4g8dA.d.mts","./node_modules/@oxc-project/types/types.d.ts","./node_modules/rolldown/dist/shared/binding-zH1vcmbM.d.mts","./node_modules/rolldown/node_modules/@rolldown/pluginutils/dist/filter/composable-filters.d.ts","./node_modules/rolldown/node_modules/@rolldown/pluginutils/dist/filter/filter-vite-plugins.d.ts","./node_modules/rolldown/node_modules/@rolldown/pluginutils/dist/filter/simple-filters.d.ts","./node_modules/rolldown/node_modules/@rolldown/pluginutils/dist/filter/index.d.ts","./node_modules/rolldown/node_modules/@rolldown/pluginutils/dist/index.d.ts","./node_modules/rolldown/dist/shared/define-config-5HJ1b9vG.d.mts","./node_modules/rolldown/dist/index.d.mts","./node_modules/rolldown/dist/parse-ast-index.d.mts","./node_modules/vite/types/internal/rollupTypeCompat.d.ts","./node_modules/rolldown/dist/shared/constructors-D0W3rNfA.d.mts","./node_modules/rolldown/dist/plugins-index.d.mts","./node_modules/rolldown/dist/shared/transform-DgZ3paSD.d.mts","./node_modules/rolldown/dist/utils-index.d.mts","./node_modules/vite/types/hot.d.ts","./node_modules/vite/dist/node/module-runner.d.ts","./node_modules/vite/types/internal/esbuildOptions.d.ts","./node_modules/vite/types/metadata.d.ts","./node_modules/vite/types/internal/terserOptions.d.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/postcss/lib/postcss.d.mts","./node_modules/lightningcss/node/ast.d.ts","./node_modules/lightningcss/node/targets.d.ts","./node_modules/lightningcss/node/index.d.ts","./node_modules/vite/types/internal/lightningcssOptions.d.ts","./node_modules/vite/types/internal/cssPreprocessorOptions.d.ts","./node_modules/rolldown/dist/filter-index.d.mts","./node_modules/vite/types/importGlob.d.ts","./node_modules/vite/dist/node/index.d.ts","./node_modules/@vitejs/plugin-react/types/optionalTypes.d.ts","./node_modules/@vitejs/plugin-react/dist/index.d.ts","./node_modules/@vitest/spy/optional-types.d.ts","./node_modules/@vitest/spy/dist/index.d.ts","./node_modules/tinyrainbow/dist/index.d.ts","./node_modules/@standard-schema/spec/dist/index.d.ts","./node_modules/@vitest/pretty-format/dist/index.d.ts","./node_modules/@vitest/utils/dist/types.d-BCElaP-c.d.ts","./node_modules/@vitest/utils/dist/diff.d.ts","./node_modules/@vitest/utils/dist/display.d.ts","./node_modules/@types/deep-eql/index.d.ts","./node_modules/assertion-error/index.d.ts","./node_modules/@types/chai/index.d.ts","./node_modules/@vitest/expect/dist/index.d.ts","./node_modules/@vitest/utils/dist/types.d.ts","./node_modules/@vitest/utils/dist/helpers.d.ts","./node_modules/@vitest/utils/dist/timers.d.ts","./node_modules/@vitest/utils/dist/index.d.ts","./node_modules/@vitest/runner/dist/tasks.d-Bh0IjN67.d.ts","./node_modules/@vitest/runner/dist/index.d.ts","./node_modules/vitest/dist/chunks/traces.d.D2T_R8rx.d.ts","./node_modules/@vitest/snapshot/dist/environment.d-DOJxxZV9.d.ts","./node_modules/@vitest/snapshot/dist/rawSnapshot.d-D_X3-62x.d.ts","./node_modules/@vitest/snapshot/dist/index.d.ts","./node_modules/vitest/dist/chunks/config.d.A1h_Y6Jt.d.ts","./node_modules/vitest/dist/chunks/environment.d.CrsxCzP1.d.ts","./node_modules/vitest/dist/chunks/rpc.d.B_8sPU0w.d.ts","./node_modules/vitest/dist/chunks/worker.d.ZpHpO4yb.d.ts","./node_modules/vitest/dist/chunks/browser.d.BcoexmFG.d.ts","./node_modules/vitest/optional-types.d.ts","./node_modules/@vitest/runner/dist/utils.d.ts","./node_modules/tinybench/dist/index.d.ts","./node_modules/vitest/dist/chunks/benchmark.d.DAaHLpsq.d.ts","./node_modules/@vitest/mocker/dist/types.d-BjI5eAwu.d.ts","./node_modules/@vitest/mocker/dist/index.d-B41z0AuW.d.ts","./node_modules/@vitest/mocker/dist/index.d.ts","./node_modules/@vitest/utils/dist/source-map.d.ts","./node_modules/vitest/dist/chunks/coverage.d.BZtK59WP.d.ts","./node_modules/@vitest/utils/dist/serialize.d.ts","./node_modules/@vitest/utils/dist/error.d.ts","./node_modules/vitest/dist/browser.d.ts","./node_modules/vitest/browser/context.d.ts","./node_modules/@vitest/snapshot/dist/manager.d.ts","./node_modules/vitest/dist/chunks/reporters.d.CEnv6XRv.d.ts","./node_modules/vitest/dist/chunks/plugin.d.BM2TCi12.d.ts","./node_modules/vitest/dist/config.d.ts","./node_modules/vitest/config.d.ts","./vitest.config.ts","./node_modules/@types/aria-query/index.d.ts","./node_modules/@testing-library/jest-dom/types/matchers.d.ts","./node_modules/@testing-library/jest-dom/types/jest.d.ts","./node_modules/@testing-library/jest-dom/types/index.d.ts","./node_modules/@testing-library/dom/types/matches.d.ts","./node_modules/@testing-library/dom/types/wait-for.d.ts","./node_modules/@testing-library/dom/types/query-helpers.d.ts","./node_modules/@testing-library/dom/types/queries.d.ts","./node_modules/@testing-library/dom/types/get-queries-for-element.d.ts","./node_modules/pretty-format/build/types.d.ts","./node_modules/pretty-format/build/index.d.ts","./node_modules/@testing-library/dom/types/screen.d.ts","./node_modules/@testing-library/dom/types/wait-for-element-to-be-removed.d.ts","./node_modules/@testing-library/dom/types/get-node-text.d.ts","./node_modules/@testing-library/dom/types/events.d.ts","./node_modules/@testing-library/dom/types/pretty-dom.d.ts","./node_modules/@testing-library/dom/types/role-helpers.d.ts","./node_modules/@testing-library/dom/types/config.d.ts","./node_modules/@testing-library/dom/types/suggestions.d.ts","./node_modules/@testing-library/dom/types/index.d.ts","./node_modules/@types/react-dom/test-utils/index.d.ts","./node_modules/@testing-library/react/types/index.d.ts","./node_modules/vitest/dist/chunks/global.d.DVsSRdQ5.d.ts","./node_modules/vitest/optional-runtime-types.d.ts","./node_modules/vitest/dist/chunks/suite.d.udJtyAgw.d.ts","./node_modules/vitest/dist/chunks/evaluatedModules.d.BxJ5omdx.d.ts","./node_modules/vitest/dist/runners.d.ts","./node_modules/expect-type/dist/utils.d.ts","./node_modules/expect-type/dist/overloads.d.ts","./node_modules/expect-type/dist/branding.d.ts","./node_modules/expect-type/dist/messages.d.ts","./node_modules/expect-type/dist/index.d.ts","./node_modules/vitest/dist/index.d.ts","./vitest.setup.ts","./node_modules/next-auth/adapters.d.ts","./node_modules/jose/dist/types/types.d.ts","./node_modules/jose/dist/types/jwe/compact/decrypt.d.ts","./node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts","./node_modules/jose/dist/types/jwe/general/decrypt.d.ts","./node_modules/jose/dist/types/jwe/general/encrypt.d.ts","./node_modules/jose/dist/types/jws/compact/verify.d.ts","./node_modules/jose/dist/types/jws/flattened/verify.d.ts","./node_modules/jose/dist/types/jws/general/verify.d.ts","./node_modules/jose/dist/types/jwt/verify.d.ts","./node_modules/jose/dist/types/jwt/decrypt.d.ts","./node_modules/jose/dist/types/jwt/produce.d.ts","./node_modules/jose/dist/types/jwe/compact/encrypt.d.ts","./node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts","./node_modules/jose/dist/types/jws/compact/sign.d.ts","./node_modules/jose/dist/types/jws/flattened/sign.d.ts","./node_modules/jose/dist/types/jws/general/sign.d.ts","./node_modules/jose/dist/types/jwt/sign.d.ts","./node_modules/jose/dist/types/jwt/encrypt.d.ts","./node_modules/jose/dist/types/jwk/thumbprint.d.ts","./node_modules/jose/dist/types/jwk/embedded.d.ts","./node_modules/jose/dist/types/jwks/local.d.ts","./node_modules/jose/dist/types/jwks/remote.d.ts","./node_modules/jose/dist/types/jwt/unsecured.d.ts","./node_modules/jose/dist/types/key/export.d.ts","./node_modules/jose/dist/types/key/import.d.ts","./node_modules/jose/dist/types/util/decode_protected_header.d.ts","./node_modules/jose/dist/types/util/decode_jwt.d.ts","./node_modules/jose/dist/types/util/errors.d.ts","./node_modules/jose/dist/types/key/generate_key_pair.d.ts","./node_modules/jose/dist/types/key/generate_secret.d.ts","./node_modules/jose/dist/types/util/base64url.d.ts","./node_modules/jose/dist/types/util/runtime.d.ts","./node_modules/jose/dist/types/index.d.ts","./node_modules/openid-client/types/index.d.ts","./node_modules/next-auth/providers/oauth-types.d.ts","./node_modules/next-auth/providers/oauth.d.ts","./node_modules/next-auth/providers/email.d.ts","./node_modules/next-auth/core/lib/cookie.d.ts","./node_modules/next-auth/core/index.d.ts","./node_modules/next-auth/providers/credentials.d.ts","./node_modules/next-auth/providers/index.d.ts","./node_modules/next-auth/jwt/types.d.ts","./node_modules/next-auth/jwt/index.d.ts","./node_modules/next-auth/utils/logger.d.ts","./node_modules/next-auth/core/types.d.ts","./node_modules/next-auth/next/index.d.ts","./node_modules/next-auth/index.d.ts","./node_modules/next-auth/providers/github.d.ts","./node_modules/next-auth/providers/google.d.ts","./src/lib/auth.ts","./src/app/api/auth/[...nextauth]/route.ts","./src/lib/types.ts","./src/lib/api.ts","./src/app/api/content-skills/route.ts","./src/app/api/content-skills/__tests__/route.test.ts","./src/app/api/entities/route.ts","./src/app/api/entities/[id]/route.ts","./src/app/api/entities/[id]/__tests__/route.test.ts","./src/app/api/entities/__tests__/route.test.ts","./src/app/api/entity-candidates/[id]/route.ts","./src/app/api/entity-candidates/[id]/__tests__/route.test.ts","./src/app/api/feedback/route.ts","./src/app/api/feedback/__tests__/route.test.ts","./src/app/api/invitations/[token]/accept/route.ts","./src/app/api/invitations/[token]/accept/__tests__/route.test.ts","./src/app/api/profile/route.ts","./src/app/api/profile/__tests__/route.test.ts","./src/app/api/profile/avatar/route.ts","./src/app/api/profile/avatar/__tests__/route.test.ts","./src/app/api/projects/route.ts","./src/app/api/projects/[id]/bluesky-credentials/route.ts","./src/app/api/projects/[id]/bluesky-credentials/__tests__/route.test.ts","./src/app/api/projects/[id]/intake/route.ts","./src/app/api/projects/[id]/intake/__tests__/route.test.ts","./src/app/api/projects/[id]/intake-allowlist/route.ts","./src/app/api/projects/[id]/intake-allowlist/[allowlistId]/route.ts","./src/app/api/projects/[id]/intake-allowlist/[allowlistId]/__tests__/route.test.ts","./src/app/api/projects/[id]/intake-allowlist/__tests__/route.test.ts","./src/app/api/projects/[id]/invitations/route.ts","./src/app/api/projects/[id]/invitations/[invitationId]/revoke/route.ts","./src/app/api/projects/[id]/invitations/__tests__/route.test.ts","./src/app/api/projects/[id]/mastodon-credentials/route.ts","./src/app/api/projects/[id]/mastodon-credentials/__tests__/route.test.ts","./src/app/api/projects/[id]/members/[membershipId]/route.ts","./src/app/api/projects/[id]/members/[membershipId]/__tests__/route.test.ts","./src/app/api/projects/[id]/rotate-intake-token/route.ts","./src/app/api/projects/[id]/rotate-intake-token/__tests__/route.test.ts","./src/app/api/projects/[id]/verify-bluesky-credentials/route.ts","./src/app/api/projects/[id]/verify-bluesky-credentials/__tests__/route.test.ts","./src/app/api/projects/[id]/verify-mastodon-credentials/route.ts","./src/app/api/projects/[id]/verify-mastodon-credentials/__tests__/route.test.ts","./src/app/api/projects/__tests__/route.test.ts","./src/app/api/review/[id]/route.ts","./src/app/api/review/[id]/__tests__/route.test.ts","./src/app/api/skills/[skillName]/route.ts","./src/app/api/skills/[skillName]/__tests__/route.test.ts","./src/app/api/source-configs/route.ts","./src/app/api/source-configs/[id]/route.ts","./src/app/api/source-configs/[id]/__tests__/route.test.ts","./src/app/api/source-configs/__tests__/route.test.ts","./src/lib/view-helpers.ts","./src/lib/dashboard-view.ts","./src/lib/profile.ts","./node_modules/@tanstack/query-core/build/modern/_tsup-dts-rollup.d.ts","./node_modules/@tanstack/query-core/build/modern/index.d.ts","./node_modules/@tanstack/react-query/build/modern/_tsup-dts-rollup.d.ts","./node_modules/@tanstack/react-query/build/modern/index.d.ts","./src/lib/useRole.ts","./src/lib/__tests__/api.test.ts","./src/lib/__tests__/auth.test.ts","./src/lib/__tests__/dashboard-view.test.ts","./src/lib/__tests__/view-helpers.test.ts","./test-support/server-only.ts","./node_modules/next/dist/compiled/@next/font/dist/types.d.ts","./node_modules/next/dist/compiled/@next/font/dist/google/index.d.ts","./node_modules/next/font/google/index.d.ts","./src/components/query-provider.tsx","./src/app/layout.tsx","./node_modules/next-auth/client/_utils.d.ts","./node_modules/next-auth/react/types.d.ts","./node_modules/next-auth/react/index.d.ts","./src/components/user-menu.tsx","./src/components/app-shell.tsx","./src/components/status-badge.tsx","./src/app/page.tsx","./src/app/__tests__/page.test.tsx","./src/app/admin/health/page.tsx","./src/app/admin/health/__tests__/page.test.tsx","./src/app/admin/projects/new/page.tsx","./src/components/copy-button.tsx","./src/app/admin/sources/page.tsx","./src/app/admin/sources/__tests__/page.test.tsx","./src/components/skill-action-bar.tsx","./src/app/content/[id]/page.tsx","./src/app/content/[id]/__tests__/page.test.tsx","./src/app/entities/page.tsx","./src/app/entities/[id]/page.tsx","./src/app/entities/[id]/__tests__/page.test.tsx","./src/app/entities/__tests__/page.test.tsx","./src/app/invite/[token]/page.tsx","./src/components/auth/social-auth-buttons.tsx","./src/components/auth/login-form.tsx","./src/app/login/page.tsx","./src/app/login/__tests__/page.test.tsx","./node_modules/file-selector/dist/file.d.ts","./node_modules/file-selector/dist/file-selector.d.ts","./node_modules/file-selector/dist/index.d.ts","./node_modules/react-dropzone/typings/react-dropzone.d.ts","./src/components/profile/avatar-dropzone.tsx","./src/components/profile/avatar-preview.tsx","./src/components/profile/profile-form.tsx","./src/components/profile/profile-settings-panel.tsx","./src/app/profile/page.tsx","./src/app/projects/[id]/members/page.tsx","./src/app/projects/[id]/members/invite/page.tsx","./src/components/__tests__/app-shell.test.tsx","./src/components/__tests__/query-provider.test.tsx","./src/components/__tests__/skill-action-bar.test.tsx","./src/components/__tests__/status-badge.test.tsx","./src/components/__tests__/user-menu.test.tsx","./src/components/auth/__tests__/login-form.test.tsx","./src/components/auth/__tests__/social-auth-buttons.test.tsx","./src/components/profile/__tests__/avatar-dropzone.test.tsx","./src/lib/__tests__/useRole.test.tsx","./.next/types/cache-life.d.ts","./.next/types/routes.d.ts","./.next/types/validator.ts","./.next/dev/types/cache-life.d.ts","./.next/dev/types/validator.ts","./node_modules/vitest/globals.d.ts"],"fileIdsList":[[101,164,172,176,179,181,182,183,195,512,513,514,515,861],[101,164,172,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,255,553,556,559,747,750,752,753,756,758,760,762,764,766,767,769,771,772,775,776,778,780,782,784,786,789,791,793,794,814,821,823,825,827,830,832,833,836,839,849,850,851,861,864],[101,164,172,176,179,181,182,183,195,512,513,514,515,864],[101,164,172,176,179,181,182,183,195,255,553,556,747,750,752,753,756,758,767,769,771,772,782,784,789,791,793,794,814,821,823,827,830,832,833,839,861,862,864],[101,164,172,176,179,181,182,183,195,557,558,559,861,864],[101,164,172,176,179,181,182,183,195,255,557,861,864],[101,164,172,176,179,181,182,183,195,800,861,864],[92,101,164,172,176,179,181,182,183,195,255,801,861,864],[101,164,172,176,179,181,182,183,195,802,861,864],[101,164,172,176,179,181,182,183,195,669,861,864],[101,164,172,176,179,181,182,183,195,666,667,668,669,670,673,674,675,676,677,678,679,680,861,864],[101,164,172,176,179,181,182,183,195,662,861,864],[101,164,172,176,179,181,182,183,195,672,861,864],[101,164,172,176,179,181,182,183,195,666,667,668,861,864],[101,164,172,176,179,181,182,183,195,666,667,861,864],[101,164,172,176,179,181,182,183,195,669,670,672,861,864],[101,164,172,176,179,181,182,183,195,667,861,864],[101,164,172,176,179,181,182,183,195,664,861,864],[101,164,172,176,179,181,182,183,195,663,861,864],[92,101,164,172,176,179,181,182,183,195,225,488,681,682,861,864],[101,164,172,176,179,181,182,183,195,624,625,861,864],[101,161,162,164,172,176,179,181,182,183,195,861,864],[101,163,164,172,176,179,181,182,183,195,861,864],[164,172,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,203,861,864],[101,164,165,170,172,175,176,179,181,182,183,185,195,200,212,861,864],[101,164,165,166,172,175,176,179,181,182,183,195,861,864],[101,164,167,172,176,179,181,182,183,195,213,861,864],[101,164,168,169,172,176,179,181,182,183,186,195,861,864],[101,164,169,172,176,179,181,182,183,195,200,209,861,864],[101,164,170,172,175,176,179,181,182,183,185,195,861,864],[101,163,164,171,172,176,179,181,182,183,195,861,864],[101,164,172,173,176,179,181,182,183,195,861,864],[101,164,172,174,175,176,179,181,182,183,195,861,864],[101,163,164,172,175,176,179,181,182,183,195,861,864],[101,164,172,175,176,177,179,181,182,183,195,200,212,861,864],[101,164,172,175,176,177,179,181,182,183,195,200,203,861,864],[101,151,164,172,175,176,178,179,181,182,183,185,195,200,212,861,864],[101,164,172,175,176,178,179,181,182,183,185,195,200,209,212,861,864],[101,164,172,176,178,179,180,181,182,183,195,200,209,212,861,864],[99,100,101,102,103,104,105,106,107,108,109,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,861,864],[101,164,172,175,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,183,195,861,864],[101,164,172,176,179,181,182,183,184,195,212,861,864],[101,164,172,175,176,179,181,182,183,185,195,200,861,864],[101,164,172,176,179,181,182,183,186,195,861,864],[101,164,172,176,179,181,182,183,187,195,861,864],[101,164,172,175,176,179,181,182,183,190,195,861,864],[101,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,861,864],[101,164,172,176,179,181,182,183,192,195,861,864],[101,164,172,176,179,181,182,183,193,195,861,864],[101,164,169,172,176,179,181,182,183,185,195,203,861,864],[101,164,172,175,176,179,181,182,183,195,196,861,864],[101,164,172,176,179,181,182,183,195,197,213,216,861,864],[101,164,172,175,176,179,181,182,183,195,200,202,203,861,864],[101,164,172,176,179,181,182,183,195,201,203,861,864],[101,164,172,176,179,181,182,183,195,203,213,861,864],[101,164,172,176,179,181,182,183,195,204,861,864],[101,161,164,172,176,179,181,182,183,195,200,206,212,861,864],[101,164,172,176,179,181,182,183,195,200,205,861,864],[101,164,172,175,176,179,181,182,183,195,207,208,861,864],[101,164,172,176,179,181,182,183,195,207,208,861,864],[101,164,169,172,176,179,181,182,183,185,195,200,209,861,864],[101,164,172,176,179,181,182,183,195,210,861,864],[101,164,172,176,179,181,182,183,185,195,211,861,864],[101,164,172,176,178,179,181,182,183,193,195,212,861,864],[101,164,172,176,179,181,182,183,195,213,214,861,864],[101,164,169,172,176,179,181,182,183,195,214,861,864],[101,164,172,176,179,181,182,183,195,200,215,861,864],[101,164,172,176,179,181,182,183,184,195,216,861,864],[101,164,172,176,179,181,182,183,195,217,861,864],[101,164,167,172,176,179,181,182,183,195,861,864],[101,164,169,172,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,213,861,864],[101,151,164,172,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,212,861,864],[101,164,172,176,179,181,182,183,195,218,861,864],[101,164,172,176,179,181,182,183,190,195,861,864],[101,164,172,176,179,181,182,183,195,208,861,864],[101,151,164,172,175,176,177,179,181,182,183,190,195,200,203,212,215,216,218,861,864],[101,164,172,176,179,181,182,183,195,200,219,861,864],[92,96,101,164,172,176,179,181,182,183,195,221,222,223,225,507,552,861,864],[92,101,164,172,176,179,181,182,183,195,861,864],[92,96,101,164,172,176,179,181,182,183,195,221,222,223,224,488,507,552,861,864],[92,96,101,164,172,176,179,181,182,183,195,221,222,224,225,507,552,861,864],[92,101,164,172,176,179,181,182,183,195,225,488,489,861,864],[92,101,164,172,176,179,181,182,183,195,225,488,861,864],[92,96,101,164,172,176,179,181,182,183,195,222,223,224,225,507,552,861,864],[92,96,101,164,172,176,179,181,182,183,195,221,223,224,225,507,552,861,864],[90,91,101,164,172,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,613,614,659,861,864],[101,164,172,176,179,181,182,183,195,617,618,619,622,623,626,861,864],[101,164,172,176,179,181,182,183,195,647,861,864],[101,164,172,176,179,181,182,183,195,647,648,861,864],[101,164,172,176,179,181,182,183,195,622,631,632,861,864],[101,164,172,176,179,181,182,183,195,622,631,861,864],[101,164,172,176,179,181,182,183,195,631,861,864],[101,164,172,176,179,181,182,183,195,620,631,635,636,861,864],[101,164,172,176,179,181,182,183,195,620,631,635,861,864],[101,164,172,176,179,181,182,183,195,616,861,864],[101,164,172,176,179,181,182,183,195,620,621,861,864],[101,164,172,176,179,181,182,183,195,620,861,864],[101,164,172,176,179,181,182,183,195,620,621,628,652,861,864],[101,164,172,176,179,181,182,183,195,628,861,864],[101,164,172,176,179,181,182,183,195,620,623,628,629,630,861,864],[101,164,172,176,179,181,182,183,195,689,690,861,864],[101,164,172,176,179,181,182,183,195,689,690,691,692,861,864],[101,164,172,176,179,181,182,183,195,689,691,861,864],[101,164,172,176,179,181,182,183,195,689,861,864],[101,164,172,176,179,181,182,183,195,841,861,864],[101,164,172,176,179,181,182,183,195,841,842,861,864],[101,164,172,176,179,181,182,183,195,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,861,864],[101,164,172,176,179,181,182,183,195,697,861,864],[101,164,172,176,179,181,182,183,195,697,707,861,864],[101,164,172,176,179,181,182,183,195,606,607,861,864],[101,164,172,176,179,181,182,183,195,743,861,864],[101,164,172,176,178,179,181,182,183,195,220,743,861,864],[101,164,172,176,179,181,182,183,195,734,741,861,864],[101,164,172,176,179,181,182,183,195,553,557,741,743,861,864],[101,164,172,176,179,181,182,183,195,696,730,737,739,740,861,864],[101,164,172,176,179,181,182,183,195,735,741,742,861,864],[101,164,172,176,179,181,182,183,195,553,557,738,743,861,864],[101,164,172,176,179,181,182,183,195,220,743,861,864],[101,164,172,176,179,181,182,183,195,735,737,743,861,864],[101,164,172,176,179,181,182,183,195,737,741,743,861,864],[101,164,172,176,179,181,182,183,195,737,861,864],[101,164,172,176,179,181,182,183,195,732,733,736,861,864],[101,164,172,176,179,181,182,183,195,729,730,731,737,743,861,864],[92,101,164,172,176,179,181,182,183,195,737,743,815,816,861,864],[92,101,164,172,176,179,181,182,183,195,737,743,861,864],[101,164,172,176,179,181,182,183,195,510,861,864],[101,164,172,176,179,181,182,183,195,512,513,514,515,861,864],[101,164,172,176,179,181,182,183,195,458,521,522,861,864],[101,164,172,176,179,181,182,183,195,230,231,233,245,269,384,395,503,861,864],[101,164,172,176,179,181,182,183,195,233,264,265,266,268,503,861,864],[101,164,172,176,179,181,182,183,195,233,401,403,405,406,408,503,505,861,864],[101,164,172,176,179,181,182,183,195,233,267,304,503,861,864],[101,164,172,176,179,181,182,183,195,231,233,244,245,251,257,262,383,384,385,394,503,505,861,864],[101,164,172,176,179,181,182,183,195,503,861,864],[101,164,172,176,179,181,182,183,195,240,246,265,285,380,861,864],[101,164,172,176,179,181,182,183,195,233,861,864],[101,164,172,176,179,181,182,183,195,226,240,246,861,864],[101,164,172,176,179,181,182,183,195,412,861,864],[101,164,172,176,179,181,182,183,195,409,410,412,861,864],[101,164,172,176,179,181,182,183,195,409,411,503,861,864],[101,164,172,176,178,179,181,182,183,195,285,482,500,861,864],[101,164,172,176,178,179,181,182,183,195,356,359,375,380,500,861,864],[101,164,172,176,178,179,181,182,183,195,328,500,861,864],[101,164,172,176,179,181,182,183,195,388,861,864],[101,164,172,176,179,181,182,183,195,387,388,389,861,864],[101,164,172,176,179,181,182,183,195,387,861,864],[98,101,164,172,176,178,179,181,182,183,195,226,233,245,251,257,263,265,269,270,283,284,351,381,382,395,503,507,861,864],[101,164,172,176,179,181,182,183,195,230,233,267,304,401,402,407,503,555,861,864],[101,164,172,176,179,181,182,183,195,267,555,861,864],[101,164,172,176,179,181,182,183,195,230,284,453,503,555,861,864],[101,164,172,176,179,181,182,183,195,555,861,864],[101,164,172,176,179,181,182,183,195,233,267,268,555,861,864],[101,164,172,176,179,181,182,183,195,404,555,861,864],[101,164,172,176,179,181,182,183,195,270,383,386,393,861,864],[92,101,164,172,176,179,181,182,183,195,458,861,864],[101,164,172,176,179,181,182,183,193,195,240,255,861,864],[101,164,172,176,179,181,182,183,195,240,255,861,864],[92,101,164,172,176,179,181,182,183,195,325,861,864],[92,101,164,172,176,179,181,182,183,195,255,861,864],[92,101,164,172,176,179,181,182,183,195,246,255,458,861,864],[101,164,172,176,179,181,182,183,195,240,311,325,326,537,544,861,864],[101,164,172,176,179,181,182,183,195,310,538,539,540,541,543,861,864],[101,164,172,176,179,181,182,183,195,361,861,864],[101,164,172,176,179,181,182,183,195,361,362,861,864],[101,164,172,176,179,181,182,183,195,244,246,313,314,861,864],[101,164,172,176,179,181,182,183,195,246,320,321,861,864],[101,164,172,176,179,181,182,183,195,246,315,323,861,864],[101,164,172,176,179,181,182,183,195,320,861,864],[101,164,172,176,179,181,182,183,195,238,246,313,314,315,316,317,318,319,320,323,861,864],[101,164,172,176,179,181,182,183,195,246,313,320,321,322,324,861,864],[101,164,172,176,179,181,182,183,195,246,314,316,317,861,864],[101,164,172,176,179,181,182,183,195,314,316,319,321,861,864],[101,164,172,176,179,181,182,183,195,542,861,864],[101,164,172,176,179,181,182,183,195,246,861,864],[92,101,164,172,176,179,181,182,183,195,234,531,861,864],[92,101,164,172,176,179,181,182,183,195,212,861,864],[92,101,164,172,176,179,181,182,183,195,267,302,861,864],[92,101,164,172,176,179,181,182,183,195,267,395,861,864],[101,164,172,176,179,181,182,183,195,300,305,861,864],[92,101,164,172,176,179,181,182,183,195,301,509,861,864],[101,164,172,176,179,181,182,183,195,810,861,864],[92,96,101,164,172,176,178,179,181,182,183,195,221,222,223,224,225,507,551,861,864],[101,164,172,176,178,179,181,182,183,195,246,861,864],[101,164,172,176,178,179,181,182,183,195,245,250,331,348,390,391,395,450,452,503,504,861,864],[101,164,172,176,179,181,182,183,195,283,392,861,864],[101,164,172,176,179,181,182,183,195,507,861,864],[101,164,172,176,179,181,182,183,195,232,861,864],[92,101,164,172,176,179,181,182,183,195,237,240,455,471,473,861,864],[101,164,172,176,179,181,182,183,193,195,240,455,470,471,472,554,861,864],[101,164,172,176,179,181,182,183,195,464,465,466,467,468,469,861,864],[101,164,172,176,179,181,182,183,195,466,861,864],[101,164,172,176,179,181,182,183,195,470,861,864],[101,164,172,176,179,181,182,183,195,255,419,420,422,861,864],[92,101,164,172,176,179,181,182,183,195,246,413,414,415,416,421,861,864],[101,164,172,176,179,181,182,183,195,419,421,861,864],[101,164,172,176,179,181,182,183,195,417,861,864],[101,164,172,176,179,181,182,183,195,418,861,864],[92,101,164,172,176,179,181,182,183,195,255,301,509,861,864],[92,101,164,172,176,179,181,182,183,195,255,508,509,861,864],[92,101,164,172,176,179,181,182,183,195,255,509,861,864],[101,164,172,176,179,181,182,183,195,348,349,861,864],[101,164,172,176,179,181,182,183,195,349,861,864],[101,164,172,176,178,179,181,182,183,195,504,509,861,864],[101,164,172,176,179,181,182,183,195,378,861,864],[101,163,164,172,176,179,181,182,183,195,377,861,864],[101,164,172,176,179,181,182,183,195,240,246,252,254,356,369,373,375,452,455,492,493,500,504,861,864],[101,164,172,176,179,181,182,183,195,246,295,317,861,864],[101,164,172,176,179,181,182,183,195,356,367,370,375,861,864],[92,101,164,172,176,179,181,182,183,195,237,240,356,359,375,378,412,459,460,461,462,463,474,475,476,477,478,479,480,481,555,861,864],[101,164,172,176,179,181,182,183,195,237,240,265,356,363,364,365,368,369,861,864],[101,164,172,176,179,181,182,183,195,200,246,265,367,374,455,456,500,861,864],[101,164,172,176,179,181,182,183,195,371,861,864],[101,164,172,176,178,179,181,182,183,193,195,234,246,250,260,292,293,296,348,351,416,450,451,492,503,504,505,507,555,861,864],[101,164,172,176,179,181,182,183,195,237,238,240,861,864],[101,164,172,176,179,181,182,183,195,356,861,864],[101,163,164,172,176,179,181,182,183,195,265,292,293,350,351,352,353,354,355,504,861,864],[101,164,172,176,179,181,182,183,195,375,861,864],[101,163,164,172,176,179,181,182,183,195,239,240,250,254,290,356,363,364,365,366,367,370,371,372,373,374,493,861,864],[101,164,172,176,178,179,181,182,183,195,290,291,363,504,505,861,864],[101,164,172,176,179,181,182,183,195,265,293,348,351,356,452,504,861,864],[101,164,172,176,178,179,181,182,183,195,503,505,861,864],[101,164,172,176,178,179,181,182,183,195,200,500,504,505,861,864],[101,164,172,176,178,179,181,182,183,193,195,226,240,245,252,254,257,260,267,287,292,293,294,295,296,331,332,334,337,339,342,343,344,345,347,395,450,452,500,503,504,505,861,864],[101,164,172,176,178,179,181,182,183,195,200,861,864],[101,164,172,176,179,181,182,183,195,233,234,235,263,500,501,502,507,509,555,861,864],[101,164,172,176,179,181,182,183,195,230,231,503,861,864],[101,164,172,176,179,181,182,183,195,424,861,864],[101,164,172,176,178,179,181,182,183,195,200,212,242,408,412,413,414,415,416,422,423,555,861,864],[101,164,172,176,179,181,182,183,193,195,212,226,240,242,254,257,293,332,337,347,348,401,428,429,430,436,439,440,450,452,500,503,861,864],[101,164,172,176,179,181,182,183,195,257,263,270,283,293,351,503,861,864],[101,164,172,176,178,179,181,182,183,195,212,234,245,254,293,434,500,503,861,864],[101,164,172,176,179,181,182,183,195,454,861,864],[101,164,172,176,178,179,181,182,183,195,424,437,438,447,861,864],[101,164,172,176,179,181,182,183,195,500,503,861,864],[101,164,172,176,179,181,182,183,195,353,493,861,864],[101,164,172,176,179,181,182,183,195,254,292,395,509,861,864],[101,164,172,176,178,179,181,182,183,193,195,232,337,397,401,430,436,439,442,500,861,864],[101,164,172,176,178,179,181,182,183,195,270,283,401,443,861,864],[101,164,172,176,179,181,182,183,195,233,294,395,445,503,505,861,864],[101,164,172,176,178,179,181,182,183,195,212,416,503,861,864],[101,164,172,176,178,179,181,182,183,195,267,294,395,396,397,406,424,444,446,503,861,864],[98,101,164,172,176,178,179,181,182,183,195,292,449,507,509,861,864],[101,164,172,176,179,181,182,183,195,346,450,861,864],[101,164,172,176,178,179,181,182,183,193,195,240,243,245,246,252,254,260,269,270,283,293,296,332,334,344,347,348,395,428,429,430,431,433,435,450,452,500,509,861,864],[101,164,172,176,178,179,181,182,183,195,200,270,436,441,447,500,861,864],[101,164,172,176,179,181,182,183,195,273,274,275,276,277,278,279,280,281,282,861,864],[101,164,172,176,179,181,182,183,195,287,338,861,864],[101,164,172,176,179,181,182,183,195,340,861,864],[101,164,172,176,179,181,182,183,195,338,861,864],[101,164,172,176,179,181,182,183,195,340,341,861,864],[101,164,172,176,178,179,181,182,183,195,244,245,246,250,251,504,861,864],[101,164,172,176,178,179,181,182,183,193,195,232,234,252,256,292,295,296,330,450,500,505,507,509,861,864],[101,164,172,176,178,179,181,182,183,193,195,212,236,243,244,254,256,293,448,493,499,504,861,864],[101,164,172,176,179,181,182,183,195,363,861,864],[101,164,172,176,179,181,182,183,195,364,861,864],[101,164,172,176,179,181,182,183,195,246,257,492,861,864],[101,164,172,176,179,181,182,183,195,365,861,864],[101,164,172,176,179,181,182,183,195,239,861,864],[101,164,172,176,179,181,182,183,195,241,253,861,864],[101,164,172,176,178,179,181,182,183,195,241,245,252,861,864],[101,164,172,176,179,181,182,183,195,248,253,861,864],[101,164,172,176,179,181,182,183,195,249,861,864],[101,164,172,176,179,181,182,183,195,241,242,861,864],[101,164,172,176,179,181,182,183,195,241,297,861,864],[101,164,172,176,179,181,182,183,195,241,861,864],[101,164,172,176,179,181,182,183,195,243,287,336,861,864],[101,164,172,176,179,181,182,183,195,335,861,864],[101,164,172,176,179,181,182,183,195,240,242,243,861,864],[101,164,172,176,179,181,182,183,195,243,333,861,864],[101,164,172,176,179,181,182,183,195,240,242,861,864],[101,164,172,176,179,181,182,183,195,292,395,861,864],[101,164,172,176,179,181,182,183,195,492,861,864],[101,164,172,176,178,179,181,182,183,195,212,252,254,258,292,395,449,452,455,456,457,483,484,487,491,493,500,504,861,864],[101,164,172,176,179,181,182,183,195,306,309,311,312,325,326,861,864],[92,101,164,172,176,179,181,182,183,195,223,225,255,485,486,861,864],[92,101,164,172,176,179,181,182,183,195,223,225,255,485,486,490,861,864],[101,164,172,176,179,181,182,183,195,379,861,864],[101,164,172,176,179,181,182,183,195,265,286,291,292,356,357,358,359,360,362,375,376,378,381,449,452,503,505,861,864],[101,164,172,176,179,181,182,183,195,325,861,864],[101,164,172,176,178,179,181,182,183,195,330,500,861,864],[101,164,172,176,179,181,182,183,195,330,861,864],[101,164,172,176,178,179,181,182,183,195,252,298,327,329,331,449,500,507,509,861,864],[101,164,172,176,179,181,182,183,195,306,307,308,309,311,312,325,326,508,861,864],[98,101,164,172,176,178,179,181,182,183,193,195,212,241,242,254,260,292,293,296,395,447,448,450,500,503,504,507,861,864],[101,164,172,176,179,181,182,183,195,237,240,247,861,864],[101,164,172,176,179,181,182,183,195,291,293,425,428,861,864],[101,164,172,176,179,181,182,183,195,291,426,494,495,496,497,498,861,864],[101,164,172,176,178,179,181,182,183,195,287,503,861,864],[101,164,172,176,178,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,290,375,861,864],[101,164,172,176,179,181,182,183,195,289,861,864],[101,164,172,176,179,181,182,183,195,291,344,861,864],[101,164,172,176,179,181,182,183,195,288,290,503,861,864],[101,164,172,176,178,179,181,182,183,195,236,291,425,426,427,500,503,504,861,864],[92,101,164,172,176,179,181,182,183,195,240,246,324,861,864],[92,101,164,172,176,179,181,182,183,195,238,861,864],[101,164,172,176,179,181,182,183,195,228,229,861,864],[92,101,164,172,176,179,181,182,183,195,234,861,864],[92,101,164,172,176,179,181,182,183,195,240,310,861,864],[92,98,101,164,172,176,179,181,182,183,195,292,296,507,509,861,864],[101,164,172,176,179,181,182,183,195,234,531,532,861,864],[92,101,164,172,176,179,181,182,183,195,305,861,864],[92,101,164,172,176,179,181,182,183,193,195,212,232,299,301,303,304,509,861,864],[101,164,172,176,179,181,182,183,195,240,267,504,861,864],[101,164,172,176,179,181,182,183,195,240,432,861,864],[92,101,164,172,176,178,179,181,182,183,193,195,230,232,305,403,507,508,861,864],[92,101,164,172,176,179,181,182,183,195,221,222,223,224,225,507,552,861,864],[92,93,94,95,96,101,164,172,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,398,399,400,861,864],[101,164,172,176,179,181,182,183,195,398,861,864],[92,96,101,164,172,176,178,179,180,181,182,183,193,195,220,221,222,223,224,225,226,232,260,265,442,470,505,506,509,552,861,864],[101,164,172,176,179,181,182,183,195,517,861,864],[101,164,172,176,179,181,182,183,195,519,861,864],[101,164,172,176,179,181,182,183,195,523,861,864],[101,164,172,176,179,181,182,183,195,811,861,864],[101,164,172,176,179,181,182,183,195,525,861,864],[101,164,172,176,179,181,182,183,195,527,528,529,861,864],[101,164,172,176,179,181,182,183,195,533,861,864],[97,101,164,172,176,179,181,182,183,195,511,516,518,520,524,526,530,534,536,546,547,549,553,554,555,556,861,864],[101,164,172,176,179,181,182,183,195,535,861,864],[101,164,172,176,179,181,182,183,195,545,861,864],[101,164,172,176,179,181,182,183,195,301,861,864],[101,164,172,176,179,181,182,183,195,548,861,864],[101,163,164,172,176,179,181,182,183,195,291,425,426,428,494,495,497,498,550,552,861,864],[101,164,172,176,179,181,182,183,195,220,861,864],[101,164,169,172,176,178,179,180,181,182,183,195,212,213,220,729,861,864],[101,164,172,176,179,181,182,183,195,601,861,864],[101,164,172,176,179,181,182,183,195,599,601,861,864],[101,164,172,176,179,181,182,183,195,590,598,599,600,602,604,861,864],[101,164,172,176,179,181,182,183,195,588,861,864],[101,164,172,176,179,181,182,183,195,591,596,601,604,861,864],[101,164,172,176,179,181,182,183,195,587,604,861,864],[101,164,172,176,179,181,182,183,195,591,592,595,596,597,604,861,864],[101,164,172,176,179,181,182,183,195,591,592,593,595,596,604,861,864],[101,164,172,176,179,181,182,183,195,588,589,590,591,592,596,597,598,600,601,602,604,861,864],[101,164,172,176,179,181,182,183,195,604,861,864],[101,164,172,176,179,181,182,183,195,586,588,589,590,591,592,593,595,596,597,598,599,600,601,602,603,861,864],[101,164,172,176,179,181,182,183,195,586,604,861,864],[101,164,172,176,179,181,182,183,195,591,593,594,596,597,604,861,864],[101,164,172,176,179,181,182,183,195,595,604,861,864],[101,164,172,176,179,181,182,183,195,596,597,601,604,861,864],[101,164,172,176,179,181,182,183,195,589,599,861,864],[101,164,172,176,179,181,182,183,195,671,861,864],[92,101,164,172,176,179,181,182,183,195,843,861,864],[101,164,172,176,179,181,182,183,195,573,861,864],[101,164,172,176,179,181,182,183,195,565,567,573,861,864],[101,164,172,176,179,181,182,183,195,566,567,861,864],[101,164,172,176,179,181,182,183,195,567,573,577,861,864],[101,164,172,176,179,181,182,183,195,566,861,864],[101,164,172,176,179,181,182,183,195,567,573,861,864],[101,164,172,176,179,181,182,183,195,565,566,567,572,861,864],[101,164,172,176,179,181,182,183,195,565,567,861,864],[101,164,172,176,179,181,182,183,195,566,567,579,861,864],[101,164,172,176,179,181,182,183,195,568,569,570,861,864],[101,164,172,176,179,181,182,183,195,571,861,864],[101,164,172,176,179,181,182,183,195,200,220,861,864],[101,116,119,122,123,164,172,176,179,181,182,183,195,212,861,864],[101,119,164,172,176,179,181,182,183,195,200,212,861,864],[101,119,123,164,172,176,179,181,182,183,195,212,861,864],[101,164,172,176,179,181,182,183,195,200,861,864],[101,113,164,172,176,179,181,182,183,195,861,864],[101,117,164,172,176,179,181,182,183,195,861,864],[101,115,116,119,164,172,176,179,181,182,183,195,212,861,864],[101,164,172,176,179,181,182,183,185,195,209,861,864],[101,113,164,172,176,179,181,182,183,195,220,861,864],[101,115,119,164,172,176,179,181,182,183,185,195,212,861,864],[101,110,111,112,114,118,164,172,175,176,179,181,182,183,195,200,212,861,864],[101,119,128,136,164,172,176,179,181,182,183,195,861,864],[101,111,117,164,172,176,179,181,182,183,195,861,864],[101,119,145,146,164,172,176,179,181,182,183,195,861,864],[101,111,114,119,164,172,176,179,181,182,183,195,203,212,220,861,864],[101,119,164,172,176,179,181,182,183,195,861,864],[101,115,119,164,172,176,179,181,182,183,195,212,861,864],[101,110,164,172,176,179,181,182,183,195,861,864],[101,113,114,115,117,118,119,120,121,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,146,147,148,149,150,164,172,176,179,181,182,183,195,861,864],[101,119,138,141,164,172,176,179,181,182,183,195,861,864],[101,119,128,129,130,164,172,176,179,181,182,183,195,861,864],[101,117,119,129,131,164,172,176,179,181,182,183,195,861,864],[101,118,164,172,176,179,181,182,183,195,861,864],[101,111,113,119,164,172,176,179,181,182,183,195,861,864],[101,119,123,129,131,164,172,176,179,181,182,183,195,861,864],[101,123,164,172,176,179,181,182,183,195,861,864],[101,117,119,122,164,172,176,179,181,182,183,195,212,861,864],[101,111,115,119,128,164,172,176,179,181,182,183,195,861,864],[101,119,138,164,172,176,179,181,182,183,195,861,864],[101,131,164,172,176,179,181,182,183,195,861,864],[101,113,119,145,164,172,176,179,181,182,183,195,203,218,220,861,864],[101,164,172,176,179,181,182,183,195,562,861,864],[101,164,172,175,176,178,179,180,181,182,183,185,195,200,209,212,219,220,562,563,564,574,575,576,578,580,582,583,584,585,605,609,610,611,612,613,861,864],[101,164,172,176,179,181,182,183,195,562,563,564,581,861,864],[101,164,172,176,179,181,182,183,195,564,861,864],[101,164,172,176,179,181,182,183,195,608,861,864],[101,164,172,176,179,181,182,183,195,574,584,613,861,864],[101,164,172,176,179,181,182,183,195,574,613,861,864],[101,164,172,176,179,181,182,183,195,654,861,864],[101,164,172,176,179,181,182,183,195,627,659,684,861,864],[101,164,172,176,179,181,182,183,195,617,620,622,623,629,630,631,633,634,637,638,650,651,653,684,861,864],[101,164,172,176,179,181,182,183,195,633,644,645,684,861,864],[101,164,172,176,179,181,182,183,195,633,634,641,684,861,864],[101,164,172,176,179,181,182,183,195,620,622,633,634,637,684,861,864],[101,164,172,176,179,181,182,183,195,582,861,864],[101,164,172,176,179,181,182,183,195,620,627,633,634,637,646,684,861,864],[101,164,172,176,179,181,182,183,195,613,657,659,861,864],[101,164,167,172,176,179,181,182,183,195,200,613,620,622,627,631,633,634,637,638,641,642,643,646,649,650,651,655,656,659,684,861,864],[101,164,172,176,179,181,182,183,195,582,633,634,637,684,861,864],[101,164,172,176,179,181,182,183,195,633,644,645,646,684,861,864],[101,164,172,176,179,181,182,183,195,582,633,638,639,640,684,861,864],[101,164,167,172,176,179,181,182,183,195,200,582,613,620,622,627,631,633,634,637,638,639,640,641,642,643,644,645,646,649,650,651,655,656,657,658,659,684,861,864],[101,164,172,176,179,181,182,183,195,582,617,620,622,627,631,633,634,637,638,639,640,641,642,644,645,646,649,684,685,686,687,688,693,861,864],[101,164,172,176,179,181,182,183,195,620,622,633,634,637,638,644,645,646,684,686,861,864],[101,164,172,176,179,181,182,183,195,694,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,748,797,821,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,748,797,823,861,864],[101,164,172,176,179,181,182,183,195,255,536,748,749,797,819,820,861,864],[101,164,172,176,179,181,182,183,195,255,749,797,819,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,748,797,827,861,864],[101,164,172,176,179,181,182,183,195,255,748,749,797,819,820,826,861,864],[101,164,172,176,179,181,182,183,195,255,746,861,864],[101,164,172,176,179,181,182,183,195,255,694,748,749,750,861,864],[101,164,172,176,179,181,182,183,195,255,553,749,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,753,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,752,861,864],[101,164,172,176,179,181,182,183,195,255,694,748,749,756,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,758,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,760,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,762,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,764,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,767,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,772,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,771,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,769,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,775,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,778,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,780,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,782,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,784,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,786,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,766,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,789,861,864],[101,164,172,176,179,181,182,183,195,255,694,748,749,791,861,864],[101,164,172,176,179,181,182,183,195,255,553,748,749,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,794,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,793,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,748,797,830,861,864],[101,164,172,176,179,181,182,183,195,255,536,749,797,819,820,829,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,748,797,833,861,864],[101,164,172,176,179,181,182,183,195,255,536,749,797,819,820,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,748,797,832,861,864],[101,164,172,176,179,181,182,183,195,255,536,743,746,749,797,861,864],[92,101,164,172,176,179,181,182,183,195,255,554,557,812,813,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,839,861,864],[101,164,172,176,179,181,182,183,195,255,838,861,864],[101,164,172,176,179,181,182,183,195,255,536,749,797,798,819,820,861,864],[101,164,172,176,179,181,182,183,195,255,749,797,819,848,861,864],[101,164,172,176,179,181,182,183,195,255,536,749,797,819,861,864],[101,164,172,176,179,181,182,183,195,255,536,749,797,819,826,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,748,819,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,803,813,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,813,829,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,820,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,813,818,861,864],[92,101,164,172,176,179,181,182,183,195,255,536,748,818,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,838,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,837,861,864],[92,101,164,172,176,179,181,182,183,195,255,536,546,817,837,861,864],[101,164,172,176,179,181,182,183,195,255,817,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,845,861,864],[92,101,164,172,176,179,181,182,183,195,255,844,861,864],[101,164,172,176,179,181,182,183,195,255,748,861,864],[92,101,164,172,176,179,181,182,183,195,255,748,861,864],[92,101,164,172,176,179,181,182,183,195,255,748,799,803,845,846,847,861,864],[92,101,164,172,176,179,181,182,183,195,255,803,861,864],[92,101,164,172,176,179,181,182,183,195,255,546,748,803,861,864],[92,101,164,172,176,179,181,182,183,195,255,536,799,803,817,861,864],[92,101,164,172,176,179,181,182,183,195,255,694,749,861,864],[101,164,172,176,179,181,182,183,195,255,694,746,861,864],[101,164,172,176,179,181,182,183,195,255,694,748,798,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,748,803,804,861,864],[101,164,172,176,179,181,182,183,195,255,694,748,797,861,864],[92,101,164,172,176,179,181,182,183,195,255,554,743,746,748,861,864],[101,164,172,176,179,181,182,183,195,255,554,736,743,744,745,861,864],[101,164,172,176,179,181,182,183,195,255,748,797,861,864],[101,164,172,176,179,181,182,183,195,255,861,864],[101,164,172,176,179,181,182,183,195,255,748,803,861,864],[101,164,172,176,179,181,182,183,195,212,255,615,660,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,861,864]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"1e9332c23e9a907175e0ffc6a49e236f97b48838cc8aec9ce7e4cec21e544b65","impliedFormat":1},{"version":"3753fbc1113dc511214802a2342280a8b284ab9094f6420e7aa171e868679f91","impliedFormat":1},{"version":"999ca32883495a866aa5737fe1babc764a469e4cde6ee6b136a4b9ae68853e4b","impliedFormat":1},{"version":"17f13ecb98cbc39243f2eee1f16d45cd8ec4706b03ee314f1915f1a8b42f6984","impliedFormat":1},{"version":"d6b1eba8496bdd0eed6fc8a685768fe01b2da4a0388b5fe7df558290bffcf32f","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f57fc4404ff020bc45b9c620aff2b40f700b95fe31164024c453a5e3c163c54","impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"0bd714129fca875f7d4c477a1a392200b0bcd13fb2e80928cd334b63830ea047","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2c9037ae6cd2c52d80ceef0b3c5ffdb488627d71529cf4f63776daf11161c9a","affectsGlobalScope":true,"impliedFormat":1},{"version":"135d5cf4d345f59f1a9caadfafcd858d3d9cc68290db616cc85797224448cccc","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc238c3f81c2984751932b6aab223cd5b830e0ac6cad76389e5e9d2ffc03287d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4a07f9b76d361f572620927e5735b77d6d2101c23cdd94383eb5b706e7b36357","affectsGlobalScope":true,"impliedFormat":1},{"version":"7c4e8dc6ab834cc6baa0227e030606d29e3e8449a9f67cdf5605ea5493c4db29","affectsGlobalScope":true,"impliedFormat":1},{"version":"de7ba0fd02e06cd9a5bd4ab441ed0e122735786e67dde1e849cced1cd8b46b78","affectsGlobalScope":true,"impliedFormat":1},{"version":"6148e4e88d720a06855071c3db02069434142a8332cf9c182cda551adedf3156","affectsGlobalScope":true,"impliedFormat":1},{"version":"d63dba625b108316a40c95a4425f8d4294e0deeccfd6c7e59d819efa19e23409","affectsGlobalScope":true,"impliedFormat":1},{"version":"0568d6befee03dd435bed4fc25c4e46865b24bdcb8c563fdc21f580a2c301904","affectsGlobalScope":true,"impliedFormat":1},{"version":"30d62269b05b584741f19a5369852d5d34895aa2ac4fd948956f886d15f9cc0d","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"ffbe6d7b295306b2ba88030f65b74c107d8d99bdcf596ea99c62a02f606108b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"996fb27b15277369c68a4ba46ed138b4e9e839a02fb4ec756f7997629242fd9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"79b712591b270d4778c89706ca2cfc56ddb8c3f895840e477388f1710dc5eda9","affectsGlobalScope":true,"impliedFormat":1},{"version":"20884846cef428b992b9bd032e70a4ef88e349263f63aeddf04dda837a7dba26","affectsGlobalScope":true,"impliedFormat":1},{"version":"5fcab789c73a97cd43828ee3cc94a61264cf24d4c44472ce64ced0e0f148bdb2","affectsGlobalScope":true,"impliedFormat":1},{"version":"db59a81f070c1880ad645b2c0275022baa6a0c4f0acdc58d29d349c6efcf0903","affectsGlobalScope":true,"impliedFormat":1},{"version":"673294292640f5722b700e7d814e17aaf7d93f83a48a2c9b38f33cbc940ad8b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"d786b48f934cbca483b3c6d0a798cb43bbb4ada283e76fb22c28e53ae05b9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ecb8e347cb6b2a8927c09b86263663289418df375f5e68e11a0ae683776978f","affectsGlobalScope":true,"impliedFormat":1},{"version":"142efd4ce210576f777dc34df121777be89eda476942d6d6663b03dcb53be3ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"379bc41580c2d774f82e828c70308f24a005b490c25ba34d679d84bcf05c3d9d","affectsGlobalScope":true,"impliedFormat":1},{"version":"ed484fb2aa8a1a23d0277056ec3336e0a0b52f9b8d6a961f338a642faf43235d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ffedae1d1c2d53fdbca1c96d3c7dda544281f7d262f99b6880634f8fd8d9820","affectsGlobalScope":true,"impliedFormat":1},{"version":"83a730b125d477dd264df8ba479afab27a3dae7152b005c214ab94dc7ee44fd3","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"dc0a7f107690ee5cd8afc8dbf05c4df78085471ce16bdd9881642ec738bc81fe","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"21da358700a3893281ce0c517a7a30cbd46be020d9f0c3f2834d0a8ad1f5fc75","impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"0ccdaa19852d25ecd84eec365c3bfa16e7859cadecf6e9ca6d0dbbbee439743f","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc2110f7decca6bfb9392e30421cfa1436479e4a6756e8fec6cbc22625d4f881","affectsGlobalScope":true,"impliedFormat":1},{"version":"096116f8fedc1765d5bd6ef360c257b4a9048e5415054b3bf3c41b07f8951b0b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5e01375c9e124a83b52ee4b3244ed1a4d214a6cfb54ac73e164a823a4a7860a","affectsGlobalScope":true,"impliedFormat":1},{"version":"f90ae2bbce1505e67f2f6502392e318f5714bae82d2d969185c4a6cecc8af2fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b58e207b93a8f1c88bbf2a95ddc686ac83962b13830fe8ad3f404ffc7051fb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fefabcb2b06736a66d2904074d56268753654805e829989a46a0161cd8412c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"c18a99f01eb788d849ad032b31cafd49de0b19e083fe775370834c5675d7df8e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5247874c2a23b9a62d178ae84f2db6a1d54e6c9a2e7e057e178cc5eea13757fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"074de5b2fdead0165a2757e3aaef20f27a6347b1c36adea27d51456795b37682","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"4137ebf04166f3a325f056aa56101adc75e9dceb30404a1844eb8604d89770e2","impliedFormat":1},{"version":"ccab02f3920fc75c01174c47fcf67882a11daf16baf9e81701d0a94636e94556","impliedFormat":1},{"version":"3e11fce78ad8c0e1d1db4ba5f0652285509be3acdd519529bc8fcef85f7dafd9","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"1cc2a09e1a61a5222d4174ab358a9f9de5e906afe79dbf7363d871a7edda3955","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"b64d4d1c5f877f9c666e98e833f0205edb9384acc46e98a1fef344f64d6aba44","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"c9381908473a1c92cb8c516b184e75f4d226dad95c3a85a5af35f670064d9a2f","impliedFormat":1},{"version":"c3f5289820990ab66b70c7fb5b63cb674001009ff84b13de40619619a9c8175f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3275d55fac10b799c9546804126239baf020d220136163f763b55a74e50e750","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa68a0a3b7cb32c00e39ee3cd31f8f15b80cac97dce51b6ee7fc14a1e8deb30b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c36e755bced82df7fb6ce8169265d0a7bb046ab4e2cb6d0da0cb72b22033e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7a93de4ff8a63bafe62ba86b89af1df0ccb5e40bb85b0c67d6bbcfdcf96bf3d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"90e85f9bc549dfe2b5749b45fe734144e96cd5d04b38eae244028794e142a77e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0a5deeb610b2a50a6350bd23df6490036a1773a8a71d70f2f9549ab009e67ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"d2ae155afe8a01cc0ae612d99117cf8ef16692ba7c4366590156fdec1bcf2d8c","impliedFormat":1},{"version":"3f5e5d9be35913db9fea42a63f3df0b7e3c8703b97670a2125587b4dbbd56d7c","impliedFormat":1},{"version":"8caeb65fdc3bfe0d13f86f67324fcb2d858ed1c55f1f0cce892eb1acfb9f3239","impliedFormat":1},{"version":"57c23df0b5f7a8e26363a3849b0bc7763f6b241207157c8e40089d1df4116f35","affectsGlobalScope":true,"impliedFormat":1},{"version":"3b8bc0c17b54081b0878673989216229e575d67a10874e84566a21025a2461ee","impliedFormat":1},{"version":"5b0db5a58b73498792a29bfebc333438e61906fef75da898b410e24e52229e6f","impliedFormat":1},{"version":"dbe055b2b29a7bab2c1ca8f259436306adb43f469dca7e639a02cd3695d3f621","impliedFormat":1},{"version":"1678b04557dca52feab73cc67610918a7f5e25bfdba3e7fa081acd625d93106d","impliedFormat":1},{"version":"e3905f6902f0b69e5eefc230daa69fdd4ab707a973ec2d086d65af1b3ea47ef0","impliedFormat":1},{"version":"2ea729503db9793f2691162fec3dd1118cab62e96d025f8eeb376d43ec293395","impliedFormat":1},{"version":"9ec87fea42b92894b0f209931a880789d43c3397d09dd99c631ae40a2f7071d1","impliedFormat":1},{"version":"c68e88cdfadfb6c8ba5fc38e58a3a166b0beae77b1f05b7d921150a32a5ffb8d","impliedFormat":1},{"version":"2bc7aa4fba46df0bd495425a7c8201437a7d465f83854fac859df2d67f664df3","impliedFormat":1},{"version":"41d17e1ad9a002feb11c8cdd2777e5bbc0cdb1e3f595d237e4dded0b6949983b","impliedFormat":1},{"version":"07e4e61e946a9c15045539ecd5f5d2d02e7aab6fa82567826857e09cf0f37c2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c4714ccc29149efb8777a1da0b04b8d2258f5d13ddbf4cd3c3d361fb531ac86","impliedFormat":1},{"version":"3ff275f84f89f8a7c0543da838f9da9614201abc4ce74c533029825adfb4433d","impliedFormat":1},{"version":"0eb5d0cbf09de5d34542b977fd6a933bb2e0817bffe8e1a541b2f1ad1b9af1ff","impliedFormat":1},{"version":"f9713757bcdfa4d58b48c0fb249e752c94a3eee8bf4532b906094246ac49ef88","impliedFormat":1},{"version":"2c2bdaa1d8ead9f68628d6d9d250e46ee8e81aa4898b4769a36956ae15e060fe","impliedFormat":1},{"version":"c32c840c62d8bd7aeb3147aa6754cd2d922b990a6b6634530cb2ebdce5adc8e9","impliedFormat":1},{"version":"e1c1a0b4d1ead0de9eca52203aeb1f771f21e6238d6fcd15aa56ac2a02f1b7bf","impliedFormat":1},{"version":"82b91e4e42e6c41bc7fc1b6c2dc5eba6a2ba98375eb1f210e6ff6bba2d54177e","impliedFormat":1},{"version":"6fe28249ac0c7bc19a79aa9264baf00efbd080e868dbe1d3052033ad1c64f206","affectsGlobalScope":true,"impliedFormat":1},{"version":"cbed824fec91efefc7bbdcb8b43d1a531fdbebd0e2ef19481501ff365a93cb70","impliedFormat":1},{"version":"d0716593b3f2b0451bcf0c24cfa86dec2235c325c89f201934248b7c742715fc","impliedFormat":1},{"version":"ec501101c2a96133a6c695f934c8f6642149cc728571b29cbb7b770984c1088e","impliedFormat":1},{"version":"b214ebcf76c51b115453f69729ee8aa7b7f8eccdae2a922b568a45c2d7ff52f7","impliedFormat":1},{"version":"429c9cdfa7d126255779efd7e6d9057ced2d69c81859bbab32073bad52e9ba76","impliedFormat":1},{"version":"2991bca2cc0f0628a278df2a2ccdb8d6cbcb700f3761abbed62bba137d5b1790","impliedFormat":1},{"version":"ce8653341224f8b45ff46d2a06f2cacb96f841f768a886c9d8dd8ec0878b11bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"230763250f20449fa7b3c9273e1967adb0023dc890d4be1553faca658ee65971","impliedFormat":1},{"version":"c3e9078b60cb329d1221f5878e88cecfa3e74460550e605a58fcfb41a66029ff","impliedFormat":1},{"version":"a74edb3bab7394a9dbde529d60632be590def2f5f01024dbd85441587fbfbbe0","impliedFormat":1},{"version":"0ea59f7d3e51440baa64f429253759b106cfcbaf51e474cae606e02265b37cf8","impliedFormat":1},{"version":"bc18a1991ba681f03e13285fa1d7b99b03b67ee671b7bc936254467177543890","impliedFormat":1},{"version":"00049ccc87f3f37726db03c01ca68fe74fd9c0109b68c29eb9923ebec2c76b13","impliedFormat":1},{"version":"fa94bbf532b7af8f394b95fa310980d6e20bd2d4c871c6a6cb9f70f03750a44b","impliedFormat":1},{"version":"68d3f35108e2608b1f2f28b36d19d7055f31c4465cc5692cbd06c716a9fe7973","impliedFormat":1},{"version":"a6d543044570fbeed13a7f9925a868081cd2b14ef59cdd9da6ae76d41cab03d3","affectsGlobalScope":true,"impliedFormat":1},{"version":"7fa2214bb0d64701bc6f9ce8cde2fd2ff8c571e0b23065fa04a8a5a6beb91511","impliedFormat":1},{"version":"f1c93e046fb3d9b7f8249629f4b63dc068dd839b824dd0aa39a5e68476dc9420","impliedFormat":1},{"version":"eab2f3179607acb3d44b2db2a76dd7d621c5039b145dc160a1ee733963f9d2f5","impliedFormat":1},{"version":"841983e39bd4cbb463be385e92fda11057cab368bf27100a801c492f1d86cbaa","impliedFormat":1},{"version":"6f5383b3df1cdf4ff1aa7fb0850f77042b5786b5e65ec9a9b6be56ebfe4d9036","impliedFormat":1},{"version":"62fc21ed9ccbd83bd1166de277a4b5daaa8d15b5fa614c75610d20f3b73fba87","impliedFormat":1},{"version":"e4156ddb25aa0e3b5303d372f26957b36778f0f6bbd4326359269873295e3058","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc1b433a84cae05ddc5672d4823170af78606ad21ecef60dbc4570190cbf1357","impliedFormat":1},{"version":"9d3821bc75c59577e52643324cec92fc2145642e8d17cf7ee07a3181f21d985d","impliedFormat":1},{"version":"7f78cfb2b343838612c192cb251746e3a7c62ac7675726a47e130d9b213f6580","impliedFormat":1},{"version":"201db9cf1687fab1adf5282fcba861f382b32303dc4f67c89d59655e78a25461","impliedFormat":1},{"version":"c77fb31bc17fd241d3922a9f88c59e3361cdf76d1328ba9412fc6bf7310b638d","impliedFormat":1},{"version":"0a20eaf2e4b1e3c1e1f87f7bccb0c936375b23b022baeea750519b7c9bc6ce83","impliedFormat":1},{"version":"b484ec11ba00e3a2235562a41898d55372ccabe607986c6fa4f4aba72093749f","impliedFormat":1},{"version":"a16b91b27bd6b706c687c88cbc8a7d4ee98e5ed6043026d6b84bda923c0aed67","impliedFormat":1},{"version":"694b812e0ed11285e8822cf8131e3ce7083a500b3b1d185fff9ed1089677bd0a","impliedFormat":1},{"version":"99ab6d0d660ce4d21efb52288a39fd35bb3f556980ec5463b1ae8f304a3bbc85","impliedFormat":1},{"version":"6eeded8c7e352be6e0efb83f4935ec752513c4d22043b52522b90849a49a3a11","impliedFormat":1},{"version":"6c1ad90050ffbb151cacc68e2d06ea1a26a945659391e32651f5d42b86fd7f2c","impliedFormat":1},{"version":"55cdbeebe76a1fa18bbd7e7bf73350a2173926bd3085bb050cf5a5397025ee4e","impliedFormat":1},{"version":"2beff543f6e9a9701df88daeee3cdd70a34b4a1c11cb4c734472195a5cb2af54","impliedFormat":1},{"version":"2e07abf27aa06353d46f4448c0bbac73431f6065eef7113128a5cd804d0c384d","impliedFormat":1},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1},{"version":"42bc0e1a903408137c3df2b06dfd7e402cdab5bbfa5fcfb871b22ebfdb30bd0b","impliedFormat":1},{"version":"9894dafe342b976d251aac58e616ac6df8db91fb9d98934ff9dd103e9e82578f","impliedFormat":1},{"version":"413df52d4ea14472c2fa5bee62f7a40abd1eb49be0b9722ee01ee4e52e63beb2","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"446a50749b24d14deac6f8843e057a6355dd6437d1fac4f9e5ce4a5071f34bff","impliedFormat":1},{"version":"182e9fcbe08ac7c012e0a6e2b5798b4352470be29a64fdc114d23c2bab7d5106","impliedFormat":1},{"version":"2f4e6b4d39426a1b85ecf4bdeb9dddbf4d9b3397d95d8555d46f925c9519ec7d","impliedFormat":1},{"version":"78a2869ad0cbf3f9045dda08c0d4562b7e1b2bfe07b19e0db072f5c3c56e9584","impliedFormat":1},{"version":"89d5d28d4f57e000b836ac273079be1b75710e28ce14750d081fb420d37e2ca5","impliedFormat":1},{"version":"fd4e24ccff3966390600d7f5d6aa1fed5a512e92ada735ea5fbc933d313ad3d3","impliedFormat":1},{"version":"b7cddfe1aa6b86b5fad3c9ccb30d05b3ccb165aebbf112f48d2d8a5f69dd98b1","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"ad0d1d75d129b1c80f911be438d6b61bfa8703930a8ff2be2f0e1f8a91841c64","impliedFormat":1},{"version":"bd2c7ada3dee03653d3f601011d30072194bc3970cd93208f9588fbdc0c69347","impliedFormat":1},{"version":"e480da45d32313e7174b265674da504f075f59ef326852f0c5a5d863b438ae85","impliedFormat":1},{"version":"ad54850f61fcf5d014e11be80d2f46fea9265cfa7e77456da876f7833ef81769","impliedFormat":1},{"version":"6f7c9e8bd2b5b6a080b07080065f94900bd3c7e5ebbd3047bc33fcce2fab1dd8","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"da5950ee2a90721df6f3fba45f5d05308f7e4c35835392215dd2cd404505e2de","impliedFormat":1},{"version":"ce75b1aebb33d510ff28af960a9221410a3eaf7f18fc5f21f9404075fba77256","impliedFormat":1},{"version":"f42d5fed19610d485c646a0c430e768115567d078c7fc855c57b0c578b3d6cd3","impliedFormat":1},{"version":"ee8df1cb8d0faaca4013a1b442e99130769ce06f438d18d510fed95890067563","impliedFormat":1},{"version":"d5630f2ad9b4541e5ce891648121022f9412ecdca1820baa1f0104f70fd7eff7","impliedFormat":1},{"version":"4d15375ab13497104bc8fe56fdef2b5fd6853f29255737d23a33fa306ff7fd69","impliedFormat":1},{"version":"2cd3fc1d0d6a1e85baffd2d4f50f5efb192b5446eef567e97c94765402f0aad4","impliedFormat":1},{"version":"e4cbf2f1e89ecccaddd2c045e600ae41b732295953fb06247c7dcbc2d281ed30","impliedFormat":1},{"version":"6dcedaef57dff0d79a05ab0ab602cde74db803d1e765468bf91263786a383e1b","impliedFormat":1},{"version":"8c1697d90c394a6fd955b98eae01238eff628e129b987a68aea10f898a48e7da","impliedFormat":1},{"version":"7580e62139cb2b44a0270c8d01abcbfcba2819a02514a527342447fa69b34ef1","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"f374cb24e93e7798c4d9e83ff872fa52d2cdb36306392b840a6ddf46cb925cb6","impliedFormat":1},{"version":"d10d63718e1646c2279e3b33831f82c60e31f622b2b7020f1196409ca4c09242","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"148679c6d0f449210a96e7d2e562d589e56fcde87f843a92808b3ff103f1a774","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"02436d7e9ead85e09a2f8e27d5f47d9464bced31738dec138ca735390815c9f0","impliedFormat":1},{"version":"f8d5ff8eafd37499f2b6a98659dd9b45a321de186b8db6b6142faed0fea3de77","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"a22dd55aa4d39906252000ab8e8a1b83b195eef7f4274eb51e457c1f11cf6580","impliedFormat":1},{"version":"540cc83ab772a2c6bc509fe1354f314825b5dba3669efdfbe4693ecd3048e34f","impliedFormat":1},{"version":"121b0696021ab885c570bbeb331be8ad82c6efe2f3b93a6e63874901bebc13e3","impliedFormat":1},{"version":"612d9da66bb046a9c1e2e8d026245ded881fc4b9f98cbfae714415d57ee0ae0b","impliedFormat":1},{"version":"32c2ad9494dad5d11b0564a619fee18f388db6c1e9e2cd3c360b3122549691eb","impliedFormat":1},{"version":"6c301d40aec56a74ec7bd7324e31a728dadf9bfba3e96def02938d3d973534ec","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"aa14cee20aa0db79f8df101fc027d929aec10feb5b8a8da3b9af3895d05b7ba2","impliedFormat":1},{"version":"493c700ac3bd317177b2eb913805c87fe60d4e8af4fb39c41f04ba81fae7e170","impliedFormat":1},{"version":"aeb554d876c6b8c818da2e118d8b11e1e559adbe6bf606cc9a611c1b6c09f670","impliedFormat":1},{"version":"acf5a2ac47b59ca07afa9abbd2b31d001bf7448b041927befae2ea5b1951d9f9","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"d71291eff1e19d8762a908ba947e891af44749f3a2cbc5bd2ec4b72f72ea795f","impliedFormat":1},{"version":"c0480e03db4b816dff2682b347c95f2177699525c54e7e6f6aa8ded890b76be7","impliedFormat":1},{"version":"25a5f6fd3a2243c859eddc99ab5fba11d970af2fe7a5df9c32b7668f76f97b01","impliedFormat":1},{"version":"8d207e1f9d2c30d6f77dfa693f3827c3fbf0d89240297e10bdfe1041d433df68","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"d682336018141807fb602709e2d95a192828fcb8d5ba06dda3833a8ea98f69e3","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"4fbd3116e00ed3a6410499924b6403cc9367fdca303e34838129b328058ede40","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"8c70ddc0c22d85e56011d49fddfaae3405eb53d47b59327b9dd589e82df672e7","impliedFormat":1},{"version":"2f9c89cbb29d362290531b48880a4024f258c6033aaeb7e59fbc62db26819650","impliedFormat":1},{"version":"a365c4d3bed3be4e4e20793c999c51f5cd7e6792322f14650949d827fbcd170f","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"273782b8454e78f6a8b30d2cfbf6860499c930595095fcc1689637115f0eddda","affectsGlobalScope":true,"impliedFormat":1},{"version":"3fbdd025f9d4d820414417eeb4107ffa0078d454a033b506e22d3a23bc3d9c41","affectsGlobalScope":true,"impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a8f8e6ab2fa07b45251f403548b78eaf2022f3c2254df3dc186cb2671fe4996d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"9f9bb6755a8ce32d656ffa4763a8144aa4f274d6b69b59d7c32811031467216e","impliedFormat":1},{"version":"5c32bdfbd2d65e8fffbb9fbda04d7165e9181b08dad61154961852366deb7540","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"0c05e9842ec4f8b7bfebfd3ca61604bb8c914ba8da9b5337c4f25da427a005f2","impliedFormat":1},{"version":"faed7a5153215dbd6ebe76dfdcc0af0cfe760f7362bed43284be544308b114cf","impliedFormat":1},{"version":"7029e566b8df176f703fb59fd437a38670c7a0e02c58b2d66dfb5b2e2b2defdb","impliedFormat":1},{"version":"7f2aa4d4989a82530aaac3f72b3dceca90e9c25bee0b1a327e8a08a1262435ad","impliedFormat":1},{"version":"d96b39301d0ded3f1a27b47759676a33a02f6f5049bfcbde81e533fd10f50dcb","impliedFormat":1},{"version":"e9f147ecca73d9346a4c073432843c159ccbe50bdcb678a78f6da10eae2cecf4","impliedFormat":1},{"version":"de061f7d72bd65c06fc1419f841dfdcb29a8e22fe6fa527d1e6eb20b897d4de0","impliedFormat":1},{"version":"663beafc2446079574570cba86e9b15f986f908ddb1b01274509970126fee945","impliedFormat":1},{"version":"a3102887d5058bf4cb5b37fa6964c09e9527c42053b3b5c642b89878620748de","impliedFormat":1},{"version":"0aaaa1727edd29673d85c9b26d7ca4d54e5407a48586903c51b48b7f7d196f61","impliedFormat":1},{"version":"d35bca0b261bff02635758c48e8ab99c61c420d0dfabbcf467e847171d876b7d","impliedFormat":1},{"version":"3bc12c40d90c342ff88a3d876996c555ed5cbee5fe8c3308a240b321f401ee46","impliedFormat":1},{"version":"ba130768aae855a5477e9e148e5c879548e6e7ccbcc56fd1934c8a18ea5b7569","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"d38530db0601215d6d767f280e3a3c54b2a83b709e8d9001acb6f61c67e965fc","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"b499af2054a037a162b3b72cd886f48bbf32a3502c865c6e29fac7d2ab3ce0b5","impliedFormat":1},{"version":"b83cb14474fa60c5f3ec660146b97d122f0735627f80d82dd03e8caa39b4388c","impliedFormat":1},{"version":"48773ca557b0319c2ee62ae249cf52a81709e8be139920d6479a66274de7c4ed","impliedFormat":1},{"version":"7274fbffbd7c9589d8d0ffba68157237afd5cecff1e99881ea3399127e60572f","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"8b4327413e5af38cd8cb97c59f48c3c866015d5d642f28518e3a891c469f240e","impliedFormat":1},{"version":"4cceef18d7f088e797a463e90b7a9dad10c6bc667724b7686e3e740ae00122be","impliedFormat":1},{"version":"7ee86fbb3754388e004de0ef9e6505485ddfb3be7640783d6d015711c03d302d","impliedFormat":1},{"version":"cc1954b539604b1e562319119ac7e888172208b32ca873f9a357a92c826bd046","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"bb8f2dbc03533abca2066ce4655c119bff353dd4514375beb93c08590c03e023","impliedFormat":1},{"version":"706dd95827e7ebaabda91d5db2b755233e0952d98570e9c032b0f066a15c1177","affectsGlobalScope":true,"impliedFormat":1},{"version":"0b103e9abfe82d14c0ad06a55d9f91d6747154ef7cacc73cf27ecad2bfb3afcf","impliedFormat":1},{"version":"990b8fad2327b77e6920cc792af320e8867e68f02ce849b12c0a6ab9a1aebb09","impliedFormat":1},{"version":"5eb8cd1cb0c9143d74a8190b577c522720878c31aef67d866fcd29973f83e955","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"a6805fcafed712aea7759f8bc731014f9d22738c1d6ef9d43b8091d1d48346d5","impliedFormat":1},{"version":"c49469a5349b3cc1965710b5b0f98ed6c028686aa8450bcb3796728873eb923e","impliedFormat":1},{"version":"4a889f2c763edb4d55cb624257272ac10d04a1cad2ed2948b10ed4a7fda2a428","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"d88ea80a6447d7391f52352ec97e56b52ebec934a4a4af6e2464cfd8b39c3ba8","impliedFormat":1},{"version":"142617b3cdf902b69c6464c9fbd942b60ab3e733ca18c032b19e0f7e2adbefe8","impliedFormat":1},{"version":"0b603555f1881f87256ffd6344d3e3ed6d466c2e701eabf381f28be8c2125892","impliedFormat":1},{"version":"897e4f7662488e3ecc79e743bdd3b78f13bdb69a97851afa5b440c4211e32ea9","impliedFormat":1},{"version":"e2e1c6d3b2d93add5200bd7bc1a8cccb4e446836b2111ece45db8683a2c765de","impliedFormat":1},{"version":"251b03d5cd243854ce870d9a9a39f491faf69898c5d6b5eee28cc7649c57417b","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"2c4de79f406d137390608e8c0a44fba2ff8e00bacfcae7c9d1781fef10e9440d","impliedFormat":1},{"version":"07ba23a10465791be5d22deaf5ef7de7658774ddff53721e5ea17fedea1bc721","impliedFormat":1},{"version":"dca8c645c5afeb03b1ecedbf16323f33e7d0afaa6256c8e047e6e38087a97f53","impliedFormat":1},{"version":"775f181bd4a533d6f8b5e55ec1d9f1624559720ae8a70e9432258da26b38d27c","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"9109a1291dd4b9f1541bea81ee11c247a2ca9e1ea89f87f13aa1811c3c069616","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"622694a8522b46f6310c2a9b5d2530dde1e2854cb5829354e6d1ff8f371cf469","impliedFormat":1},{"version":"cd8ce8d68567f62dd580b3c3c37777ac3f5b81944c7417f5ea83030eab533385","impliedFormat":1},{"version":"e374d1eaa05b7dc38580062942ac8351ce79cbe11f6dbce4946a582a5680582d","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"a9e6c0ff3f8186fccd05752cf75fc94e147c02645087ac6de5cc16403323d870","impliedFormat":1},{"version":"49af4b52f0d4d2304c5f2c6fe5fab3e153e0acc38830d0202821b877c097dd02","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"e68b8e5a1df7c1be2bc105141456ecba70215806e1c28bfbc5c12bfce4be6e68","impliedFormat":1},{"version":"511c8f02329808d47d00b859c532ae9115590048b17325a946c74dac48428650","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"b5f9e66625783eefcbe3d2da074b2e7ba2066d61ce3fc6ef4f22805ad946cab4","impliedFormat":1},{"version":"e37115962d284b9f7a37c2bdd2add50f88365dde41f5e0ff591ffc48a8ec7575","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"f89488602bec98a142072fae7ea5ba99431a569ff580c64b7be39896474799d8","impliedFormat":1},{"version":"bbbc47961f39a57df103cf4ca3bb8f8732b4b6678a18225a0aa76d59c466956c","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"2ffb043dc5163458e473b7010859f86e01dc4edffcae0a93d885d028b426a546","impliedFormat":1},{"version":"c8f004e6036aa1c764ad4ec543cf89a5c1893a9535c80ef3f2b653e370de45e6","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"b064c36f35de7387d71c599bfcf28875849a1dbc733e82bd26cae3d1cd060521","impliedFormat":1},{"version":"05c7280d72f3ed26f346cbe7cbbbb002fb7f15739197cbbee6ab3fd1a6cb9347","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"04b7b2e0832dfd3c31e81df3975e8d8fda28e7ff999b0aa2932608a8f6661d5c","impliedFormat":1},{"version":"ca2d34c6ed5cbd3070b8b6f32f42ae54adcc6499c1e4b99f0a5798b3f27cc653","impliedFormat":1},{"version":"9ec68995e66dd6b9dac834bf5ae85fde802714ea2e82151a5d1d53ef01b463ef","impliedFormat":1},{"version":"5c4d626b4902f2ef8a1cc146d761d276cef988016dc674e3b98fbad70e64bc9f","impliedFormat":1},{"version":"fdfaa0aad899524962e2955287b5b991ffe3be50f64e02eb60c933ca44644a94","impliedFormat":1},{"version":"53c972a0f9bc3a4ec70fff7314123ea8cfcf75b3703046f767d2dc1eea87b2fb","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"7d73b24e7bf31dfb8a931ca6c4245f6bb0814dfae17e4b60c9e194a631fe5f7b","impliedFormat":1},{"version":"d130c5f73768de51402351d5dc7d1b36eaec980ca697846e53156e4ea9911476","impliedFormat":1},{"version":"413586add0cfe7369b64979d4ec2ed56c3f771c0667fbde1bf1f10063ede0b08","impliedFormat":1},{"version":"06472528e998d152375ad3bd8ebcb69ff4694fd8d2effaf60a9d9f25a37a097a","impliedFormat":1},{"version":"7303b45138d2511035056a5901a1490ebdcbf055cbb1276f8629c5121cbe733e","impliedFormat":1},{"version":"27f874cd5327507eeff699a74567f60c1215b94509f4308633a7b01922471ed2","impliedFormat":1},{"version":"a401617604fa1f6ce437b81689563dfdc377069e4c58465dbd8d16069aede0a5","impliedFormat":1},{"version":"2c6cf04bc525caf6546e859e8ef10bfb9573837ec0bc5ec7b53a7b1b8ca72781","impliedFormat":1},{"version":"8695dec09ad439b0ceef3776ea68a232e381135b516878f0901ed2ea114fd0fe","impliedFormat":1},{"version":"304b44b1e97dd4c94697c3313df89a578dca4930a104454c99863f1784a54357","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"87cc05fe13108f02e12da7e3efd8e360fef78d96a0c9e11408ea1b1b9fb3e03d","impliedFormat":1},{"version":"1abbf67c218d23c2ce76887caac2df6c7dab3d97ba2b65348432b876f510002a","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"c06ef3b2569b1c1ad99fcd7fe5fba8d466e2619da5375dfa940a94e0feea899b","impliedFormat":1},{"version":"f7d628893c9fa52ba3ab01bcb5e79191636c4331ee5667ecc6373cbccff8ae12","impliedFormat":1},{"version":"1d879125d1ec570bf04bc1f362fdbe0cb538315c7ac4bcfcdf0c1e9670846aa6","impliedFormat":1},{"version":"8bd496cf710d4873d15e4891a5dbf945673e3321ca74cf75187e347fd5ed295e","impliedFormat":1},{"version":"a6dba407fc287f1e25454e75028c91bbc00675f2d1c4e8b3edcc36c08611a486","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"e91f7b1344577a02f051b9b471f33044fef8334a76dc9e1de003d17595a5219b","impliedFormat":1},{"version":"c0723195c85e19656d6b5b9fdb81d3f3403c1ae4679e722c6ea058c516b38d12","impliedFormat":1},{"version":"186eea74805194f04e41038fc5eca653788b9dedbab7c2d7d17e10139622dd92","impliedFormat":1},{"version":"71d9eb4c4e99456b78ae182fb20a5dfc20eb1667f091dbb9335b3c017dd1c783","impliedFormat":1},{"version":"cfa846a7b7847a1d973605fbb8c91f47f3a0f0643c18ac05c47077ebc72e71c7","impliedFormat":1},{"version":"1594da19968752a22b2ac48c2d0e60575700e745c577a8a4a676b841238ad5bb","impliedFormat":1},{"version":"e0cee12109e0a10a4c3d6769fcc7644b7c1ea7f52365bea51728f5af29f8a137","impliedFormat":1},{"version":"7d4254b4c6c67a29d5e7f65e67d72540480ac2cfb041ca484847f5ae70480b62","impliedFormat":1},{"version":"3536968defef8a75514f547ead5e2e9c1e984820290ec9b00c5fdfb6ef786535","impliedFormat":1},{"version":"d83773870080c30a230e322ce13a9c6f3398e8dacea4ea8a83e26370f3bac23e","impliedFormat":1},{"version":"dcfeaf98d66314fec29a9076c4290e45d0b196a65827becc19138e9c7b855f37","impliedFormat":1},{"version":"6849fe9210fe4946d5f085bfed36758f33dc6ae15a751338d178dd4daa017c46","impliedFormat":1},{"version":"888cda0fa66d7f74e985a3f7b1af1f64b8ff03eb3d5e80d051c3cbdeb7f32ab7","impliedFormat":1},{"version":"60681e13f3545be5e9477acb752b741eae6eaf4cc01658a25ec05bff8b82a2ef","impliedFormat":1},{"version":"ffae4e1e06aa848a1e4bcef162cd1c48e5909b26223515981310af9c036bdfc7","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"34e16eb7c31768a11a08aebcfb3d70d7b8f0b016197e98d8419e566ceae6d6c8","impliedFormat":1},{"version":"f94ec1f7e4b709d26960306c9082a7a1b728a6e13089346aa48ba57c74cbf47e","impliedFormat":1},{"version":"9a11cb4033405e96c247cd5aa29790212aaffdd127869e8a5219103f0b389fd5","impliedFormat":1},{"version":"01479d9d5a5dda16d529b91811375187f61a06e74be294a35ecce77e0b9e8d6c","impliedFormat":1},{"version":"aff5213585cb72e94054dfe17250ff315f3569b3919d1ef1ad235f37c4ee894e","impliedFormat":1},{"version":"fb2ea35e1be6388d722d7725e2b49c697d34d9c890c3b96758faaeb86d35cef8","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"1a4dc28334a926d90ba6a2d811ba0ff6c22775fcc13679521f034c124269fd40","impliedFormat":1},{"version":"f05315ff85714f0b87cc0b54bcd3dde2716e5a6b99aedcc19cad02bf2403e08c","impliedFormat":1},{"version":"5fad3b31fc17a5bc58095118a8b160f5260964787c52e7eb51e3d4fcf5d4a6f0","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"456006a6975b26c0a1785feddae165f6d307e2d601ffde27e21fc4a790e448a4","impliedFormat":1},{"version":"c857e0aae3f5f444abd791ec81206020fbcc1223e187316677e026d1c1d6fe08","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"1fe0d18b111e1145a7e7601855bccd4ca20f24e3b9a5aba6bb1fa9d1a7059170","impliedFormat":1},{"version":"5632c3c26d420c063eebe64c45b1248b9492a67bf44f1d0c57e9dc8f6cf449bb","impliedFormat":1},{"version":"0df5aa619ab12993a39ea6dae062ee46eadbb4d738916460e636ada52bced75b","impliedFormat":1},{"version":"8fca3039857709484e5893c05c1f9126ab7451fa6c29e19bb8c2411a2e937345","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"10ab7be91f87ebe8916b62cf28af2e45b5601fc7b0e311adf838f912c6b31dd8","impliedFormat":1},{"version":"bc636fbc08e0979ceb7eb0731a33000283d77a33b62e1f71ee65be50394e40ba","impliedFormat":1},{"version":"7e0b7f91c5ab6e33f511efc640d36e6f933510b11be24f98836a20a2dc914c2d","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"75bbd3be047d539988a0ff0b56384ef7a6a25f3b676ad96bee547d44c31622a7","impliedFormat":1},{"version":"42960001a776b089ade681ab5cfddc936e0afb0615133ec1841f3dee89d3e1bf","impliedFormat":1},{"version":"0aedb02516baf3e66b2c1db9fef50666d6ed257edac0f866ea32f1aa05aa474f","impliedFormat":1},{"version":"da47712b394d944328245482603bc6f416d3949b67c9392279caab595076b510","affectsGlobalScope":true,"impliedFormat":1},{"version":"37d0071d8f0a06dc55c2c5e0ec3391affd4fd107c53410bf358196ec0bf3923f","impliedFormat":1},{"version":"b213dad76ca37fd552274c9499056e1c0d9c1bd38a55bb7f68b22ba6b84c3ad7","impliedFormat":1},{"version":"56ccb49443bfb72e5952f7012f0de1a8679f9f75fc93a5c1ac0bafb28725fc5f","impliedFormat":1},{"version":"20fa37b636fdcc1746ea0738f733d0aed17890d1cd7cb1b2f37010222c23f13e","impliedFormat":1},{"version":"d90b9f1520366d713a73bd30c5a9eb0040d0fb6076aff370796bc776fd705943","impliedFormat":1},{"version":"bc03c3c352f689e38c0ddd50c39b1e65d59273991bfc8858a9e3c0ebb79c023b","impliedFormat":1},{"version":"19df3488557c2fc9b4d8f0bac0fd20fb59aa19dec67c81f93813951a81a867f8","affectsGlobalScope":true,"impliedFormat":1},{"version":"b25350193e103ae90423c5418ddb0ad1168dc9c393c9295ef34980b990030617","affectsGlobalScope":true,"impliedFormat":1},{"version":"bef86adb77316505c6b471da1d9b8c9e428867c2566270e8894d4d773a1c4dc2","impliedFormat":1},{"version":"5a49adaef698b7ad7e6127949fa1b0bbd3d46b7cbd11c54e392a4dcdd51f5190","impliedFormat":1},{"version":"96171c03c2e7f314d66d38acd581f9667439845865b7f85da8df598ff9617476","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"2489bf04d77dc025ba67f49f1a56eb24b9db477d5ff88123d887e163ed1776aa","impliedFormat":1},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"0b77b819b5417775fccb20c678293cf614c054a5b1a65421a5b933a9124ba998","impliedFormat":1},{"version":"e1f6076688a95bd82deaac740fccbe3cdea0d8a22057cccc9c5bce4398bdd33b","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"b1f1d57fde8247599731b24a733395c880a6561ec0c882efaaf20d7df968c5af","impliedFormat":1},{"version":"6715dc4eb59c8ea9abe2b78c235ed331dc710a06fe56798868dbc4d40cd1b707","impliedFormat":1},{"version":"35e6379c3f7cb27b111ad4c1aa69538fd8e788ab737b8ff7596a1b40e96f4f90","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"5a3ea721d03a361ccbdd7390ccd75f6e84cbca3a3f01f4b331ecc9af31890c49","impliedFormat":1},{"version":"e7dfaee4af38d45b1cab8a1ee0b3bc1f85ddcf64545ed391d675d78ae6526274","affectsGlobalScope":true,"impliedFormat":1},{"version":"e8daa443eaf9a27fd382cc1f8ebe30330c0f4d89511cfb469166874806751d35","impliedFormat":1},{"version":"af48e58339188d5737b608d41411a9c054685413d8ae88b8c1d0d9bfabdf6e7e","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"1de8c302fd35220d8f29dea378a4ae45199dc8ff83ca9923aca1400f2b28848a","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"332248ee37cca52903572e66c11bef755ccc6e235835e63d3c3e60ddda3e9b93","impliedFormat":1},{"version":"94e8cc88ae2ef3d920bb3bdc369f48436db123aa2dc07f683309ad8c9968a1e1","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"320f4091e33548b554d2214ce5fc31c96631b513dffa806e2e3a60766c8c49d9","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"d90d5f524de38889d1e1dbc2aeef00060d779f8688c02766ddb9ca195e4a713d","impliedFormat":1},{"version":"07ed3ddab975995eea41b22f3010506fb9f5fb301d04820b07d7a1aee5477d7c","impliedFormat":1},{"version":"969d8b0965849f4bae7cab0ba90bd1e1220e95999c2c6f01117fa7500901c017","impliedFormat":1},{"version":"6ec840ee5e2bc103f557fe38b1d585ee250540468713d7634ee066de372bf332","impliedFormat":1},{"version":"b0309e1eda99a9e76f87c18992d9c3689b0938266242835dd4611f2b69efe456","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"6ceb10ca57943be87ff9debe978f4ab73593c0c85ee802c051a93fc96aaf7a20","impliedFormat":1},{"version":"1de3ffe0cc28a9fe2ac761ece075826836b5a02f340b412510a59ba1d41a505a","impliedFormat":1},{"version":"e46d6cc08d243d8d0d83986f609d830991f00450fb234f5b2f861648c42dc0d8","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"ff863d17c6c659440f7c5c536e4db7762d8c2565547b2608f36b798a743606ca","impliedFormat":1},{"version":"5412ad0043cd60d1f1406fc12cb4fb987e9a734decbdd4db6f6acf71791e36fe","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"e297c0a524edee7677939122f90027bfbe5f2698939d9a85728e5044b39c7124","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"bc9ee0192f056b3d5527bcd78dc3f9e527a9ba2bdc0a2c296fbc9027147df4b2","impliedFormat":1},{"version":"b62381cae176db34f003cc6172ee8f3e0122014889d66391aa73698105cf4934","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"30d8da250766efa99490fc02801047c2c6d72dd0da1bba6581c7e80d1d8842a4","impliedFormat":1},{"version":"03566202f5553bd2d9de22dfab0c61aa163cabb64f0223c08431fb3fc8f70280","impliedFormat":1},{"version":"41eb514d9ce0a6e87957f08a4b7af70d93f87637f37dee706e2d92a6601c25a9","impliedFormat":1},{"version":"e7765aa8bcb74a38b3230d212b4547686eb9796621ffb4367a104451c3f9614f","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"7bd01f0f28cd3aeb2046274d85208e245965f6f2948edf4f7b2057bcf9f22ccc","impliedFormat":99},{"version":"d2f2cf2b8cc92bea913cda4a076e0f790b23a21e84f989d12f0116a7fe3906e0","impliedFormat":99},{"version":"6de125ea94866c736c6d58d68eb15272cf7d1020a5b459fea1c660027eca9a90","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5b20bc288ee49989c95b20847fc93b96bf61cc0845598897a6a53a967dd7d07","affectsGlobalScope":true,"impliedFormat":1},{"version":"064ac1c2ac4b2867c2ceaa74bbdce0cb6a4c16e7c31a6497097159c18f74aa7c","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"d3b315763d91265d6b0e7e7fa93cfdb8a80ce7cdd2d9f55ba0f37a22db00bdb8","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},{"version":"bb6d9c2b075a5c675259c75950c5f2359b17bf031b8cbdff8c04aee66a6a514f","affectsGlobalScope":true},"7ad303e40d4fddf44f156129e397511953a71481c5cfd86b1862649aaaf240cc",{"version":"168aa44771e55cda628eeb5fbb6f626f6f263d827967b119e8c06abd7f1362ec","signature":"435a1e418e8338be3f39614b96b81a9aa2700bc8c27bc6b98f064ff9ce17c363"},{"version":"3b89216a7e38a454985ad17bb2ff85792837dc812f2a89fa5f60ad0a2e216fa7","impliedFormat":99},{"version":"16fe60bb544cfedfd2b5bb2f7d0b3957be7978706d57d9f06edc9c0c8dbdba23","impliedFormat":99},{"version":"82179358c2d9d7347f1602dc9300039a2250e483137b38ebf31d4d2e5519c181","impliedFormat":99},{"version":"c73fdf42528325dd17940937ed787b15ae3445c6a2dae1a2b74bc4d87d337ca2","impliedFormat":99},{"version":"e8e17dfef3cfa9f0847ac93dd535a9896af7fb57c1a1b164484bb1b0ee4a25d8","impliedFormat":99},{"version":"51d2ffea2d1ee4a81c775938588c1e16620281adb60cbc26579a2fc6baa10bd2","impliedFormat":99},{"version":"148debd12783ded0a60d115daeacd8136f77757ae89a05c4e18de6dd77646fd2","impliedFormat":99},{"version":"0088b02dca63c47b273a140d0a3944bdc6dc2eb765fff0ca98e3c3a2786b3a5a","impliedFormat":99},{"version":"a651d06b780fa354231f19b040cbcde484bede3218885752b4f9e9a8f72d3b5f","impliedFormat":99},{"version":"06e26f75bed4c8389a8a63f0e6d6a9068038873dc95d8d1338e8c370a0ae8bc3","impliedFormat":99},{"version":"a2155e2675fd1af52b0b70779371c28611cdd1076b29d0f68bf93b983e5ddce0","impliedFormat":99},{"version":"a413e4b0b99280e1e58f5fe7b2b585e8a9be4996df8c58585399c9e2ca8a683e","impliedFormat":99},{"version":"609ab2c225766bc0851251c1db0fd5492673e190074045d21dc5dc7c3c46d785","impliedFormat":99},{"version":"c074e054c9db79055d37d7d70131e9a3234b8186773b3edb617c13f80bcf8774","impliedFormat":99},{"version":"7d3e062a778b8f5ea4f0cac7e925e31f88e6739812ebc5f827474324a4048f14","impliedFormat":99},{"version":"7f3857dc5cfe1e5e977edb14e931d9939a952e8e41997263a927f8f0299ea652","impliedFormat":99},{"version":"3559624d0102d10d7765c292c60ccbc229541534db32061e06df88bfe1064636","impliedFormat":99},{"version":"5a9834c603c65aee5cba0c1d6b3c7aee85cdc7862832a23165c6aa4139c165f2","impliedFormat":99},{"version":"a7d7b5fa83cd7b3b4c2aa73bc29e7cbd53d5690b74f6fb39a5558af0a94967ba","impliedFormat":99},{"version":"4e003c868b0d8f8ad200b96cbc653e18e513fa23e1c19c4fe3cc25d4394efc47","impliedFormat":99},{"version":"605450898939e8abce51e8085a41b60640278337a969c33cd6b169e7c4f9c3f2","impliedFormat":99},{"version":"e0864480ea083087d705f9405bd6bf59b795e8474c3447f0d6413b2bce535a09","impliedFormat":99},{"version":"e67cbea16f1994af89efd700542dbf3828a46a52b29e4d67e801bd7869dc103c","impliedFormat":99},{"version":"f582b0fcbf1eea9b318ab92fb89ea9ab2ebb84f9b60af89328a91155e1afce72","impliedFormat":99},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"e3cf0611709328b449ec13f8c436712d62003620ce480139fae46ce001c2ee9f","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"3fd8a5aefd8c3feb3936ca66f5aa89dff7bf6e6537b4158dbd0f6e0d65ed3b9e","impliedFormat":1},{"version":"a18642ddf216f162052a16cba0944892c4c4c977d3306a87cb673d46abbb0cbf","impliedFormat":1},{"version":"41c41c6e90133bb2a14f7561f29944771886e5535945b2b372e2f6ed6987746e","impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":99},{"version":"960bd764c62ac43edc24eaa2af958a4b4f1fa5d27df5237e176d0143b36a39c6","affectsGlobalScope":true,"impliedFormat":99},{"version":"f093d4bd6a9267be5f8ecbfbca19f4f3359b3839883206150c5d833606569e84","impliedFormat":99},{"version":"59f8dc89b9e724a6a667f52cdf4b90b6816ae6c9842ce176d38fcc973669009e","affectsGlobalScope":true,"impliedFormat":99},{"version":"4a13397dffad4475c45c70fde584c925fe8c9218b3c7ab94397b68fc434f63b6","impliedFormat":99},{"version":"2faebfa830ae4cfbfb58e48b0ec20a2a63882d776f0ca36ec7155d45cf1b7f2d","impliedFormat":99},{"version":"b478fad6cb2c66bfbfc027983240b416a7733013f878056ba92cf809020018a0","impliedFormat":99},{"version":"c76c02846ba7d40b9b3488f0e8d75d02cbdee2f0bc5fcd55dd3bd2e1457646ea","impliedFormat":99},{"version":"4ead13a482c539b77394b2a97e3b877b809eac596390371cea490286f53b996a","impliedFormat":99},{"version":"06db2f8ba1d1dfacf04529cb731081ab23f133f29c7608ebdfbcab356996827c","impliedFormat":99},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"3a582c6e8906f5b094ccf0de6cc6f4f8a54b05a34f52517aba5c9c7f704f6b28","impliedFormat":99},{"version":"ef13c73d6157a32933c612d476c1524dd674cf5b9a88571d7d6a0d147544d529","impliedFormat":99},{"version":"3b0a56d056d81a011e484b9c05d5e430711aaecd561a788bad1d0498aad782c7","impliedFormat":99},{"version":"0528f6d21f7a02d4092895090d2dd86104bd5a3e79eced96d5a1a7dd90943d17","impliedFormat":99},{"version":"427fe2004642504828c1476d0af4270e6ad4db6de78c0b5da3e4c5ca95052a99","impliedFormat":1},{"version":"2eeffcee5c1661ddca53353929558037b8cf305ffb86a803512982f99bcab50d","impliedFormat":99},{"version":"9afb4cb864d297e4092a79ee2871b5d3143ea14153f62ef0bb04ede25f432030","affectsGlobalScope":true,"impliedFormat":99},{"version":"5c935b7fc4ddc1410ea1cd7cd4e35ed106a6e4920dd27a9480a40fd224359dc3","affectsGlobalScope":true,"impliedFormat":99},{"version":"b5ce343886d23392be9c8280e9f24a87f1d7d3667f6672c2fe4aa61fa4ece7d4","impliedFormat":99},{"version":"72ce5b734c05da85c85a6f6dc05823b051d6aa41acaedeeb1d17c72f3b4efa72","impliedFormat":99},{"version":"b0857bb28fd5236ace84280f79a25093f919fd0eff13e47cc26ea03de60a7294","impliedFormat":99},{"version":"5e43e0824f10cd8c48e7a8c5c673638488925a12c31f0f9e0957965c290eb14c","impliedFormat":99},{"version":"9443967db823b66d1682be7fc66392be7c7924e10c3e54900f456341e94591a6","impliedFormat":99},{"version":"424f71d1fae96ac2e878af92345bb87bea1d29f757228fbc190133b305643f2c","impliedFormat":99},{"version":"61bb64660ee150f3ab618340e15cca0a81664801bede7c966ca0eca3a952fe63","impliedFormat":99},{"version":"42a12f2faa483c9b48195ed794d22698162274e755f6e07219c2351c4f08d732","impliedFormat":99},{"version":"ec0c42bb0f465e4993f2bc68a6ce9df9a2dcbc7b83e21748f82f1b69561938e3","impliedFormat":99},{"version":"f50ff37a9cbbe74475f426474d9827083c7c2c138a954d28f1690df338f69291","impliedFormat":99},{"version":"61fd6c17235d530c40f543dd7c40afab091d91c1ef890baeed30db6d82b04b28","impliedFormat":99},{"version":"bcbd3becd08b4515225880abea0dbfbbf0d1181ce3af8f18f72f61edbe4febfb","impliedFormat":99},{"version":"091767bc841f937654ed597d49e023ed59850355e746ae1a6f20ab31076ee1fb","impliedFormat":99},{"version":"19c6d6135af59693698d384050b45a8a049493500add442f58e4bd7c8a255ab6","impliedFormat":99},{"version":"6a0dba12d55314638a8c51108b20fe2f68f1364a619d098918bda91c22dec154","impliedFormat":99},{"version":"8124828a11be7db984fcdab052fd4ff756b18edcfa8d71118b55388176210923","impliedFormat":99},{"version":"ed9bb55ddcbebd5cb3eee991f57ff21438546ee40ee1c310281bd12a6c7cf65b","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"6987dfb4b0c4e02112cc4e548e7a77b3d9ddfeffa8c8a2db13ceac361a4567d9","impliedFormat":99},{"version":"5e2ba3d18d78aebbde1f34bde356e41e9c76eeaeaeee56a37036596a9eff4211","impliedFormat":99},{"version":"8280ae8ccc0493b32d1742d585357ab9f0a508ea050af25a5a20d64010d0a5cf","impliedFormat":99},{"version":"7adfd9f9056ecd4ae6c65fde2a98654960c662714c73f048478959d04c09e144","impliedFormat":99},{"version":"437b7613a30a2fcde463f7b707c6d5567a8823fbc51de50b8641bf5b1d126fad","impliedFormat":99},{"version":"63ea959e28c110923f495576e614fb8b36c09b6828b467b2c7cd7f03b03ccf9f","impliedFormat":99},{"version":"1601a95dbb33059fc3d12638ed2a9aecff899e339c5c0f3a0b28768866d385b4","impliedFormat":99},{"version":"56fc978580577d30f4c2cdb5b1eb9217b66ed66537dd27141256f426e4b8dd68","impliedFormat":99},{"version":"2c5413050a2580becf9d82dd7e3006b95623e96f145356bf73230cd635352f70","impliedFormat":99},{"version":"860bedc71ead192ea4a0ea5ef4686e65724d14b391ebd1a6671a7044e6bd8e15","impliedFormat":99},{"version":"7c0a845bee4a084cbb8654709f48e5f13e2f6d45e5e2dde7c57cadf79fd9e3d5","impliedFormat":99},{"version":"07ad8a597ac75084e3dd9f9fadf5e8d7ccdcfe2f0c94ea0cf1cd8aa027a6c46e","impliedFormat":99},{"version":"94ddb4a2bb0c69e8efea22c58c2b6f84017eba469a4e433f5396ea8619d051cb","impliedFormat":99},{"version":"064499a671b662b25675beccdd04fb0bdebb6bd49bdb90d448e4b1ce3db20526","impliedFormat":99},{"version":"7bbff6783e96c691a41a7cf12dd5486b8166a01b0c57d071dbcfca55c9525ec4","impliedFormat":99},{"version":"ae7d986f19db00cd62ce8573307f910ec2103d7fc30df09cedeec3cabec13082","signature":"4b96dd19fd2949d28ce80e913412b0026dc421e5bf6c31d87c7b5eb11b5753b4"},{"version":"ae77d81a5541a8abb938a0efedf9ac4bea36fb3a24cc28cfa11c598863aba571","impliedFormat":1},{"version":"f329dfad7970297cbf07ddc8fce2ad4a24e2a3855917c661922ef86eb24dd1f1","impliedFormat":1},{"version":"841784cfa9046a2b3e453d638ea5c3e53680eb8225a45db1c13813f6ea4095e5","affectsGlobalScope":true,"impliedFormat":1},{"version":"646ef1cff0ec3cf8e96adb1848357788f244b217345944c2be2942a62764b771","impliedFormat":1},{"version":"3cfb7c0c642b19fb75132154040bb7cd840f0002f9955b14154e69611b9b3f81","impliedFormat":1},{"version":"8387ec1601cf6b8948672537cf8d430431ba0d87b1f9537b4597c1ab8d3ade5b","impliedFormat":1},{"version":"d16f1c460b1ca9158e030fdf3641e1de11135e0c7169d3e8cf17cc4cc35d5e64","impliedFormat":1},{"version":"a934063af84f8117b8ce51851c1af2b76efe960aa4c7b48d0343a1b15c01aedf","impliedFormat":1},{"version":"e3c5ad476eb2fca8505aee5bdfdf9bf11760df5d0f9545db23f12a5c4d72a718","impliedFormat":1},{"version":"462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094","impliedFormat":1},{"version":"5923d8facbac6ecf7c84739a5c701a57af94a6f6648d6229a6c768cf28f0f8cb","impliedFormat":1},{"version":"d0570ce419fb38287e7b39c910b468becb5b2278cf33b1000a3d3e82a46ecae2","impliedFormat":1},{"version":"3aca7f4260dad9dcc0a0333654cb3cde6664d34a553ec06c953bce11151764d7","impliedFormat":1},{"version":"a0a6f0095f25f08a7129bc4d7cb8438039ec422dc341218d274e1e5131115988","impliedFormat":1},{"version":"b58f396fe4cfe5a0e4d594996bc8c1bfe25496fbc66cf169d41ac3c139418c77","impliedFormat":1},{"version":"45785e608b3d380c79e21957a6d1467e1206ac0281644e43e8ed6498808ace72","impliedFormat":1},{"version":"bece27602416508ba946868ad34d09997911016dbd6893fb884633017f74e2c5","impliedFormat":1},{"version":"2a90177ebaef25de89351de964c2c601ab54d6e3a157cba60d9cd3eaf5a5ee1a","impliedFormat":1},{"version":"82200e963d3c767976a5a9f41ecf8c65eca14a6b33dcbe00214fcbe959698c46","impliedFormat":1},{"version":"b4966c503c08bbd9e834037a8ab60e5f53c5fd1092e8873c4a1c344806acdab2","impliedFormat":1},{"version":"3d3208d0f061e4836dd5f144425781c172987c430f7eaee483fadaa3c5780f9f","impliedFormat":1},{"version":"34a8a5b4c21e7a6d07d3b6bce72371da300ec1aed58961067e13f1f4dc849712","impliedFormat":1},{"version":"4ffba3c5848b4fe62ee59b754fd5f256ad9656a0db6d37b9a2a8cb40dfc7ac21","impliedFormat":99},{"version":"c76c02846ba7d40b9b3488f0e8d75d02cbdee2f0bc5fcd55dd3bd2e1457646ea","impliedFormat":99},{"version":"32b35cf0dc3a1b1a7118b61c34ce2ad1a29695851679f9ec34e0776f2ece2a69","impliedFormat":99},{"version":"b413fbc6658fe2774f8bf9a15cf4c53e586fc38a2d5256b3b9647da242c14389","impliedFormat":99},{"version":"59e5e964b84fdb2378e9455e4e59405030e4ed2b4c6f891ce395f17796af3cbb","impliedFormat":99},{"version":"c30a41267fc04c6518b17e55dcb2b810f267af4314b0b6d7df1c33a76ce1b330","impliedFormat":1},{"version":"72422d0bac4076912385d0c10911b82e4694fc106e2d70added091f88f0824ba","impliedFormat":1},{"version":"da251b82c25bee1d93f9fd80c5a61d945da4f708ca21285541d7aff83ecb8200","impliedFormat":1},{"version":"64db14db2bf37ac089766fdb3c7e1160fabc10e9929bc2deeede7237e4419fc8","impliedFormat":1},{"version":"98b94085c9f78eba36d3d2314affe973e8994f99864b8708122750788825c771","impliedFormat":1},{"version":"90ba95a763101bb61b8a799731a2ed60b5016b8135c1a2d5186862d4b534d4a1","impliedFormat":99},{"version":"ad763fa0c24ede2b818eb6598c12dd581451f94688fb9ed963beba20d513a7ec","signature":"90ec9100c29e008c3d9194acd818e2cfa6dc6e177154bc8e10c5959aa35619ed"},{"version":"b7ca2f47522d4ea41e65ff92c4c6dd9c4c8260da7c456a7631a9c88dc056b4d0","impliedFormat":1},{"version":"4f01e4d0959f9125b89e5737eb1ca2bfa69fd6b7d6126eba22feb8b505b00cde","impliedFormat":1},{"version":"4363a1adb9c77f2ed1ca383a41fbab1afadd35d485c018b2f84e834edde6a2c7","impliedFormat":1},{"version":"1d6458533adb99938d041a93e73c51d6c00e65f84724e9585e3cc8940b25523f","impliedFormat":1},{"version":"b0878fbd194bdc4d49fc9c42bfeeb25650842fe1412c88e283dc80854b019768","impliedFormat":1},{"version":"a892ea0b88d9d19281e99d61baba3155200acced679b8af290f86f695b589b16","impliedFormat":1},{"version":"03b42e83b3bcdf5973d28641d72b81979e3ce200318e4b46feb8347a1828cd5d","impliedFormat":1},{"version":"8a3d57426cd8fb0d59f6ca86f62e05dde8bfd769de3ba45a1a4b2265d84bac5a","impliedFormat":1},{"version":"afc6e1f323b476fdf274e61dab70f26550a1be2353e061ab34e6eed180d349b6","impliedFormat":1},{"version":"7c14483430d839976481fe42e26207f5092f797e1a4190823086f02cd09c113c","impliedFormat":1},{"version":"828a3bea78921789cbd015e968b5b09b671f19b1c14c4bbf3490b58fbf7d6841","impliedFormat":1},{"version":"69759c42e48938a714ee2f002fe5679a7ab56f0b5f29d571e4c31a5398d038fe","impliedFormat":1},{"version":"6e5e666fa6adeb60774b576084eeff65181a40443166f0a46ae9ba0829300fcb","impliedFormat":1},{"version":"1a4d43bdc0f2e240395fd204e597349411c1141dd08f5114c37d6268c3c9d577","impliedFormat":1},{"version":"874e58f8d945c7ac25599128a40ec9615aa67546e91ca12cbf12f97f6baf54ff","impliedFormat":1},{"version":"da2627da8d01662eb137ccd84af7ffa8c94cf2b2547d4970f17802324e54defc","impliedFormat":1},{"version":"07af06b740c01ed0473ebdd3f2911c8e4f5ebf4094291d31db7c1ab24ff559aa","impliedFormat":1},{"version":"ba1450574b1962fcf595fc53362b4d684c76603da5f45b44bc4c7eeed5de045b","impliedFormat":1},{"version":"b7903668ee9558d758c64c15d66a89ed328fee5ac629b2077415f0b6ca2f41bc","impliedFormat":1},{"version":"c7628425ee3076c4530b4074f7d48f012577a59f5ddade39cea236d6405c36ba","impliedFormat":1},{"version":"28c8aff998cc623ab0864a26e2eb1a31da8eb04e59f31fa80f02ec78eb225bcd","impliedFormat":1},{"version":"78d542989bdf7b6ba5410d5a884c0ab5ec54aa9ce46916d34267f885fcf65270","impliedFormat":1},{"version":"4d95060af2775a3a86db5ab47ca7a0ed146d1f6f13e71d96f7ac3b321718a832","impliedFormat":1},{"version":"6708cd298541a89c2abf66cceffc6c661f8ee31c013f98ddb58d2ec4407d0876","impliedFormat":1},{"version":"2e90928c29c445563409d89a834662c2ba6a660204fb3d4dc181914e77f8e29d","impliedFormat":1},{"version":"84be1b8b8011c2aab613901b83309d017d57f6e1c2450dfda11f7b107953286a","impliedFormat":1},{"version":"d7af890ef486b4734d206a66b215ebc09f6743b7fb2f3c79f2fb8716d1912d27","impliedFormat":1},{"version":"7e82c1d070c866eaf448ac7f820403d4e1b86112de582901178906317efc35ad","impliedFormat":1},{"version":"c5c4f547338457f4e8e2bec09f661af14ee6e157c7dc711ccca321ab476dbc6d","impliedFormat":1},{"version":"223e233cb645b44fa058320425293e68c5c00744920fc31f55f7df37b32f11ad","impliedFormat":1},{"version":"1394fe4da1ab8ab3ea2f2b0fcbfd7ccbb8f65f5581f98d10b037c91194141b03","impliedFormat":1},{"version":"086d9e59a579981bdf4f3bfa6e8e893570e5005f7219292bf7d90c153066cdfc","impliedFormat":1},{"version":"1ea59d0d71022de8ea1c98a3f88d452ad5701c7f85e74ddaa0b3b9a34ed0e81c","impliedFormat":1},{"version":"cd66a32437a555f7eb63490509a038d1122467f77fe7a114986186d156363215","impliedFormat":1},{"version":"f53d243499acfacc46e882bbf0bf1ae93ecea350e6c22066a062520b94055e47","impliedFormat":1},{"version":"65522e30a02d2720811b11b658c976bff99b553436d99bafd80944acba5b33b4","impliedFormat":1},{"version":"76b3244ec0b2f5b09b4ebf0c7419260813820f128d2b592b07ea59622038e45c","impliedFormat":1},{"version":"66eb7e876b49beff61e33f746f87b6e586382b49f3de21d54d41313aadb27ee6","impliedFormat":1},{"version":"69e8dc4b276b4d431f5517cd6507f209669691c9fb2f97933e7dbd5619fd07b7","impliedFormat":1},{"version":"361a647c06cec2e7437fa5d7cdf07a0dcce3247d93fbf3b6de1dc75139ff5700","impliedFormat":1},{"version":"fe5726291be816d0c89213057cd0c411bb9e39e315ed7e1987adc873f0e26856","impliedFormat":1},{"version":"1b76990de23762eb038e8d80b3f9c810974a7ed2335caa97262c5b752760f11a","impliedFormat":1},{"version":"5e050e05fe99cd06f2d4ad70e73aa4a72961d0df99525e9cad4a78fa588f387b","impliedFormat":1},{"version":"4ff327e8b16da9d54347b548f85675e35a1dc1076f2c22b2858e276771010dd2","impliedFormat":1},{"version":"f767787945b5c51c0c488f50b3b3aeb2804dfd2ddafcb61125d8d8857c339f5a","impliedFormat":1},{"version":"14ab21a9aeff5710d1d1262459a6d49fb42bed835aa0f4cfc36b75aa36faddcd","impliedFormat":1},{"version":"ba3c4682491b477c63716864a035b2cfdd727e64ec3a61f2ca0c9af3c0116cfd","affectsGlobalScope":true,"impliedFormat":1},{"version":"b222d32836d745e1e021bb10f6a0f4a562dd42206203060a8539a6b9f16523f0","impliedFormat":1},{"version":"a3f6d8995864820a0207b7ef4ce1ed6a8dd2fccc7e70d015da15034807c38e1c","impliedFormat":1},{"version":"651df11341eff0b769fb83af75b1872e6cedf406674c5eaa2650551aceb5a816","impliedFormat":1},{"version":"774a466295d26eddab911b9f567040364e7b7d0eb8003ad3bfc92b97eeecf066","signature":"f15b4a91c10bf30ff3708a5e3968c0a52ca4e86a4b9a5b1c4fc8e9b5f1292f21"},{"version":"1e9d4c3b066e0228765cef074a9bf49d0b8d3af461c97f2c511e8f7110b56235","signature":"e59faabf094dd75dcf08847ca1b8ae16daf269ca02c744c521b38ed5d297578c"},{"version":"48d3d3a869cf85c67c62d87c031946cb9ae89fc59d3d0b274d17c88097e9847d","signature":"16e6aa6706cf2bfdef5a587057b3b672099a2dd478584f26aacc4dd07336d0a0"},{"version":"a6a2173b6c4ebb031d158b06004ef4db7678ea8df66be27b317973460591c433","signature":"7bf23ce970d42624e8d08ff91d0a8dbe0063953e9038c2c75e827a0770da33cf"},{"version":"9dfe98d745dfc706198852e032c3c936092a4484951872c0d18639cf2ab698cf","signature":"326009db2b0a0f3290cbde2271f91e51869148a3e286ccc3ac41559f6c830642"},{"version":"9f01d110e167ddcfaa01b236040a12699b6ed7a2070d7c362f4403f092fed008","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"030523c514236148e0f25975ef74b2490c391cb9596cd668728f7db5b0702e08","signature":"411166ca21d7ae80b737bb1c7e9900e897ac7b27c06647063730ba8207522e46"},{"version":"ca52a606c31ddaae5f7c4055fccb3563cf7e85601c96dbf28b7d0795d2d21af0","signature":"82d82e5e9d2c282a1dddebbcc73d9ff5c89f60859cd8769d0f6eae7b76f3f4d1"},{"version":"41d8a2df75ff7b6ee4c82ef8ce52032aa272358a5f9187a6b5986849200ed411","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f25d11b3d781ff8db8614eeee12b43f81ad05389fb25acf10b8c3473b0e2a1d0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"78b3214dba4e86c41e4cdf5de5dcc853806ff74fad5df3dc1087cdbe19ad00e0","signature":"5c4de1b33c2b0c5b0b823fca5ba9e5dde3c995aa8a2c42db12b2e8b64f408805"},{"version":"9980dd11b1e848c16ad84eae18f4e7bfa3331f81c2617533b0e3bba61cb9ab3f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"45b82fd88270dd3d0aff39220b341eca789f77c82813dc6695b2058479d8dc28","signature":"3d67e5bdef7b1039301b51fc5ba5303c133b863f30e0de39c8aa1db5d897f1b6"},{"version":"915d1bc5f4c3b9cc8e25964dcd29b3c00ffb36ab2c8b12b2472533b3113485a9","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f212078a0ecefabbbbc627b89e28c871b559c1c57a4dcae32c8d7c2b6c4f0a00","signature":"b495b6770d46b4ebe3dbe0c1f6d64e8b50ea14ea8d2b63377813ae35672a4535"},{"version":"d237e0ff6755c658dc810e02b3a290f593f0ee2708eccbf1e2cd65c512c8de4a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cadbf01db107d2e9cceb5ecdd3f8f0084d996f07f2fca714409247deb26f0b85","signature":"ed905f29ff05cd1d3cf260949314ca896356f5c8bc011fdee9d75cde0404c6a0"},{"version":"a0563dec1ee31e5e8150681234665a122d20bb748d51a4b841ba1bcee31697a7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ea7a4796f350344bbf39b19518d78169bd66a819f787b0a4d1a6ad642dbce7a2","signature":"8f6ef9414b1d23dc0a927160ed27770f93c1fe749a9af0e626f5a0e8918d5fc1"},{"version":"428631c15a500daa23f33c9e1ca427423fe56515e811133ff5e80891238fb242","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cc5bfa36a8f86eea23a84b78f27ba0f842426cefb5a13d4b53b80a331aaaa268","signature":"3dcde6c75b6c7fc3bc7fca2ca1273467974a8b54061edd648ed854bc4c5d842a"},{"version":"fcc8fb0967eda9256921f3cfc521d49f3d473232d4c754c09de1d50faa996e35","signature":"37e053a2b54ed97cab1c0e0e6a0ed4d610d6011b24e4cef7633007fb573468e1"},{"version":"463efb47c0879fb7e518ca8d209ffc49e2d682d0c47eaf30d074a1032290ac50","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a794572bfb0885ec1de20b701e498a415c0480e4fb6c20dbaa125b374ae691cb","signature":"46d3c81ba82aaaeabab3baa813cbb84b4c9808ec7ad495ad5fdc27d9358aa314"},{"version":"dd26e8bac53b883eef657e5c7961170c27944f68825e82a43625c5ccc51c2937","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"016e4353e0414c98c2ed2eb75df1509a38b457ddf000e9ff98125f82ea42c335","signature":"767c0245753b6e027a6f0550eb7b69bbd09d3f758aa1e9a07dcfe0992c0f629e"},{"version":"3ef6579bdd5883302539b5370c5670331f3584dff9e2ef14bab39991f00a29cb","signature":"1e894cd9840e407527a3f67119371f90d7ae5f8179905de7776a345ed4af6d32"},{"version":"e34737b913a2f8775f45e495e660016e0ab0fda68c579f2024d1b596dde2d81c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6677681f753ead74430e87a3398cc92c183570992c77804fc4215178ffc86116","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3b642738f6cd019494d207949a63b67b6ad9013bb3f1fb5f8fa36687879db5c4","signature":"f583f64899de9c7804febba31f2808aad09101b0d3342e42793c8588ed608f63"},{"version":"35a644763f50424937b17109eb552ddc767270a5526a0a827810ab86585dc53a","signature":"1b4159a10366adf4fd777a6bd595b4b846f1d3d37d07498c3498fd4b71e0f813"},{"version":"12fc2285fa58f8178c3fce7cf56795d11bdbc3974e264f31ff3cdcb689fd5813","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c81045d6db59c1c83c84797bc1f78422a691ced9ca9a3554e2f8ef9aacc7dcfa","signature":"441f2bb4f78eb9fb86d27aceee4d92908b532110aa6b7fac80adede58bab926d"},{"version":"56d498db61c90d4706b23d1b2235e8e8fb4c525a16c19c33cabe50812506a134","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"bce05280900b4eb3333d10064531569c9fcfce6aa7aeca1ca03b2120539892a5","signature":"c96264be178c5e42597043c462b21cbc073618f43d5e1e88bd32a516068a2380"},{"version":"26f7e680445f38787829c77c194f3df7741657f8e80e7b51588dfe74da7b2c7f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f5a22523635ca6f47c20b386b010ca1258aa19af5f4299f8752809c599315bcf","signature":"48c3a71b54800c134fcff4becfedca8347ff86645e004f8754d1e2b1385d1e9c"},{"version":"487012655811883dddb922cd44d08642d753adc1df21ba652e11ac7030aeede2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4f69209ce0e934946c859c4cc6248ef4a2dc528f5baf9b4fcdea5cf3e08d9d38","signature":"e8f8aa08e63443a0cc63ce2f9fc9582addd622528fad2ecfc413b2a91c688fac"},{"version":"041b81f9c2cc3f95588f5ef2da13fce1d895ddd5160979c84c01aaa8873145b4","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ad15d915012090304670ff62dd5e48d7694c11787f4884f51cf80f873dd40aa4","signature":"ef3c092bb7ed970d2273e55a61b12bc4741bca2219e1fe703350550c99ca6f42"},{"version":"cad40fd88fb3c219fc234d0d56bf87e8d3ceb86505f11e3714fc21c6d761cd59","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a51d9c3485a1f3fc48f7f04f771cd827828082e49c12754d7e5708719675449e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fb5ce2f104fde415289b4febf27da970e40afefe42b87b15db28818ca9f94132","signature":"ccefe690307d556c5e44dd7ffb7d23faa0a3388926925ed0352107cd2efc4d9c"},{"version":"6e568ff38a9d7070783b26b39b93df658c7466032f4ffa22431dfe2808b7a8bd","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3613723853e81414c2e1220e3e0a696a997cc49e4390060b6ac7fd91aec150e4","signature":"0334a8b41901a52fc9195c66c97cee6aaaecc5aae79b9fc31f245a5df48b3ecc"},{"version":"e4bba7f9edd598a76e24e59f3d47d09cb0295d6c52c8a89e8fa04dc6e6428b16","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"082900437524e8bc6903f0d26479f590af1814cc5080fc502e07d38c951500be","signature":"24564adc2074d75d218bfe8b711580df8848afbd3ce2890184cd98e76529d387"},{"version":"34eb44fd7813a487540b6afa04da3175388b1841aa3612569e8aa07652363b69","signature":"9bada675e959a3571ad60f30eaee87c92b05fea94beca1f7fd212dc65734177a"},{"version":"a76367ab8ebbbfb0db994a3d394b71b72c3b72fb69542222ace317e4b61f74ca","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cbeab042293806df5cb4e45ba714036477971cb40a2539b20c331a632c1a2c46","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"0f55b5907842f16784dca83f2c82ac05e1e8740f2ec866c95289e05061384e4b","signature":"93bf045e7f996840ff1a3e1fa340836585224d394c52de4e98ff79f8be816dd1"},{"version":"7c8545a7c4ec6978ebb4af07475d76007eb084c4f7f45aeba7ee817eeb4316e1","signature":"37eeb4730a8634d70c51bd1933939e66cba31f30600246487f3568cee7742b9c"},{"version":"852c367110c2934ed35a33ad276111aa9b3016ec92b36b86a37b43642ad9458d","signature":"92a24950b269736d532ea9daf0cdd8ea7361b70095f3468e59afea80af884516"},{"version":"5a2cdf6adeec348bbc876221be4367e8adff0bb78a5680ebd7d71e5c3bad6cc0","impliedFormat":99},{"version":"e004826eac62081f867c66dabd92d3ef7d126d93a70430a2c88429228c3ecc50","impliedFormat":99},{"version":"38d6857b58d2ac42442e396311c542062d4f0dad40f2adb496dd5fd0756ee400","impliedFormat":99},{"version":"34b7d1e2d15845cf08bcf5e3c01adbb92cea1ec27564ee249ba486cdfb28526c","impliedFormat":99},{"version":"0d0861810ff9d344ab37f055edbc4d14e68e2fa18e113ce8cc33aef9bb500b5f","signature":"2791178671f71d9b41ed9ef814e549d85cc6a77c61e2f8a87ec25f2176d9a3fc"},{"version":"992404964e9cefb3143cde9bc8e5eb5e7010c3a207f5bfa52df49287fce758be","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"968043e1fd7bb3c6b1c22d3341ede621d16a628b2bc4c35d1e43f0064ff7c1a6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6d261c7be483027c9d281ba71314350ad8ce66efabf46f7b6099f39fe3b8d218","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c02d45c2a6bc50186c1972f44c31ff22e887b30205ce84607bb4661c0fdc9846","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7fc06e1e53688bb32ccd86e730e08bacd38d9ad7ad006a234211b230825efb2d","signature":"d95aac1823e54b4183acab8f7fe3bec5dd7bd4aa297f56004fbcb972d299e377"},{"version":"fe93c474ab38ac02e30e3af073412b4f92b740152cf3a751fdaee8cbea982341","impliedFormat":1},{"version":"3255b97f3f24af29c79cc1aa88004efb13b6285ebdde0a567bf32e19bb65250d","impliedFormat":1},{"version":"1e00b8bf9e3766c958218cd6144ffe08418286f89ff44ba5a2cc830c03dd22c7","impliedFormat":1},{"version":"cc0e0705b17f5987925bf05b5a7da622a76ad691274a428cf18fb28b33a7a1cf","signature":"01e6799210215286acf67be8c15da37b72af300e0f7f32c7f11535415e25ee88"},{"version":"ab640e52df6129fa178d5c0f2860542954ea38af4b0801a92c3ac09f6a9eec7c","signature":"91212f9905f489a1993df856acac1939544f6166e4cafff1c4f0949e37a8a11d"},{"version":"c3d577953f04c0188d8b9c63b2748b814efda6440336fa49557f0079f5cf748a","impliedFormat":1},{"version":"787fe950e18951b7970ec98cb05b3d0b11fcdfeb2091a7ea481ac9e52bf6c086","impliedFormat":1},{"version":"13ceda04874f09091da1994ba5f58bf1e9439af93336616257691863560b3f13","impliedFormat":1},{"version":"488c53c963104e91a6f2a1f16cbaee1a963f6f4527f0051256740c94ed34d6bb","signature":"fb69d502157f1cf71cb8c737f6909c2e82f2a53b8157f840411444435f5da3d1"},{"version":"7075686875dce9990810c2dfeefc1d3e1dd29cd815389854746fcc457dfbdce7","signature":"c2f4c6ab17d07762713d80c4c29cba3cfffd690fe6c569a17c0be5d0d3e810f5"},{"version":"a941595362ff7e12adee1605aea8495d9bc96cd833d95c87f83cad2b5838165a","signature":"b67fa3b5b051ead6f5048d73c953d289234953f832922ffc4dfe293d5c6bfc98"},{"version":"c98b1727a4c0ccfbd4df609bad278f1af184a069d232f978a327d53110677480","signature":"1556f3a35ddd259c925802c27bac4fe626e489e685fc3ee1f3101169f02f993d"},{"version":"1114a96ff6bdee7270e584688b4c46a5be6e50c47e6d8d26e4a8649556a851c9","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"76cc225f61f545122672c27ff69aa27d1e7578d653c5fe942ebe88601cea0b02","signature":"87d223b2d0fc4ffc6f3bd5bbf3d4e036171c472cffb6a792c31427b714f4f442"},{"version":"06272d55719e7d65de722274ae4593bfe06a90f4924b8807e4e04cbd15fb43c1","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"46b2d594365b3ffa714de5625c0859471e14d1b010e24cce18be153d1f6efa8d","signature":"3ebf64d5f0b695aef10ddeeff762fc0216e05bcd3d9572fb8763859b2d74be41"},{"version":"f3415880499901a01feef00e1b3042f670dabad8b5a131c22994f5f951dbdf2f","signature":"28007b7d2b577a868c587c22500f2ab77490b6390909ffbdd3b04dac98e69a18"},{"version":"37d5e316ab9ae4c7ff7e1856e2ecdf4930f17d7b43523f73843dbf8d6d13a43c","signature":"711d67575686fe3e0ac16b0a6080ab554fa53447a33228be88bfd57323da61bb"},{"version":"58c88a6bf756bcb45f70e3c79ac4e08209093ad4f112df86d57acc500290e067","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fc51205c27f22f7194f3c026cdf19c5b27f19190a97955b9ffce0db45858e42f","signature":"ba90586e9f08bbe0d660358dcee98b83dadce2bfa013ca3e8d93f8d7924a9c66"},{"version":"54c008f175512ea8e8854d138dcf76b2af5e59e6816e82e87a360d76f3c7f820","signature":"34c21c211ba158af8c7cbdf93784d24a472017b37b792b1c2d8ae21c36488729"},{"version":"1eb5c0da9dcb448145de4b74e1b37b6a05da0e4ae0b393c7224af6ee7cccf913","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"8bc24afa3c5fb73fd0dc89f091d2cb65b9d54f7a90b37302e9bd679ba504b0e3","signature":"e5ff90224997311a3ef066d1d0e3a85f3e2b2348a035581ff7099304497d0775"},{"version":"51610870e75caaf1ff890f1fb949366cd7d843b4aa2e734c166bb307a78f33ea","signature":"2ed4659f7cb57cc7471545251d21c6f8ab503526ec15c1a991a3d5be96258c79"},{"version":"de3c85bccb34f80e5cfd3f5e63648f2f8bdc8c9d19a67b0807adb4cfd8793afc","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c064058bfb6150ce094497c75bc491e7f92389c2b1fed5f6923e7a035315d3a1","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c3936ac555912c80003fd9e659007f016ae2bc61fb8b91f696f860d19fd6e9bc","signature":"dc035ffa274dd975386f883b4d99361db8e73edd0fc77b1e4bc0d09be0c5074d"},{"version":"f20b8d5c86e426ce2d1505f4a3114e66411272aa8394e0ad323c0c3b1d1fadff","signature":"b41f35e5ae414583d72cc2aa3a17cc23d40f4b9e221f92acd74ed09bb3daaa63"},{"version":"74b006e51c1fe0198db4fd239bdfb2063fb3b0139bbb3dbaab79f323f42ba6bf","signature":"83057fe16cf05e5bf626fd4e46379506199130438abbd2ae42de6234bb202181"},{"version":"c247b5bb6d297cb7c9607645b1486b2f7b3ee0c05fac97d18e6daf4e3b25ebeb","signature":"5a224c6b95c526ad3f70b7b5894a2fe9b50cb5a9eed53b87806cf1045f8d7bab"},{"version":"dd7a9804bfd52806479d69214126956478367bb3a2c333b589ddcc253aae03d3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"882b28abe64dae4932c83ebb71e4155da340929fe08a2055f3e573ef17f70fc3","impliedFormat":1},{"version":"4a3e425808751200a7709671667ad3d7e7cbfd0a06d469cab42adf06c2601f4a","impliedFormat":1},{"version":"401da46338f5b4f97c2a5f8a0faaace045c51aabd751d2dc704159f64feafe89","impliedFormat":1},{"version":"c705d4594093bcde53fc292c5526aedd3145170ceba73a9476ee97de6a915fe2","impliedFormat":1},{"version":"65399deec596f31712911c2d81964d913370e0d4a04c51df29cc3c99c9ac298b","signature":"c8b6a1356346524d07db6d395ed25c816fb0935840b3f6af9296402392feca76"},{"version":"b44403d97ecd48d2f5ec3f3175a9a9dce873ec5d3797459ed057e7a1ad597d54","signature":"1fcf7139261418de9dce0edd9f8e95a8ace6fd591da1c95fb959e19e7c6f4281"},{"version":"01922ef0992b637b2a096856708c280e6e2b5085d3ff743c27e891e0d3d28ea7","signature":"450f56af343ef42c693dde50c0dbe427f37297afb67c4864f81dd7c69fdfbd8b"},{"version":"02ed4b9c64b599d8a0d9c242c9f7e43fa44ebc4cdda1b8143a29d2bfdcaebb44","signature":"5bfa909232756aeaa1797184b579b7b47f5f6917a0fdf3b2566fe4bc4afc72a8"},{"version":"f4adb32677aa22d47ed1048448f8974667250c8deb8135f321cad0cb4d0d4007","signature":"be914a2abd74279a5ff3c561f641f30569d1a2f7618fcd806cfdb8c1fad34326"},{"version":"15a1cea3d3fd19c8818aaac408d84096485d3f154eac44e129c2a2a2609d85ba","signature":"88a64ff66b36ff55ce621b22e512515acd895e815a065f2897813b5f194521d8"},{"version":"a0e1a608868e8805852e4e9274fc1e3e22573273a292b4e59f19892c495fa239","signature":"0f601b1cf9ca46ef05b387bc05b169852e0a2e3e30babe89a59d21af43187522"},{"version":"4d1b4f2a7c556f22c71dfce1be2455614fb9f838d4b9144a447a03bd514bdd5e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"20375a205b37d0f527f1f3fb6cc5d6c2076c1b57f74b9024f8153e0f3f0289a9","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3557b3416d97219e58a39fecce338b086bd42db6ce7ef701e8265783fbd20c6f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"83f21e09f1a6d980cc7cf83252deccd5d997e67266ae8bd450ef8899fcab1884","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"38757383a22721ebcf7a7430d10cd39967c0f896d758798906d29c8ab8722924","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f4722121739886d9694fe6d74b91f654b2b26459edd6e275fb5ff1a509bbc262","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"1c46f9542d2d4a44df84e20e37348c86abfb57804268f5236874fa8a8b7639f5","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6915db003807400f9c80754166096b1ca5552111f80eb58c311ab561cca84735","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ad42288f8c9ebfd4451e4256a2f091cfc26b958d29db3612c19efbbb476882a2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},"d1986184a09a52db8228cb2bb2a61a8c05c9354e5b93cec8e2628d8579c892d7",{"version":"e37704e8bdf72de83d6ca4620f748ebf6272afc9b748f4e541afffd32b0c2924","affectsGlobalScope":true},{"version":"4332f611f915908b335662f95ac6047288d3bc0b939e06d59e369b96a28eaa70","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},"d1986184a09a52db8228cb2bb2a61a8c05c9354e5b93cec8e2628d8579c892d7",{"version":"ec9db470620906cec5c2b53d821e2917355bfed3fd87cea28eafd5d6d7496459","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"8d7cbeea0454e05a3cdf3370c5df267072c4f1dc6c48a45a9ad750d7890443d7","affectsGlobalScope":true,"impliedFormat":99}],"root":[[559,561],661,695,[746,799],[804,809],813,814,[818,840],[845,865]],"options":{"allowJs":false,"esModuleInterop":true,"jsx":4,"module":99,"skipLibCheck":true,"strict":true,"target":4},"referencedMap":[[864,1],[559,2],[865,3],[861,4],[862,2],[863,5],[560,6],[561,7],[403,2],[566,2],[619,2],[800,2],[801,8],[802,9],[803,10],[679,2],[676,2],[675,2],[670,11],[681,12],[666,13],[677,14],[669,15],[668,16],[678,2],[673,17],[680,2],[674,18],[667,2],[665,19],[664,20],[663,13],[683,21],[662,2],[626,22],[624,2],[161,23],[162,23],[163,24],[101,25],[164,26],[165,27],[166,28],[99,2],[167,29],[168,30],[169,31],[170,32],[171,33],[172,34],[173,34],[174,35],[175,36],[176,37],[177,38],[102,2],[100,2],[178,39],[179,40],[180,41],[220,42],[181,43],[182,44],[183,43],[184,45],[185,46],[186,47],[187,48],[188,48],[189,48],[190,49],[191,50],[192,51],[193,52],[194,53],[195,54],[196,54],[197,55],[198,2],[199,2],[200,56],[201,57],[202,56],[203,58],[204,59],[205,60],[206,61],[207,62],[208,63],[209,64],[210,65],[211,66],[212,67],[213,68],[214,69],[215,70],[216,71],[217,72],[103,43],[104,2],[105,73],[106,74],[107,2],[108,75],[109,2],[152,76],[153,77],[154,78],[155,78],[156,79],[157,2],[158,26],[159,80],[160,77],[218,81],[219,82],[224,83],[488,84],[225,85],[223,86],[490,87],[489,88],[682,84],[221,89],[486,2],[222,90],[90,2],[92,91],[485,84],[255,84],[615,92],[614,2],[627,93],[648,94],[649,95],[647,2],[620,2],[633,96],[632,97],[644,96],[635,98],[637,99],[656,99],[636,100],[617,101],[616,2],[622,102],[623,103],[653,104],[629,105],[631,106],[652,2],[650,105],[630,2],[621,103],[628,2],[625,2],[91,2],[691,107],[693,108],[692,109],[690,110],[689,2],[842,111],[841,2],[843,112],[729,113],[698,114],[708,114],[699,114],[709,114],[700,114],[701,114],[716,114],[715,114],[717,114],[718,114],[710,114],[702,114],[711,114],[703,114],[712,114],[704,114],[706,114],[714,115],[707,114],[713,115],[719,115],[705,114],[720,114],[725,114],[726,114],[721,114],[697,2],[727,2],[723,114],[722,114],[724,114],[728,114],[606,2],[608,116],[607,2],[696,117],[815,118],[735,119],[734,120],[741,121],[743,122],[739,123],[738,124],[742,120],[736,125],[733,126],[744,127],[745,127],[737,128],[731,2],[732,129],[817,130],[816,131],[740,2],[511,132],[516,133],[523,134],[506,135],[259,2],[267,136],[407,137],[410,138],[382,2],[395,139],[402,140],[284,2],[384,2],[265,2],[381,141],[427,142],[266,2],[257,143],[409,144],[411,145],[412,146],[483,147],[376,148],[329,149],[389,150],[390,151],[388,152],[387,2],[383,153],[408,154],[268,155],[453,2],[454,156],[295,157],[269,158],[296,157],[332,157],[235,157],[405,159],[404,2],[394,160],[501,2],[244,2],[522,161],[461,162],[462,163],[458,164],[540,2],[359,2],[463,165],[459,166],[545,167],[544,168],[539,2],[310,2],[362,169],[361,2],[538,170],[460,84],[315,171],[322,172],[324,173],[314,2],[319,174],[321,175],[323,176],[318,177],[316,2],[320,178],[541,2],[537,2],[543,179],[542,2],[313,180],[532,181],[535,182],[303,183],[302,184],[301,185],[548,84],[300,186],[289,2],[550,2],[811,187],[810,2],[551,84],[552,188],[227,2],[391,189],[392,190],[393,191],[231,2],[396,2],[251,192],[226,2],[475,84],[233,193],[474,194],[473,195],[464,2],[465,2],[472,2],[467,2],[470,196],[466,2],[468,197],[471,198],[469,197],[264,2],[261,2],[262,157],[416,2],[421,199],[422,200],[420,201],[418,202],[419,203],[414,2],[481,165],[256,165],[510,204],[517,205],[521,206],[350,207],[349,2],[344,2],[497,208],[505,209],[377,210],[378,211],[456,212],[366,2],[479,213],[354,84],[371,214],[482,215],[367,2],[370,216],[368,2],[480,217],[477,218],[476,2],[478,2],[374,2],[452,219],[239,220],[352,221],[356,222],[372,223],[375,224],[364,225],[357,226],[504,227],[430,228],[348,229],[236,230],[503,231],[232,232],[423,233],[415,2],[424,234],[441,235],[413,2],[440,236],[98,2],[435,237],[260,2],[455,238],[431,2],[245,2],[247,2],[386,2],[439,239],[263,2],[287,240],[373,241],[293,242],[353,2],[438,2],[417,2],[443,243],[444,244],[385,2],[446,245],[448,246],[447,247],[397,2],[437,230],[450,248],[347,249],[436,250],[442,251],[272,2],[276,2],[275,2],[274,2],[279,2],[273,2],[282,2],[281,2],[278,2],[277,2],[280,2],[283,252],[271,2],[339,253],[338,2],[343,254],[340,255],[342,256],[345,254],[341,255],[252,257],[331,258],[500,259],[498,2],[527,260],[529,261],[493,262],[528,263],[240,264],[237,264],[270,2],[254,265],[253,266],[249,267],[250,268],[258,269],[286,269],[297,269],[333,270],[298,270],[242,271],[241,2],[337,272],[336,273],[335,274],[334,275],[243,276],[484,277],[285,278],[492,279],[457,280],[487,281],[491,282],[380,283],[379,284],[360,285],[346,286],[328,287],[330,288],[327,289],[449,290],[351,2],[515,2],[248,291],[451,292],[499,293],[358,2],[288,294],[365,295],[363,296],[290,297],[425,298],[494,2],[291,299],[426,299],[513,2],[512,2],[514,2],[496,2],[495,2],[428,300],[355,2],[325,301],[246,302],[304,2],[230,303],[292,2],[519,84],[229,2],[531,304],[312,84],[525,165],[311,305],[508,306],[309,304],[234,2],[533,307],[307,84],[308,84],[299,2],[228,2],[306,308],[305,309],[294,310],[369,52],[429,52],[445,2],[433,311],[432,2],[317,180],[238,2],[326,84],[502,192],[509,312],[93,84],[96,313],[97,314],[94,84],[95,2],[406,74],[401,315],[400,2],[399,316],[398,2],[507,317],[518,318],[520,319],[524,320],[812,321],[526,322],[530,323],[558,324],[534,324],[557,325],[536,326],[546,327],[547,328],[549,329],[553,330],[556,192],[555,2],[554,331],[730,332],[602,333],[600,334],[601,335],[589,336],[590,334],[597,337],[588,338],[593,339],[603,2],[594,340],[599,341],[605,342],[604,343],[587,344],[595,345],[596,346],[591,347],[598,333],[592,348],[672,349],[671,2],[844,350],[611,351],[574,352],[575,353],[578,354],[567,355],[577,356],[573,357],[565,2],[579,358],[580,359],[568,2],[569,2],[571,360],[570,2],[572,361],[434,362],[586,2],[645,2],[618,2],[88,2],[89,2],[14,2],[15,2],[17,2],[16,2],[2,2],[18,2],[19,2],[20,2],[21,2],[22,2],[23,2],[24,2],[25,2],[3,2],[26,2],[27,2],[4,2],[28,2],[32,2],[29,2],[30,2],[31,2],[33,2],[34,2],[35,2],[5,2],[36,2],[37,2],[38,2],[39,2],[6,2],[43,2],[40,2],[41,2],[42,2],[44,2],[7,2],[45,2],[50,2],[51,2],[46,2],[47,2],[48,2],[49,2],[8,2],[55,2],[52,2],[53,2],[54,2],[56,2],[9,2],[57,2],[58,2],[59,2],[61,2],[60,2],[62,2],[63,2],[10,2],[64,2],[65,2],[66,2],[11,2],[67,2],[68,2],[69,2],[70,2],[71,2],[72,2],[12,2],[73,2],[74,2],[75,2],[76,2],[77,2],[1,2],[78,2],[79,2],[13,2],[80,2],[81,2],[82,2],[83,2],[84,2],[85,2],[86,2],[87,2],[128,363],[140,364],[125,365],[141,366],[150,367],[116,368],[117,369],[115,370],[149,331],[144,371],[148,372],[119,373],[137,374],[118,375],[147,376],[113,377],[114,371],[120,378],[121,2],[127,379],[124,378],[111,380],[151,381],[142,382],[131,383],[130,378],[132,384],[135,385],[129,386],[133,387],[145,331],[122,388],[123,389],[136,390],[112,366],[139,391],[138,378],[126,389],[134,392],[143,2],[110,2],[146,393],[563,394],[613,395],[582,396],[564,394],[562,2],[581,397],[612,2],[610,2],[583,2],[609,398],[576,399],[585,2],[584,400],[655,401],[660,402],[654,403],[646,404],[642,405],[638,406],[651,2],[639,98],[687,407],[684,408],[658,409],[657,410],[640,411],[686,412],[634,2],[641,413],[659,414],[694,415],[688,416],[866,417],[685,2],[643,2],[822,418],[824,419],[823,420],[825,421],[828,422],[827,423],[747,424],[751,425],[750,426],[754,427],[753,426],[755,428],[752,426],[757,429],[756,426],[759,430],[758,426],[761,431],[760,426],[763,432],[765,433],[764,426],[762,426],[768,434],[767,426],[773,435],[772,426],[774,436],[771,426],[770,437],[769,426],[776,426],[777,438],[775,426],[779,439],[778,426],[781,440],[780,426],[783,441],[782,426],[785,442],[784,426],[787,443],[786,426],[788,444],[766,426],[790,445],[789,426],[792,446],[791,447],[795,448],[794,426],[796,449],[793,426],[831,450],[830,451],[834,452],[833,453],[835,454],[832,453],[836,455],[814,456],[840,457],[839,458],[821,459],[849,460],[851,461],[850,462],[852,463],[853,464],[854,465],[855,466],[856,467],[819,468],[857,469],[858,470],[838,471],[837,472],[826,165],[859,473],[845,474],[846,475],[847,476],[848,477],[813,478],[829,479],[820,165],[818,480],[805,481],[806,482],[807,483],[860,484],[808,485],[749,486],[746,487],[798,488],[799,475],[748,489],[804,490],[797,475],[809,489],[661,491],[695,492]],"affectedFilesPendingEmit":[865,863,561,822,824,823,825,828,827,747,751,750,754,753,755,752,757,756,759,758,761,760,763,765,764,762,768,767,773,772,774,771,770,769,776,777,775,779,778,781,780,783,782,785,784,787,786,788,766,790,789,792,791,795,794,796,793,831,830,834,833,835,832,836,814,840,839,821,849,851,850,852,853,854,855,856,819,857,858,838,837,826,859,845,846,847,848,813,829,820,818,805,806,807,860,808,749,746,798,799,748,804,797,809,661,695],"version":"6.0.3"} \ No newline at end of file +{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2024.d.ts","./node_modules/typescript/lib/lib.es2025.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2024.collection.d.ts","./node_modules/typescript/lib/lib.es2024.object.d.ts","./node_modules/typescript/lib/lib.es2024.promise.d.ts","./node_modules/typescript/lib/lib.es2024.regexp.d.ts","./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2024.string.d.ts","./node_modules/typescript/lib/lib.es2025.collection.d.ts","./node_modules/typescript/lib/lib.es2025.float16.d.ts","./node_modules/typescript/lib/lib.es2025.intl.d.ts","./node_modules/typescript/lib/lib.es2025.iterator.d.ts","./node_modules/typescript/lib/lib.es2025.promise.d.ts","./node_modules/typescript/lib/lib.es2025.regexp.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.date.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.error.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","./node_modules/typescript/lib/lib.esnext.temporal.d.ts","./node_modules/typescript/lib/lib.esnext.typedarrays.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/react/global.d.ts","./node_modules/csstype/index.d.ts","./node_modules/@types/react/index.d.ts","./node_modules/next/dist/styled-jsx/types/css.d.ts","./node_modules/next/dist/styled-jsx/types/macro.d.ts","./node_modules/next/dist/styled-jsx/types/style.d.ts","./node_modules/next/dist/styled-jsx/types/global.d.ts","./node_modules/next/dist/styled-jsx/types/index.d.ts","./node_modules/next/dist/server/get-page-files.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/blob.d.ts","./node_modules/@types/node/web-globals/console.d.ts","./node_modules/@types/node/web-globals/crypto.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/encoding.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/utility.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client-stats.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/round-robin-pool.d.ts","./node_modules/undici-types/h2c-client.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-call-history.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/snapshot-agent.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/cache-interceptor.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/importmeta.d.ts","./node_modules/@types/node/web-globals/messaging.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/performance.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/web-globals/streams.d.ts","./node_modules/@types/node/web-globals/timers.d.ts","./node_modules/@types/node/web-globals/url.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/inspector/promises.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/path/posix.d.ts","./node_modules/@types/node/path/win32.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/quic.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/test/reporters.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/util/types.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/react/canary.d.ts","./node_modules/@types/react/experimental.d.ts","./node_modules/@types/react-dom/index.d.ts","./node_modules/@types/react-dom/canary.d.ts","./node_modules/@types/react-dom/experimental.d.ts","./node_modules/next/dist/lib/fallback.d.ts","./node_modules/next/dist/compiled/webpack/webpack.d.ts","./node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","./node_modules/next/dist/shared/lib/entry-constants.d.ts","./node_modules/next/dist/shared/lib/constants.d.ts","./node_modules/next/dist/lib/bundler.d.ts","./node_modules/next/dist/server/config.d.ts","./node_modules/next/dist/lib/load-custom-routes.d.ts","./node_modules/next/dist/shared/lib/image-config.d.ts","./node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","./node_modules/next/dist/server/body-streams.d.ts","./node_modules/next/dist/server/request/search-params.d.ts","./node_modules/next/dist/shared/lib/segment-cache/vary-params-decoding.d.ts","./node_modules/next/dist/server/app-render/vary-params.d.ts","./node_modules/next/dist/server/request/params.d.ts","./node_modules/next/dist/server/route-kind.d.ts","./node_modules/next/dist/server/route-definitions/route-definition.d.ts","./node_modules/next/dist/server/route-matches/route-match.d.ts","./node_modules/next/dist/client/components/app-router-headers.d.ts","./node_modules/next/dist/server/lib/cache-control.d.ts","./node_modules/next/dist/shared/lib/app-router-types.d.ts","./node_modules/next/dist/server/lib/cache-handlers/types.d.ts","./node_modules/next/dist/server/use-cache/use-cache-wrapper.d.ts","./node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","./node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","./node_modules/next/dist/lib/constants.d.ts","./node_modules/next/dist/server/render-result.d.ts","./node_modules/next/dist/server/response-cache/types.d.ts","./node_modules/next/dist/server/response-cache/index.d.ts","./node_modules/@types/react/jsx-runtime.d.ts","./node_modules/next/dist/next-devtools/userspace/pages/pages-dev-overlay-setup.d.ts","./node_modules/next/dist/build/static-paths/types.d.ts","./node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","./node_modules/next/dist/build/adapter/setup-node-env.external.d.ts","./node_modules/next/dist/server/instrumentation/types.d.ts","./node_modules/next/dist/lib/setup-exception-listeners.d.ts","./node_modules/next/dist/lib/worker.d.ts","./node_modules/next/dist/server/lib/experimental/ppr.d.ts","./node_modules/next/dist/lib/page-types.d.ts","./node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","./node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","./node_modules/next/dist/build/analysis/get-page-static-info.d.ts","./node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","./node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","./node_modules/next/dist/server/require-hook.d.ts","./node_modules/next/dist/server/node-polyfill-crypto.d.ts","./node_modules/next/dist/server/node-environment-baseline.d.ts","./node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","./node_modules/next/dist/server/node-environment-extensions/console-file.d.ts","./node_modules/next/dist/server/node-environment-extensions/console-exit.d.ts","./node_modules/next/dist/server/node-environment-extensions/console-dim.external.d.ts","./node_modules/next/dist/server/node-environment-extensions/unhandled-rejection.external.d.ts","./node_modules/next/dist/server/node-environment-extensions/random.d.ts","./node_modules/next/dist/server/node-environment-extensions/date.d.ts","./node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","./node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","./node_modules/next/dist/server/node-environment-extensions/fast-set-immediate.external.d.ts","./node_modules/next/dist/server/node-environment.d.ts","./node_modules/next/dist/build/page-extensions-type.d.ts","./node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","./node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","./node_modules/next/dist/server/lib/i18n-provider.d.ts","./node_modules/next/dist/server/web/next-url.d.ts","./node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","./node_modules/next/dist/server/web/spec-extension/cookies.d.ts","./node_modules/next/dist/server/web/spec-extension/request.d.ts","./node_modules/next/dist/shared/lib/deep-readonly.d.ts","./node_modules/next/dist/server/lib/incremental-cache/index.d.ts","./node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","./node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","./node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","./node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","./node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","./node_modules/next/dist/shared/lib/mitt.d.ts","./node_modules/next/dist/client/with-router.d.ts","./node_modules/next/dist/client/router.d.ts","./node_modules/next/dist/client/route-loader.d.ts","./node_modules/next/dist/client/page-loader.d.ts","./node_modules/next/dist/shared/lib/bloom-filter.d.ts","./node_modules/next/dist/shared/lib/router/router.d.ts","./node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","./node_modules/next/dist/client/components/readonly-url-search-params.d.ts","./node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","./node_modules/next/dist/client/flight-data-helpers.d.ts","./node_modules/next/dist/client/components/segment-cache/cache-key.d.ts","./node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","./node_modules/next/dist/client/components/segment-cache/types.d.ts","./node_modules/next/dist/shared/lib/segment-cache/segment-value-encoding.d.ts","./node_modules/next/dist/client/components/segment-cache/scheduler.d.ts","./node_modules/next/dist/client/components/segment-cache/cache-map.d.ts","./node_modules/next/dist/client/components/segment-cache/vary-path.d.ts","./node_modules/next/dist/client/components/segment-cache/cache.d.ts","./node_modules/next/dist/client/components/router-reducer/ppr-navigations.d.ts","./node_modules/next/dist/client/components/segment-cache/navigation.d.ts","./node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","./node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","./node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","./node_modules/next/dist/build/templates/pages.d.ts","./node_modules/next/dist/server/route-modules/pages/module.d.ts","./node_modules/next/dist/server/render.d.ts","./node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","./node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","./node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","./node_modules/next/dist/server/route-matchers/route-matcher.d.ts","./node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","./node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","./node_modules/next/dist/server/normalizers/normalizer.d.ts","./node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","./node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","./node_modules/next/dist/server/normalizers/request/suffix.d.ts","./node_modules/next/dist/server/normalizers/request/rsc.d.ts","./node_modules/next/dist/server/normalizers/request/next-data.d.ts","./node_modules/next/dist/server/after/builtin-request-context.d.ts","./node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.d.ts","./node_modules/next/dist/server/route-modules/pages/builtin/_error.d.ts","./node_modules/next/dist/server/load-default-error-components.d.ts","./node_modules/next/dist/server/base-server.d.ts","./node_modules/next/dist/server/after/after.d.ts","./node_modules/next/dist/server/after/after-context.d.ts","./node_modules/next/dist/server/use-cache/cache-life.d.ts","./node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","./node_modules/next/dist/server/lib/lazy-result.d.ts","./node_modules/next/dist/server/app-render/create-error-handler.d.ts","./node_modules/next/dist/shared/lib/action-revalidation-kind.d.ts","./node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","./node_modules/next/dist/server/async-storage/work-store.d.ts","./node_modules/next/dist/server/web/http.d.ts","./node_modules/next/dist/client/components/hooks-server-context.d.ts","./node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","./node_modules/next/dist/client/components/redirect-status-code.d.ts","./node_modules/next/dist/client/components/redirect-error.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","./node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","./node_modules/next/dist/server/app-render/cache-signal.d.ts","./node_modules/next/dist/server/app-render/instant-validation/boundary-tracking.d.ts","./node_modules/next/dist/server/app-render/instant-validation/instant-validation-error.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","./node_modules/next/dist/server/app-render/instant-validation/instant-samples.d.ts","./node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","./node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","./node_modules/next/dist/server/lib/implicit-tags.d.ts","./node_modules/next/dist/server/app-render/staged-rendering.d.ts","./node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","./node_modules/next/dist/build/templates/app-route.d.ts","./node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","./node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","./node_modules/next/dist/server/route-modules/app-route/module.d.ts","./node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","./node_modules/next/dist/build/segment-config/app/app-segments.d.ts","./node_modules/next/dist/build/get-supported-browsers.d.ts","./node_modules/next/dist/build/utils.d.ts","./node_modules/next/dist/build/rendering-mode.d.ts","./node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.d.ts","./node_modules/next/dist/server/lib/cpu-profile.d.ts","./node_modules/next/dist/build/turborepo-access-trace/types.d.ts","./node_modules/next/dist/build/turborepo-access-trace/result.d.ts","./node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","./node_modules/next/dist/build/turborepo-access-trace/index.d.ts","./node_modules/next/dist/export/routes/types.d.ts","./node_modules/next/dist/export/types.d.ts","./node_modules/next/dist/export/worker.d.ts","./node_modules/next/dist/build/worker.d.ts","./node_modules/next/dist/build/index.d.ts","./node_modules/next/dist/lib/coalesced-function.d.ts","./node_modules/next/dist/server/lib/router-utils/types.d.ts","./node_modules/next/dist/trace/types.d.ts","./node_modules/next/dist/trace/trace.d.ts","./node_modules/next/dist/trace/shared.d.ts","./node_modules/next/dist/trace/index.d.ts","./node_modules/next/dist/build/load-jsconfig.d.ts","./node_modules/@next/env/dist/index.d.ts","./node_modules/next/dist/build/webpack/plugins/telemetry-plugin/use-cache-tracker-utils.d.ts","./node_modules/next/dist/build/webpack/plugins/telemetry-plugin/telemetry-plugin.d.ts","./node_modules/next/dist/telemetry/storage.d.ts","./node_modules/next/dist/build/build-context.d.ts","./node_modules/next/dist/build/webpack-config.d.ts","./node_modules/next/dist/build/swc/generated-native.d.ts","./node_modules/next/dist/build/define-env.d.ts","./node_modules/next/dist/build/swc/index.d.ts","./node_modules/next/dist/build/swc/types.d.ts","./node_modules/next/dist/server/dev/parse-version-info.d.ts","./node_modules/next/dist/next-devtools/shared/types.d.ts","./node_modules/next/dist/server/dev/dev-indicator-server-state.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/cache-indicator.d.ts","./node_modules/next/dist/server/lib/parse-stack.d.ts","./node_modules/next/dist/next-devtools/server/shared.d.ts","./node_modules/next/dist/next-devtools/shared/stack-frame.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/utils/get-error-by-type.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/container/runtime-error/render-error.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/shared.d.ts","./node_modules/next/dist/server/dev/debug-channel.d.ts","./node_modules/next/dist/server/dev/hot-reloader-types.d.ts","./node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","./node_modules/next/dist/server/web/spec-extension/response.d.ts","./node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","./node_modules/next/dist/server/web/types.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","./node_modules/next/dist/server/base-http/node.d.ts","./node_modules/next/dist/server/lib/async-callback-set.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","./node_modules/sharp/lib/index.d.ts","./node_modules/next/dist/server/image-optimizer.d.ts","./node_modules/next/dist/server/next-server.d.ts","./node_modules/next/dist/server/lib/types.d.ts","./node_modules/next/dist/server/lib/lru-cache.d.ts","./node_modules/next/dist/server/lib/dev-bundler-service.d.ts","./node_modules/next/dist/server/dev/static-paths-worker.d.ts","./node_modules/next/dist/server/dev/next-dev-server.d.ts","./node_modules/next/dist/server/next.d.ts","./node_modules/next/dist/server/lib/render-server.d.ts","./node_modules/next/dist/server/lib/router-server.d.ts","./node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","./node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","./node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","./node_modules/next/dist/server/lib/router-utils/router-server-context.d.ts","./node_modules/next/dist/server/route-modules/route-module.d.ts","./node_modules/next/dist/server/load-components.d.ts","./node_modules/next/dist/server/web/adapter.d.ts","./node_modules/next/dist/server/app-render/types.d.ts","./node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","./node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","./node_modules/next/dist/server/lib/app-dir-module.d.ts","./node_modules/next/dist/server/app-render/app-render.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/client/components/error-boundary.d.ts","./node_modules/next/dist/client/components/layout-router.d.ts","./node_modules/next/dist/client/components/render-from-template-context.d.ts","./node_modules/next/dist/client/components/client-page.d.ts","./node_modules/next/dist/client/components/client-segment.d.ts","./node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","./node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","./node_modules/next/dist/lib/metadata/types/extra-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","./node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","./node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","./node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","./node_modules/next/dist/lib/metadata/types/resolvers.d.ts","./node_modules/next/dist/lib/metadata/types/icons.d.ts","./node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","./node_modules/next/dist/lib/metadata/metadata.d.ts","./node_modules/next/dist/lib/framework/boundary-components.d.ts","./node_modules/next/dist/server/app-render/rsc/preloads.d.ts","./node_modules/next/dist/server/app-render/rsc/postpone.d.ts","./node_modules/next/dist/server/app-render/rsc/taint.d.ts","./node_modules/next/dist/server/app-render/collect-segment-data.d.ts","./node_modules/next/dist/server/app-render/instant-validation/instant-validation.d.ts","./node_modules/next/dist/next-devtools/userspace/app/segment-explorer-node.d.ts","./node_modules/next/dist/server/app-render/entry-base.d.ts","./node_modules/next/dist/build/templates/app-page.d.ts","./node_modules/next/dist/server/route-modules/app-page/helpers/prerender-manifest-matcher.d.ts","./node_modules/@types/react/jsx-dev-runtime.d.ts","./node_modules/@types/react/compiler-runtime.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.d.ts","./node_modules/@types/react-dom/client.d.ts","./node_modules/@types/react-dom/static.d.ts","./node_modules/@types/react-dom/server.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/app-page/module.d.ts","./node_modules/next/dist/server/request/fallback-params.d.ts","./node_modules/next/dist/server/web/spec-extension/image-response.d.ts","./node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","./node_modules/next/dist/server/web/spec-extension/url-pattern.d.ts","./node_modules/next/dist/server/after/index.d.ts","./node_modules/next/dist/server/request/connection.d.ts","./node_modules/next/dist/server/web/exports/index.d.ts","./node_modules/next/dist/server/request-meta.d.ts","./node_modules/next/dist/cli/next-test.d.ts","./node_modules/next/dist/shared/lib/size-limit.d.ts","./node_modules/next/dist/server/config-shared.d.ts","./node_modules/next/dist/server/base-http/index.d.ts","./node_modules/next/dist/server/api-utils/index.d.ts","./node_modules/next/dist/build/adapter/build-complete.d.ts","./node_modules/next/dist/types.d.ts","./node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/utils.d.ts","./node_modules/next/dist/pages/_app.d.ts","./node_modules/next/app.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","./node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","./node_modules/next/dist/server/use-cache/cache-tag.d.ts","./node_modules/next/cache.d.ts","./node_modules/next/dist/pages/_document.d.ts","./node_modules/next/document.d.ts","./node_modules/next/dist/shared/lib/dynamic.d.ts","./node_modules/next/dynamic.d.ts","./node_modules/next/dist/pages/_error.d.ts","./node_modules/next/dist/client/components/catch-error.d.ts","./node_modules/next/dist/api/error.d.ts","./node_modules/next/error.d.ts","./node_modules/next/dist/shared/lib/head.d.ts","./node_modules/next/head.d.ts","./node_modules/next/dist/server/request/cookies.d.ts","./node_modules/next/dist/server/request/headers.d.ts","./node_modules/next/dist/server/request/draft-mode.d.ts","./node_modules/next/headers.d.ts","./node_modules/next/dist/shared/lib/get-img-props.d.ts","./node_modules/next/dist/client/image-component.d.ts","./node_modules/next/dist/shared/lib/image-external.d.ts","./node_modules/next/image.d.ts","./node_modules/next/dist/client/link.d.ts","./node_modules/next/link.d.ts","./node_modules/next/dist/client/components/unrecognized-action-error.d.ts","./node_modules/next/dist/client/components/redirect.d.ts","./node_modules/next/dist/client/components/not-found.d.ts","./node_modules/next/dist/client/components/forbidden.d.ts","./node_modules/next/dist/client/components/unauthorized.d.ts","./node_modules/next/dist/client/components/unstable-rethrow.server.d.ts","./node_modules/next/dist/client/components/unstable-rethrow.d.ts","./node_modules/next/dist/client/components/navigation.react-server.d.ts","./node_modules/next/dist/client/components/navigation.d.ts","./node_modules/next/navigation.d.ts","./node_modules/next/router.d.ts","./node_modules/next/dist/client/script.d.ts","./node_modules/next/script.d.ts","./node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","./node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","./node_modules/next/dist/compiled/@vercel/og/types.d.ts","./node_modules/next/server.d.ts","./node_modules/next/types/global.d.ts","./node_modules/next/types/compiled.d.ts","./node_modules/next/types.d.ts","./node_modules/next/index.d.ts","./node_modules/next/image-types/global.d.ts","./.next/dev/types/routes.d.ts","./next-env.d.ts","./next.config.ts","./node_modules/vite/types/hmrPayload.d.ts","./node_modules/vite/dist/node/chunks/moduleRunnerTransport.d.ts","./node_modules/vite/types/customEvent.d.ts","./node_modules/rolldown/dist/shared/logging-C6h4g8dA.d.mts","./node_modules/@oxc-project/types/types.d.ts","./node_modules/rolldown/dist/shared/binding-zH1vcmbM.d.mts","./node_modules/rolldown/node_modules/@rolldown/pluginutils/dist/filter/composable-filters.d.ts","./node_modules/rolldown/node_modules/@rolldown/pluginutils/dist/filter/filter-vite-plugins.d.ts","./node_modules/rolldown/node_modules/@rolldown/pluginutils/dist/filter/simple-filters.d.ts","./node_modules/rolldown/node_modules/@rolldown/pluginutils/dist/filter/index.d.ts","./node_modules/rolldown/node_modules/@rolldown/pluginutils/dist/index.d.ts","./node_modules/rolldown/dist/shared/define-config-5HJ1b9vG.d.mts","./node_modules/rolldown/dist/index.d.mts","./node_modules/rolldown/dist/parse-ast-index.d.mts","./node_modules/vite/types/internal/rollupTypeCompat.d.ts","./node_modules/rolldown/dist/shared/constructors-D0W3rNfA.d.mts","./node_modules/rolldown/dist/plugins-index.d.mts","./node_modules/rolldown/dist/shared/transform-DgZ3paSD.d.mts","./node_modules/rolldown/dist/utils-index.d.mts","./node_modules/vite/types/hot.d.ts","./node_modules/vite/dist/node/module-runner.d.ts","./node_modules/vite/types/internal/esbuildOptions.d.ts","./node_modules/vite/types/metadata.d.ts","./node_modules/vite/types/internal/terserOptions.d.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/postcss/lib/postcss.d.mts","./node_modules/lightningcss/node/ast.d.ts","./node_modules/lightningcss/node/targets.d.ts","./node_modules/lightningcss/node/index.d.ts","./node_modules/vite/types/internal/lightningcssOptions.d.ts","./node_modules/vite/types/internal/cssPreprocessorOptions.d.ts","./node_modules/rolldown/dist/filter-index.d.mts","./node_modules/vite/types/importGlob.d.ts","./node_modules/vite/dist/node/index.d.ts","./node_modules/@vitejs/plugin-react/types/optionalTypes.d.ts","./node_modules/@vitejs/plugin-react/dist/index.d.ts","./node_modules/@vitest/spy/optional-types.d.ts","./node_modules/@vitest/spy/dist/index.d.ts","./node_modules/tinyrainbow/dist/index.d.ts","./node_modules/@standard-schema/spec/dist/index.d.ts","./node_modules/@vitest/pretty-format/dist/index.d.ts","./node_modules/@vitest/utils/dist/types.d-BCElaP-c.d.ts","./node_modules/@vitest/utils/dist/diff.d.ts","./node_modules/@vitest/utils/dist/display.d.ts","./node_modules/@types/deep-eql/index.d.ts","./node_modules/assertion-error/index.d.ts","./node_modules/@types/chai/index.d.ts","./node_modules/@vitest/expect/dist/index.d.ts","./node_modules/@vitest/utils/dist/types.d.ts","./node_modules/@vitest/utils/dist/helpers.d.ts","./node_modules/@vitest/utils/dist/timers.d.ts","./node_modules/@vitest/utils/dist/index.d.ts","./node_modules/@vitest/runner/dist/tasks.d-Bh0IjN67.d.ts","./node_modules/@vitest/runner/dist/index.d.ts","./node_modules/vitest/dist/chunks/traces.d.D2T_R8rx.d.ts","./node_modules/@vitest/snapshot/dist/environment.d-DOJxxZV9.d.ts","./node_modules/@vitest/snapshot/dist/rawSnapshot.d-D_X3-62x.d.ts","./node_modules/@vitest/snapshot/dist/index.d.ts","./node_modules/vitest/dist/chunks/config.d.A1h_Y6Jt.d.ts","./node_modules/vitest/dist/chunks/environment.d.CrsxCzP1.d.ts","./node_modules/vitest/dist/chunks/rpc.d.B_8sPU0w.d.ts","./node_modules/vitest/dist/chunks/worker.d.ZpHpO4yb.d.ts","./node_modules/vitest/dist/chunks/browser.d.BcoexmFG.d.ts","./node_modules/vitest/optional-types.d.ts","./node_modules/@vitest/runner/dist/utils.d.ts","./node_modules/tinybench/dist/index.d.ts","./node_modules/vitest/dist/chunks/benchmark.d.DAaHLpsq.d.ts","./node_modules/@vitest/mocker/dist/types.d-BjI5eAwu.d.ts","./node_modules/@vitest/mocker/dist/index.d-B41z0AuW.d.ts","./node_modules/@vitest/mocker/dist/index.d.ts","./node_modules/@vitest/utils/dist/source-map.d.ts","./node_modules/vitest/dist/chunks/coverage.d.BZtK59WP.d.ts","./node_modules/@vitest/utils/dist/serialize.d.ts","./node_modules/@vitest/utils/dist/error.d.ts","./node_modules/vitest/dist/browser.d.ts","./node_modules/vitest/browser/context.d.ts","./node_modules/@vitest/snapshot/dist/manager.d.ts","./node_modules/vitest/dist/chunks/reporters.d.CEnv6XRv.d.ts","./node_modules/vitest/dist/chunks/plugin.d.BM2TCi12.d.ts","./node_modules/vitest/dist/config.d.ts","./node_modules/vitest/config.d.ts","./vitest.config.ts","./node_modules/@types/aria-query/index.d.ts","./node_modules/@testing-library/jest-dom/types/matchers.d.ts","./node_modules/@testing-library/jest-dom/types/jest.d.ts","./node_modules/@testing-library/jest-dom/types/index.d.ts","./node_modules/@testing-library/dom/types/matches.d.ts","./node_modules/@testing-library/dom/types/wait-for.d.ts","./node_modules/@testing-library/dom/types/query-helpers.d.ts","./node_modules/@testing-library/dom/types/queries.d.ts","./node_modules/@testing-library/dom/types/get-queries-for-element.d.ts","./node_modules/pretty-format/build/types.d.ts","./node_modules/pretty-format/build/index.d.ts","./node_modules/@testing-library/dom/types/screen.d.ts","./node_modules/@testing-library/dom/types/wait-for-element-to-be-removed.d.ts","./node_modules/@testing-library/dom/types/get-node-text.d.ts","./node_modules/@testing-library/dom/types/events.d.ts","./node_modules/@testing-library/dom/types/pretty-dom.d.ts","./node_modules/@testing-library/dom/types/role-helpers.d.ts","./node_modules/@testing-library/dom/types/config.d.ts","./node_modules/@testing-library/dom/types/suggestions.d.ts","./node_modules/@testing-library/dom/types/index.d.ts","./node_modules/@types/react-dom/test-utils/index.d.ts","./node_modules/@testing-library/react/types/index.d.ts","./node_modules/vitest/dist/chunks/global.d.DVsSRdQ5.d.ts","./node_modules/vitest/optional-runtime-types.d.ts","./node_modules/vitest/dist/chunks/suite.d.udJtyAgw.d.ts","./node_modules/vitest/dist/chunks/evaluatedModules.d.BxJ5omdx.d.ts","./node_modules/vitest/dist/runners.d.ts","./node_modules/expect-type/dist/utils.d.ts","./node_modules/expect-type/dist/overloads.d.ts","./node_modules/expect-type/dist/branding.d.ts","./node_modules/expect-type/dist/messages.d.ts","./node_modules/expect-type/dist/index.d.ts","./node_modules/vitest/dist/index.d.ts","./vitest.setup.ts","./node_modules/next-auth/adapters.d.ts","./node_modules/jose/dist/types/types.d.ts","./node_modules/jose/dist/types/jwe/compact/decrypt.d.ts","./node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts","./node_modules/jose/dist/types/jwe/general/decrypt.d.ts","./node_modules/jose/dist/types/jwe/general/encrypt.d.ts","./node_modules/jose/dist/types/jws/compact/verify.d.ts","./node_modules/jose/dist/types/jws/flattened/verify.d.ts","./node_modules/jose/dist/types/jws/general/verify.d.ts","./node_modules/jose/dist/types/jwt/verify.d.ts","./node_modules/jose/dist/types/jwt/decrypt.d.ts","./node_modules/jose/dist/types/jwt/produce.d.ts","./node_modules/jose/dist/types/jwe/compact/encrypt.d.ts","./node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts","./node_modules/jose/dist/types/jws/compact/sign.d.ts","./node_modules/jose/dist/types/jws/flattened/sign.d.ts","./node_modules/jose/dist/types/jws/general/sign.d.ts","./node_modules/jose/dist/types/jwt/sign.d.ts","./node_modules/jose/dist/types/jwt/encrypt.d.ts","./node_modules/jose/dist/types/jwk/thumbprint.d.ts","./node_modules/jose/dist/types/jwk/embedded.d.ts","./node_modules/jose/dist/types/jwks/local.d.ts","./node_modules/jose/dist/types/jwks/remote.d.ts","./node_modules/jose/dist/types/jwt/unsecured.d.ts","./node_modules/jose/dist/types/key/export.d.ts","./node_modules/jose/dist/types/key/import.d.ts","./node_modules/jose/dist/types/util/decode_protected_header.d.ts","./node_modules/jose/dist/types/util/decode_jwt.d.ts","./node_modules/jose/dist/types/util/errors.d.ts","./node_modules/jose/dist/types/key/generate_key_pair.d.ts","./node_modules/jose/dist/types/key/generate_secret.d.ts","./node_modules/jose/dist/types/util/base64url.d.ts","./node_modules/jose/dist/types/util/runtime.d.ts","./node_modules/jose/dist/types/index.d.ts","./node_modules/openid-client/types/index.d.ts","./node_modules/next-auth/providers/oauth-types.d.ts","./node_modules/next-auth/providers/oauth.d.ts","./node_modules/next-auth/providers/email.d.ts","./node_modules/next-auth/core/lib/cookie.d.ts","./node_modules/next-auth/core/index.d.ts","./node_modules/next-auth/providers/credentials.d.ts","./node_modules/next-auth/providers/index.d.ts","./node_modules/next-auth/jwt/types.d.ts","./node_modules/next-auth/jwt/index.d.ts","./node_modules/next-auth/utils/logger.d.ts","./node_modules/next-auth/core/types.d.ts","./node_modules/next-auth/next/index.d.ts","./node_modules/next-auth/index.d.ts","./node_modules/next-auth/providers/github.d.ts","./node_modules/next-auth/providers/google.d.ts","./src/lib/auth.ts","./src/app/api/auth/[...nextauth]/route.ts","./src/lib/types.ts","./src/lib/api.ts","./src/app/api/content-skills/route.ts","./src/app/api/content-skills/__tests__/route.test.ts","./src/app/api/entities/route.ts","./src/app/api/entities/[id]/route.ts","./src/app/api/entities/[id]/__tests__/route.test.ts","./src/app/api/entities/__tests__/route.test.ts","./src/app/api/entity-candidates/[id]/route.ts","./src/app/api/entity-candidates/[id]/__tests__/route.test.ts","./src/app/api/feedback/route.ts","./src/app/api/feedback/__tests__/route.test.ts","./src/app/api/invitations/[token]/accept/route.ts","./src/app/api/invitations/[token]/accept/__tests__/route.test.ts","./src/app/api/profile/route.ts","./src/app/api/profile/__tests__/route.test.ts","./src/app/api/profile/avatar/route.ts","./src/app/api/profile/avatar/__tests__/route.test.ts","./src/app/api/projects/route.ts","./src/app/api/projects/[id]/bluesky-credentials/route.ts","./src/app/api/projects/[id]/bluesky-credentials/__tests__/route.test.ts","./src/app/api/projects/[id]/intake/route.ts","./src/app/api/projects/[id]/intake/__tests__/route.test.ts","./src/app/api/projects/[id]/intake-allowlist/route.ts","./src/app/api/projects/[id]/intake-allowlist/[allowlistId]/route.ts","./src/app/api/projects/[id]/intake-allowlist/[allowlistId]/__tests__/route.test.ts","./src/app/api/projects/[id]/intake-allowlist/__tests__/route.test.ts","./src/app/api/projects/[id]/invitations/route.ts","./src/app/api/projects/[id]/invitations/[invitationId]/revoke/route.ts","./src/app/api/projects/[id]/invitations/__tests__/route.test.ts","./src/app/api/projects/[id]/mastodon-credentials/route.ts","./src/app/api/projects/[id]/mastodon-credentials/__tests__/route.test.ts","./src/app/api/projects/[id]/members/[membershipId]/route.ts","./src/app/api/projects/[id]/members/[membershipId]/__tests__/route.test.ts","./src/app/api/projects/[id]/rotate-intake-token/route.ts","./src/app/api/projects/[id]/rotate-intake-token/__tests__/route.test.ts","./src/app/api/projects/[id]/verify-bluesky-credentials/route.ts","./src/app/api/projects/[id]/verify-bluesky-credentials/__tests__/route.test.ts","./src/app/api/projects/[id]/verify-mastodon-credentials/route.ts","./src/app/api/projects/[id]/verify-mastodon-credentials/__tests__/route.test.ts","./src/app/api/projects/__tests__/route.test.ts","./src/app/api/review/[id]/route.ts","./src/app/api/review/[id]/__tests__/route.test.ts","./src/app/api/skills/[skillName]/route.ts","./src/app/api/skills/[skillName]/__tests__/route.test.ts","./src/app/api/source-configs/route.ts","./src/app/api/source-configs/[id]/route.ts","./src/app/api/source-configs/[id]/__tests__/route.test.ts","./src/app/api/source-configs/__tests__/route.test.ts","./src/lib/view-helpers.ts","./src/lib/dashboard-view.ts","./src/lib/profile.ts","./node_modules/@tanstack/query-core/build/modern/_tsup-dts-rollup.d.ts","./node_modules/@tanstack/query-core/build/modern/index.d.ts","./node_modules/@tanstack/react-query/build/modern/_tsup-dts-rollup.d.ts","./node_modules/@tanstack/react-query/build/modern/index.d.ts","./src/lib/useRole.ts","./src/lib/__tests__/api.test.ts","./src/lib/__tests__/auth.test.ts","./src/lib/__tests__/dashboard-view.test.ts","./src/lib/__tests__/view-helpers.test.ts","./test-support/server-only.ts","./node_modules/next/dist/compiled/@next/font/dist/types.d.ts","./node_modules/next/dist/compiled/@next/font/dist/google/index.d.ts","./node_modules/next/font/google/index.d.ts","./src/components/query-provider.tsx","./src/app/layout.tsx","./node_modules/next-auth/client/_utils.d.ts","./node_modules/next-auth/react/types.d.ts","./node_modules/next-auth/react/index.d.ts","./src/components/user-menu.tsx","./src/components/app-shell.tsx","./src/components/status-badge.tsx","./src/app/page.tsx","./src/app/__tests__/page.test.tsx","./src/app/admin/health/page.tsx","./src/app/admin/health/__tests__/page.test.tsx","./src/app/admin/projects/new/page.tsx","./src/components/copy-button.tsx","./src/app/admin/sources/page.tsx","./src/app/admin/sources/__tests__/page.test.tsx","./src/components/skill-action-bar.tsx","./src/app/content/[id]/page.tsx","./src/app/content/[id]/__tests__/page.test.tsx","./src/app/entities/page.tsx","./src/app/entities/[id]/page.tsx","./src/app/entities/[id]/__tests__/page.test.tsx","./src/app/entities/__tests__/page.test.tsx","./src/app/invite/[token]/page.tsx","./src/components/auth/social-auth-buttons.tsx","./src/components/auth/login-form.tsx","./src/app/login/page.tsx","./src/app/login/__tests__/page.test.tsx","./node_modules/file-selector/dist/file.d.ts","./node_modules/file-selector/dist/file-selector.d.ts","./node_modules/file-selector/dist/index.d.ts","./node_modules/react-dropzone/typings/react-dropzone.d.ts","./src/components/profile/avatar-dropzone.tsx","./src/components/profile/avatar-preview.tsx","./src/components/profile/profile-form.tsx","./src/components/profile/profile-settings-panel.tsx","./src/app/profile/page.tsx","./src/app/projects/[id]/members/page.tsx","./src/app/projects/[id]/members/invite/page.tsx","./src/components/__tests__/app-shell.test.tsx","./src/components/__tests__/query-provider.test.tsx","./src/components/__tests__/skill-action-bar.test.tsx","./src/components/__tests__/status-badge.test.tsx","./src/components/__tests__/user-menu.test.tsx","./src/components/auth/__tests__/login-form.test.tsx","./src/components/auth/__tests__/social-auth-buttons.test.tsx","./src/components/profile/__tests__/avatar-dropzone.test.tsx","./src/lib/__tests__/useRole.test.tsx","./.next/types/cache-life.d.ts","./.next/types/routes.d.ts","./.next/types/validator.ts","./.next/dev/types/cache-life.d.ts","./.next/dev/types/validator.ts","./node_modules/vitest/globals.d.ts"],"fileIdsList":[[101,164,172,176,179,181,182,183,195,512,513,514,515,861],[101,164,172,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,255,553,556,559,747,750,752,753,756,758,760,762,764,766,767,769,771,772,775,776,778,780,782,784,786,789,791,793,794,814,821,823,825,827,830,832,833,836,839,849,850,851,861,864],[101,164,172,176,179,181,182,183,195,512,513,514,515,864],[101,164,172,176,179,181,182,183,195,255,553,556,747,750,752,753,756,758,767,769,771,772,782,784,789,791,793,794,814,821,823,827,830,832,833,839,861,862,864],[101,164,172,176,179,181,182,183,195,557,558,559,861,864],[101,164,172,176,179,181,182,183,195,255,557,861,864],[101,164,172,176,179,181,182,183,195,800,861,864],[92,101,164,172,176,179,181,182,183,195,255,801,861,864],[101,164,172,176,179,181,182,183,195,802,861,864],[101,164,172,176,179,181,182,183,195,669,861,864],[101,164,172,176,179,181,182,183,195,666,667,668,669,670,673,674,675,676,677,678,679,680,861,864],[101,164,172,176,179,181,182,183,195,662,861,864],[101,164,172,176,179,181,182,183,195,672,861,864],[101,164,172,176,179,181,182,183,195,666,667,668,861,864],[101,164,172,176,179,181,182,183,195,666,667,861,864],[101,164,172,176,179,181,182,183,195,669,670,672,861,864],[101,164,172,176,179,181,182,183,195,667,861,864],[101,164,172,176,179,181,182,183,195,664,861,864],[101,164,172,176,179,181,182,183,195,663,861,864],[92,101,164,172,176,179,181,182,183,195,225,488,681,682,861,864],[101,164,172,176,179,181,182,183,195,624,625,861,864],[101,161,162,164,172,176,179,181,182,183,195,861,864],[101,163,164,172,176,179,181,182,183,195,861,864],[164,172,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,203,861,864],[101,164,165,170,172,175,176,179,181,182,183,185,195,200,212,861,864],[101,164,165,166,172,175,176,179,181,182,183,195,861,864],[101,164,167,172,176,179,181,182,183,195,213,861,864],[101,164,168,169,172,176,179,181,182,183,186,195,861,864],[101,164,169,172,176,179,181,182,183,195,200,209,861,864],[101,164,170,172,175,176,179,181,182,183,185,195,861,864],[101,163,164,171,172,176,179,181,182,183,195,861,864],[101,164,172,173,176,179,181,182,183,195,861,864],[101,164,172,174,175,176,179,181,182,183,195,861,864],[101,163,164,172,175,176,179,181,182,183,195,861,864],[101,164,172,175,176,177,179,181,182,183,195,200,212,861,864],[101,164,172,175,176,177,179,181,182,183,195,200,203,861,864],[101,151,164,172,175,176,178,179,181,182,183,185,195,200,212,861,864],[101,164,172,175,176,178,179,181,182,183,185,195,200,209,212,861,864],[101,164,172,176,178,179,180,181,182,183,195,200,209,212,861,864],[99,100,101,102,103,104,105,106,107,108,109,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,861,864],[101,164,172,175,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,183,195,861,864],[101,164,172,176,179,181,182,183,184,195,212,861,864],[101,164,172,175,176,179,181,182,183,185,195,200,861,864],[101,164,172,176,179,181,182,183,186,195,861,864],[101,164,172,176,179,181,182,183,187,195,861,864],[101,164,172,175,176,179,181,182,183,190,195,861,864],[101,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,861,864],[101,164,172,176,179,181,182,183,192,195,861,864],[101,164,172,176,179,181,182,183,193,195,861,864],[101,164,169,172,176,179,181,182,183,185,195,203,861,864],[101,164,172,175,176,179,181,182,183,195,196,861,864],[101,164,172,176,179,181,182,183,195,197,213,216,861,864],[101,164,172,175,176,179,181,182,183,195,200,202,203,861,864],[101,164,172,176,179,181,182,183,195,201,203,861,864],[101,164,172,176,179,181,182,183,195,203,213,861,864],[101,164,172,176,179,181,182,183,195,204,861,864],[101,161,164,172,176,179,181,182,183,195,200,206,212,861,864],[101,164,172,176,179,181,182,183,195,200,205,861,864],[101,164,172,175,176,179,181,182,183,195,207,208,861,864],[101,164,172,176,179,181,182,183,195,207,208,861,864],[101,164,169,172,176,179,181,182,183,185,195,200,209,861,864],[101,164,172,176,179,181,182,183,195,210,861,864],[101,164,172,176,179,181,182,183,185,195,211,861,864],[101,164,172,176,178,179,181,182,183,193,195,212,861,864],[101,164,172,176,179,181,182,183,195,213,214,861,864],[101,164,169,172,176,179,181,182,183,195,214,861,864],[101,164,172,176,179,181,182,183,195,200,215,861,864],[101,164,172,176,179,181,182,183,184,195,216,861,864],[101,164,172,176,179,181,182,183,195,217,861,864],[101,164,167,172,176,179,181,182,183,195,861,864],[101,164,169,172,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,213,861,864],[101,151,164,172,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,212,861,864],[101,164,172,176,179,181,182,183,195,218,861,864],[101,164,172,176,179,181,182,183,190,195,861,864],[101,164,172,176,179,181,182,183,195,208,861,864],[101,151,164,172,175,176,177,179,181,182,183,190,195,200,203,212,215,216,218,861,864],[101,164,172,176,179,181,182,183,195,200,219,861,864],[92,96,101,164,172,176,179,181,182,183,195,221,222,223,225,507,552,861,864],[92,101,164,172,176,179,181,182,183,195,861,864],[92,96,101,164,172,176,179,181,182,183,195,221,222,223,224,488,507,552,861,864],[92,96,101,164,172,176,179,181,182,183,195,221,222,224,225,507,552,861,864],[92,101,164,172,176,179,181,182,183,195,225,488,489,861,864],[92,101,164,172,176,179,181,182,183,195,225,488,861,864],[92,96,101,164,172,176,179,181,182,183,195,222,223,224,225,507,552,861,864],[92,96,101,164,172,176,179,181,182,183,195,221,223,224,225,507,552,861,864],[90,91,101,164,172,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,613,614,659,861,864],[101,164,172,176,179,181,182,183,195,617,618,619,622,623,626,861,864],[101,164,172,176,179,181,182,183,195,647,861,864],[101,164,172,176,179,181,182,183,195,647,648,861,864],[101,164,172,176,179,181,182,183,195,622,631,632,861,864],[101,164,172,176,179,181,182,183,195,622,631,861,864],[101,164,172,176,179,181,182,183,195,631,861,864],[101,164,172,176,179,181,182,183,195,620,631,635,636,861,864],[101,164,172,176,179,181,182,183,195,620,631,635,861,864],[101,164,172,176,179,181,182,183,195,616,861,864],[101,164,172,176,179,181,182,183,195,620,621,861,864],[101,164,172,176,179,181,182,183,195,620,861,864],[101,164,172,176,179,181,182,183,195,620,621,628,652,861,864],[101,164,172,176,179,181,182,183,195,628,861,864],[101,164,172,176,179,181,182,183,195,620,623,628,629,630,861,864],[101,164,172,176,179,181,182,183,195,689,690,861,864],[101,164,172,176,179,181,182,183,195,689,690,691,692,861,864],[101,164,172,176,179,181,182,183,195,689,691,861,864],[101,164,172,176,179,181,182,183,195,689,861,864],[101,164,172,176,179,181,182,183,195,841,861,864],[101,164,172,176,179,181,182,183,195,841,842,861,864],[101,164,172,176,179,181,182,183,195,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,861,864],[101,164,172,176,179,181,182,183,195,697,861,864],[101,164,172,176,179,181,182,183,195,697,707,861,864],[101,164,172,176,179,181,182,183,195,606,607,861,864],[101,164,172,176,179,181,182,183,195,743,861,864],[101,164,172,176,178,179,181,182,183,195,220,743,861,864],[101,164,172,176,179,181,182,183,195,734,741,861,864],[101,164,172,176,179,181,182,183,195,553,557,741,743,861,864],[101,164,172,176,179,181,182,183,195,696,730,737,739,740,861,864],[101,164,172,176,179,181,182,183,195,735,741,742,861,864],[101,164,172,176,179,181,182,183,195,553,557,738,743,861,864],[101,164,172,176,179,181,182,183,195,220,743,861,864],[101,164,172,176,179,181,182,183,195,735,737,743,861,864],[101,164,172,176,179,181,182,183,195,737,741,743,861,864],[101,164,172,176,179,181,182,183,195,737,861,864],[101,164,172,176,179,181,182,183,195,732,733,736,861,864],[101,164,172,176,179,181,182,183,195,729,730,731,737,743,861,864],[92,101,164,172,176,179,181,182,183,195,737,743,815,816,861,864],[92,101,164,172,176,179,181,182,183,195,737,743,861,864],[101,164,172,176,179,181,182,183,195,510,861,864],[101,164,172,176,179,181,182,183,195,512,513,514,515,861,864],[101,164,172,176,179,181,182,183,195,458,521,522,861,864],[101,164,172,176,179,181,182,183,195,230,231,233,245,269,384,395,503,861,864],[101,164,172,176,179,181,182,183,195,233,264,265,266,268,503,861,864],[101,164,172,176,179,181,182,183,195,233,401,403,405,406,408,503,505,861,864],[101,164,172,176,179,181,182,183,195,233,267,304,503,861,864],[101,164,172,176,179,181,182,183,195,231,233,244,245,251,257,262,383,384,385,394,503,505,861,864],[101,164,172,176,179,181,182,183,195,503,861,864],[101,164,172,176,179,181,182,183,195,240,246,265,285,380,861,864],[101,164,172,176,179,181,182,183,195,233,861,864],[101,164,172,176,179,181,182,183,195,226,240,246,861,864],[101,164,172,176,179,181,182,183,195,412,861,864],[101,164,172,176,179,181,182,183,195,409,410,412,861,864],[101,164,172,176,179,181,182,183,195,409,411,503,861,864],[101,164,172,176,178,179,181,182,183,195,285,482,500,861,864],[101,164,172,176,178,179,181,182,183,195,356,359,375,380,500,861,864],[101,164,172,176,178,179,181,182,183,195,328,500,861,864],[101,164,172,176,179,181,182,183,195,388,861,864],[101,164,172,176,179,181,182,183,195,387,388,389,861,864],[101,164,172,176,179,181,182,183,195,387,861,864],[98,101,164,172,176,178,179,181,182,183,195,226,233,245,251,257,263,265,269,270,283,284,351,381,382,395,503,507,861,864],[101,164,172,176,179,181,182,183,195,230,233,267,304,401,402,407,503,555,861,864],[101,164,172,176,179,181,182,183,195,267,555,861,864],[101,164,172,176,179,181,182,183,195,230,284,453,503,555,861,864],[101,164,172,176,179,181,182,183,195,555,861,864],[101,164,172,176,179,181,182,183,195,233,267,268,555,861,864],[101,164,172,176,179,181,182,183,195,404,555,861,864],[101,164,172,176,179,181,182,183,195,270,383,386,393,861,864],[92,101,164,172,176,179,181,182,183,195,458,861,864],[101,164,172,176,179,181,182,183,193,195,240,255,861,864],[101,164,172,176,179,181,182,183,195,240,255,861,864],[92,101,164,172,176,179,181,182,183,195,325,861,864],[92,101,164,172,176,179,181,182,183,195,255,861,864],[92,101,164,172,176,179,181,182,183,195,246,255,458,861,864],[101,164,172,176,179,181,182,183,195,240,311,325,326,537,544,861,864],[101,164,172,176,179,181,182,183,195,310,538,539,540,541,543,861,864],[101,164,172,176,179,181,182,183,195,361,861,864],[101,164,172,176,179,181,182,183,195,361,362,861,864],[101,164,172,176,179,181,182,183,195,244,246,313,314,861,864],[101,164,172,176,179,181,182,183,195,246,320,321,861,864],[101,164,172,176,179,181,182,183,195,246,315,323,861,864],[101,164,172,176,179,181,182,183,195,320,861,864],[101,164,172,176,179,181,182,183,195,238,246,313,314,315,316,317,318,319,320,323,861,864],[101,164,172,176,179,181,182,183,195,246,313,320,321,322,324,861,864],[101,164,172,176,179,181,182,183,195,246,314,316,317,861,864],[101,164,172,176,179,181,182,183,195,314,316,319,321,861,864],[101,164,172,176,179,181,182,183,195,542,861,864],[101,164,172,176,179,181,182,183,195,246,861,864],[92,101,164,172,176,179,181,182,183,195,234,531,861,864],[92,101,164,172,176,179,181,182,183,195,212,861,864],[92,101,164,172,176,179,181,182,183,195,267,302,861,864],[92,101,164,172,176,179,181,182,183,195,267,395,861,864],[101,164,172,176,179,181,182,183,195,300,305,861,864],[92,101,164,172,176,179,181,182,183,195,301,509,861,864],[101,164,172,176,179,181,182,183,195,810,861,864],[92,96,101,164,172,176,178,179,181,182,183,195,221,222,223,224,225,507,551,861,864],[101,164,172,176,178,179,181,182,183,195,246,861,864],[101,164,172,176,178,179,181,182,183,195,245,250,331,348,390,391,395,450,452,503,504,861,864],[101,164,172,176,179,181,182,183,195,283,392,861,864],[101,164,172,176,179,181,182,183,195,507,861,864],[101,164,172,176,179,181,182,183,195,232,861,864],[92,101,164,172,176,179,181,182,183,195,237,240,455,471,473,861,864],[101,164,172,176,179,181,182,183,193,195,240,455,470,471,472,554,861,864],[101,164,172,176,179,181,182,183,195,464,465,466,467,468,469,861,864],[101,164,172,176,179,181,182,183,195,466,861,864],[101,164,172,176,179,181,182,183,195,470,861,864],[101,164,172,176,179,181,182,183,195,255,419,420,422,861,864],[92,101,164,172,176,179,181,182,183,195,246,413,414,415,416,421,861,864],[101,164,172,176,179,181,182,183,195,419,421,861,864],[101,164,172,176,179,181,182,183,195,417,861,864],[101,164,172,176,179,181,182,183,195,418,861,864],[92,101,164,172,176,179,181,182,183,195,255,301,509,861,864],[92,101,164,172,176,179,181,182,183,195,255,508,509,861,864],[92,101,164,172,176,179,181,182,183,195,255,509,861,864],[101,164,172,176,179,181,182,183,195,348,349,861,864],[101,164,172,176,179,181,182,183,195,349,861,864],[101,164,172,176,178,179,181,182,183,195,504,509,861,864],[101,164,172,176,179,181,182,183,195,378,861,864],[101,163,164,172,176,179,181,182,183,195,377,861,864],[101,164,172,176,179,181,182,183,195,240,246,252,254,356,369,373,375,452,455,492,493,500,504,861,864],[101,164,172,176,179,181,182,183,195,246,295,317,861,864],[101,164,172,176,179,181,182,183,195,356,367,370,375,861,864],[92,101,164,172,176,179,181,182,183,195,237,240,356,359,375,378,412,459,460,461,462,463,474,475,476,477,478,479,480,481,555,861,864],[101,164,172,176,179,181,182,183,195,237,240,265,356,363,364,365,368,369,861,864],[101,164,172,176,179,181,182,183,195,200,246,265,367,374,455,456,500,861,864],[101,164,172,176,179,181,182,183,195,371,861,864],[101,164,172,176,178,179,181,182,183,193,195,234,246,250,260,292,293,296,348,351,416,450,451,492,503,504,505,507,555,861,864],[101,164,172,176,179,181,182,183,195,237,238,240,861,864],[101,164,172,176,179,181,182,183,195,356,861,864],[101,163,164,172,176,179,181,182,183,195,265,292,293,350,351,352,353,354,355,504,861,864],[101,164,172,176,179,181,182,183,195,375,861,864],[101,163,164,172,176,179,181,182,183,195,239,240,250,254,290,356,363,364,365,366,367,370,371,372,373,374,493,861,864],[101,164,172,176,178,179,181,182,183,195,290,291,363,504,505,861,864],[101,164,172,176,179,181,182,183,195,265,293,348,351,356,452,504,861,864],[101,164,172,176,178,179,181,182,183,195,503,505,861,864],[101,164,172,176,178,179,181,182,183,195,200,500,504,505,861,864],[101,164,172,176,178,179,181,182,183,193,195,226,240,245,252,254,257,260,267,287,292,293,294,295,296,331,332,334,337,339,342,343,344,345,347,395,450,452,500,503,504,505,861,864],[101,164,172,176,178,179,181,182,183,195,200,861,864],[101,164,172,176,179,181,182,183,195,233,234,235,263,500,501,502,507,509,555,861,864],[101,164,172,176,179,181,182,183,195,230,231,503,861,864],[101,164,172,176,179,181,182,183,195,424,861,864],[101,164,172,176,178,179,181,182,183,195,200,212,242,408,412,413,414,415,416,422,423,555,861,864],[101,164,172,176,179,181,182,183,193,195,212,226,240,242,254,257,293,332,337,347,348,401,428,429,430,436,439,440,450,452,500,503,861,864],[101,164,172,176,179,181,182,183,195,257,263,270,283,293,351,503,861,864],[101,164,172,176,178,179,181,182,183,195,212,234,245,254,293,434,500,503,861,864],[101,164,172,176,179,181,182,183,195,454,861,864],[101,164,172,176,178,179,181,182,183,195,424,437,438,447,861,864],[101,164,172,176,179,181,182,183,195,500,503,861,864],[101,164,172,176,179,181,182,183,195,353,493,861,864],[101,164,172,176,179,181,182,183,195,254,292,395,509,861,864],[101,164,172,176,178,179,181,182,183,193,195,232,337,397,401,430,436,439,442,500,861,864],[101,164,172,176,178,179,181,182,183,195,270,283,401,443,861,864],[101,164,172,176,179,181,182,183,195,233,294,395,445,503,505,861,864],[101,164,172,176,178,179,181,182,183,195,212,416,503,861,864],[101,164,172,176,178,179,181,182,183,195,267,294,395,396,397,406,424,444,446,503,861,864],[98,101,164,172,176,178,179,181,182,183,195,292,449,507,509,861,864],[101,164,172,176,179,181,182,183,195,346,450,861,864],[101,164,172,176,178,179,181,182,183,193,195,240,243,245,246,252,254,260,269,270,283,293,296,332,334,344,347,348,395,428,429,430,431,433,435,450,452,500,509,861,864],[101,164,172,176,178,179,181,182,183,195,200,270,436,441,447,500,861,864],[101,164,172,176,179,181,182,183,195,273,274,275,276,277,278,279,280,281,282,861,864],[101,164,172,176,179,181,182,183,195,287,338,861,864],[101,164,172,176,179,181,182,183,195,340,861,864],[101,164,172,176,179,181,182,183,195,338,861,864],[101,164,172,176,179,181,182,183,195,340,341,861,864],[101,164,172,176,178,179,181,182,183,195,244,245,246,250,251,504,861,864],[101,164,172,176,178,179,181,182,183,193,195,232,234,252,256,292,295,296,330,450,500,505,507,509,861,864],[101,164,172,176,178,179,181,182,183,193,195,212,236,243,244,254,256,293,448,493,499,504,861,864],[101,164,172,176,179,181,182,183,195,363,861,864],[101,164,172,176,179,181,182,183,195,364,861,864],[101,164,172,176,179,181,182,183,195,246,257,492,861,864],[101,164,172,176,179,181,182,183,195,365,861,864],[101,164,172,176,179,181,182,183,195,239,861,864],[101,164,172,176,179,181,182,183,195,241,253,861,864],[101,164,172,176,178,179,181,182,183,195,241,245,252,861,864],[101,164,172,176,179,181,182,183,195,248,253,861,864],[101,164,172,176,179,181,182,183,195,249,861,864],[101,164,172,176,179,181,182,183,195,241,242,861,864],[101,164,172,176,179,181,182,183,195,241,297,861,864],[101,164,172,176,179,181,182,183,195,241,861,864],[101,164,172,176,179,181,182,183,195,243,287,336,861,864],[101,164,172,176,179,181,182,183,195,335,861,864],[101,164,172,176,179,181,182,183,195,240,242,243,861,864],[101,164,172,176,179,181,182,183,195,243,333,861,864],[101,164,172,176,179,181,182,183,195,240,242,861,864],[101,164,172,176,179,181,182,183,195,292,395,861,864],[101,164,172,176,179,181,182,183,195,492,861,864],[101,164,172,176,178,179,181,182,183,195,212,252,254,258,292,395,449,452,455,456,457,483,484,487,491,493,500,504,861,864],[101,164,172,176,179,181,182,183,195,306,309,311,312,325,326,861,864],[92,101,164,172,176,179,181,182,183,195,223,225,255,485,486,861,864],[92,101,164,172,176,179,181,182,183,195,223,225,255,485,486,490,861,864],[101,164,172,176,179,181,182,183,195,379,861,864],[101,164,172,176,179,181,182,183,195,265,286,291,292,356,357,358,359,360,362,375,376,378,381,449,452,503,505,861,864],[101,164,172,176,179,181,182,183,195,325,861,864],[101,164,172,176,178,179,181,182,183,195,330,500,861,864],[101,164,172,176,179,181,182,183,195,330,861,864],[101,164,172,176,178,179,181,182,183,195,252,298,327,329,331,449,500,507,509,861,864],[101,164,172,176,179,181,182,183,195,306,307,308,309,311,312,325,326,508,861,864],[98,101,164,172,176,178,179,181,182,183,193,195,212,241,242,254,260,292,293,296,395,447,448,450,500,503,504,507,861,864],[101,164,172,176,179,181,182,183,195,237,240,247,861,864],[101,164,172,176,179,181,182,183,195,291,293,425,428,861,864],[101,164,172,176,179,181,182,183,195,291,426,494,495,496,497,498,861,864],[101,164,172,176,178,179,181,182,183,195,287,503,861,864],[101,164,172,176,178,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,290,375,861,864],[101,164,172,176,179,181,182,183,195,289,861,864],[101,164,172,176,179,181,182,183,195,291,344,861,864],[101,164,172,176,179,181,182,183,195,288,290,503,861,864],[101,164,172,176,178,179,181,182,183,195,236,291,425,426,427,500,503,504,861,864],[92,101,164,172,176,179,181,182,183,195,240,246,324,861,864],[92,101,164,172,176,179,181,182,183,195,238,861,864],[101,164,172,176,179,181,182,183,195,228,229,861,864],[92,101,164,172,176,179,181,182,183,195,234,861,864],[92,101,164,172,176,179,181,182,183,195,240,310,861,864],[92,98,101,164,172,176,179,181,182,183,195,292,296,507,509,861,864],[101,164,172,176,179,181,182,183,195,234,531,532,861,864],[92,101,164,172,176,179,181,182,183,195,305,861,864],[92,101,164,172,176,179,181,182,183,193,195,212,232,299,301,303,304,509,861,864],[101,164,172,176,179,181,182,183,195,240,267,504,861,864],[101,164,172,176,179,181,182,183,195,240,432,861,864],[92,101,164,172,176,178,179,181,182,183,193,195,230,232,305,403,507,508,861,864],[92,101,164,172,176,179,181,182,183,195,221,222,223,224,225,507,552,861,864],[92,93,94,95,96,101,164,172,176,179,181,182,183,195,861,864],[101,164,172,176,179,181,182,183,195,398,399,400,861,864],[101,164,172,176,179,181,182,183,195,398,861,864],[92,96,101,164,172,176,178,179,180,181,182,183,193,195,220,221,222,223,224,225,226,232,260,265,442,470,505,506,509,552,861,864],[101,164,172,176,179,181,182,183,195,517,861,864],[101,164,172,176,179,181,182,183,195,519,861,864],[101,164,172,176,179,181,182,183,195,523,861,864],[101,164,172,176,179,181,182,183,195,811,861,864],[101,164,172,176,179,181,182,183,195,525,861,864],[101,164,172,176,179,181,182,183,195,527,528,529,861,864],[101,164,172,176,179,181,182,183,195,533,861,864],[97,101,164,172,176,179,181,182,183,195,511,516,518,520,524,526,530,534,536,546,547,549,553,554,555,556,861,864],[101,164,172,176,179,181,182,183,195,535,861,864],[101,164,172,176,179,181,182,183,195,545,861,864],[101,164,172,176,179,181,182,183,195,301,861,864],[101,164,172,176,179,181,182,183,195,548,861,864],[101,163,164,172,176,179,181,182,183,195,291,425,426,428,494,495,497,498,550,552,861,864],[101,164,172,176,179,181,182,183,195,220,861,864],[101,164,169,172,176,178,179,180,181,182,183,195,212,213,220,729,861,864],[101,164,172,176,179,181,182,183,195,601,861,864],[101,164,172,176,179,181,182,183,195,599,601,861,864],[101,164,172,176,179,181,182,183,195,590,598,599,600,602,604,861,864],[101,164,172,176,179,181,182,183,195,588,861,864],[101,164,172,176,179,181,182,183,195,591,596,601,604,861,864],[101,164,172,176,179,181,182,183,195,587,604,861,864],[101,164,172,176,179,181,182,183,195,591,592,595,596,597,604,861,864],[101,164,172,176,179,181,182,183,195,591,592,593,595,596,604,861,864],[101,164,172,176,179,181,182,183,195,588,589,590,591,592,596,597,598,600,601,602,604,861,864],[101,164,172,176,179,181,182,183,195,604,861,864],[101,164,172,176,179,181,182,183,195,586,588,589,590,591,592,593,595,596,597,598,599,600,601,602,603,861,864],[101,164,172,176,179,181,182,183,195,586,604,861,864],[101,164,172,176,179,181,182,183,195,591,593,594,596,597,604,861,864],[101,164,172,176,179,181,182,183,195,595,604,861,864],[101,164,172,176,179,181,182,183,195,596,597,601,604,861,864],[101,164,172,176,179,181,182,183,195,589,599,861,864],[101,164,172,176,179,181,182,183,195,671,861,864],[92,101,164,172,176,179,181,182,183,195,843,861,864],[101,164,172,176,179,181,182,183,195,573,861,864],[101,164,172,176,179,181,182,183,195,565,567,573,861,864],[101,164,172,176,179,181,182,183,195,566,567,861,864],[101,164,172,176,179,181,182,183,195,567,573,577,861,864],[101,164,172,176,179,181,182,183,195,566,861,864],[101,164,172,176,179,181,182,183,195,567,573,861,864],[101,164,172,176,179,181,182,183,195,565,566,567,572,861,864],[101,164,172,176,179,181,182,183,195,565,567,861,864],[101,164,172,176,179,181,182,183,195,566,567,579,861,864],[101,164,172,176,179,181,182,183,195,568,569,570,861,864],[101,164,172,176,179,181,182,183,195,571,861,864],[101,164,172,176,179,181,182,183,195,200,220,861,864],[101,116,119,122,123,164,172,176,179,181,182,183,195,212,861,864],[101,119,164,172,176,179,181,182,183,195,200,212,861,864],[101,119,123,164,172,176,179,181,182,183,195,212,861,864],[101,164,172,176,179,181,182,183,195,200,861,864],[101,113,164,172,176,179,181,182,183,195,861,864],[101,117,164,172,176,179,181,182,183,195,861,864],[101,115,116,119,164,172,176,179,181,182,183,195,212,861,864],[101,164,172,176,179,181,182,183,185,195,209,861,864],[101,113,164,172,176,179,181,182,183,195,220,861,864],[101,115,119,164,172,176,179,181,182,183,185,195,212,861,864],[101,110,111,112,114,118,164,172,175,176,179,181,182,183,195,200,212,861,864],[101,119,128,136,164,172,176,179,181,182,183,195,861,864],[101,111,117,164,172,176,179,181,182,183,195,861,864],[101,119,145,146,164,172,176,179,181,182,183,195,861,864],[101,111,114,119,164,172,176,179,181,182,183,195,203,212,220,861,864],[101,119,164,172,176,179,181,182,183,195,861,864],[101,115,119,164,172,176,179,181,182,183,195,212,861,864],[101,110,164,172,176,179,181,182,183,195,861,864],[101,113,114,115,117,118,119,120,121,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,146,147,148,149,150,164,172,176,179,181,182,183,195,861,864],[101,119,138,141,164,172,176,179,181,182,183,195,861,864],[101,119,128,129,130,164,172,176,179,181,182,183,195,861,864],[101,117,119,129,131,164,172,176,179,181,182,183,195,861,864],[101,118,164,172,176,179,181,182,183,195,861,864],[101,111,113,119,164,172,176,179,181,182,183,195,861,864],[101,119,123,129,131,164,172,176,179,181,182,183,195,861,864],[101,123,164,172,176,179,181,182,183,195,861,864],[101,117,119,122,164,172,176,179,181,182,183,195,212,861,864],[101,111,115,119,128,164,172,176,179,181,182,183,195,861,864],[101,119,138,164,172,176,179,181,182,183,195,861,864],[101,131,164,172,176,179,181,182,183,195,861,864],[101,113,119,145,164,172,176,179,181,182,183,195,203,218,220,861,864],[101,164,172,176,179,181,182,183,195,562,861,864],[101,164,172,175,176,178,179,180,181,182,183,185,195,200,209,212,219,220,562,563,564,574,575,576,578,580,582,583,584,585,605,609,610,611,612,613,861,864],[101,164,172,176,179,181,182,183,195,562,563,564,581,861,864],[101,164,172,176,179,181,182,183,195,564,861,864],[101,164,172,176,179,181,182,183,195,608,861,864],[101,164,172,176,179,181,182,183,195,574,584,613,861,864],[101,164,172,176,179,181,182,183,195,574,613,861,864],[101,164,172,176,179,181,182,183,195,654,861,864],[101,164,172,176,179,181,182,183,195,627,659,684,861,864],[101,164,172,176,179,181,182,183,195,617,620,622,623,629,630,631,633,634,637,638,650,651,653,684,861,864],[101,164,172,176,179,181,182,183,195,633,644,645,684,861,864],[101,164,172,176,179,181,182,183,195,633,634,641,684,861,864],[101,164,172,176,179,181,182,183,195,620,622,633,634,637,684,861,864],[101,164,172,176,179,181,182,183,195,582,861,864],[101,164,172,176,179,181,182,183,195,620,627,633,634,637,646,684,861,864],[101,164,172,176,179,181,182,183,195,613,657,659,861,864],[101,164,167,172,176,179,181,182,183,195,200,613,620,622,627,631,633,634,637,638,641,642,643,646,649,650,651,655,656,659,684,861,864],[101,164,172,176,179,181,182,183,195,582,633,634,637,684,861,864],[101,164,172,176,179,181,182,183,195,633,644,645,646,684,861,864],[101,164,172,176,179,181,182,183,195,582,633,638,639,640,684,861,864],[101,164,167,172,176,179,181,182,183,195,200,582,613,620,622,627,631,633,634,637,638,639,640,641,642,643,644,645,646,649,650,651,655,656,657,658,659,684,861,864],[101,164,172,176,179,181,182,183,195,582,617,620,622,627,631,633,634,637,638,639,640,641,642,644,645,646,649,684,685,686,687,688,693,861,864],[101,164,172,176,179,181,182,183,195,620,622,633,634,637,638,644,645,646,684,686,861,864],[101,164,172,176,179,181,182,183,195,694,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,748,797,821,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,748,797,823,861,864],[101,164,172,176,179,181,182,183,195,255,536,748,749,797,819,820,861,864],[101,164,172,176,179,181,182,183,195,255,749,797,819,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,748,797,827,861,864],[101,164,172,176,179,181,182,183,195,255,748,749,797,819,820,826,861,864],[101,164,172,176,179,181,182,183,195,255,746,861,864],[101,164,172,176,179,181,182,183,195,255,694,748,749,750,861,864],[101,164,172,176,179,181,182,183,195,255,553,749,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,753,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,752,861,864],[101,164,172,176,179,181,182,183,195,255,694,748,749,756,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,758,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,760,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,762,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,764,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,767,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,772,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,771,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,769,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,775,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,778,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,780,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,782,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,784,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,786,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,766,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,789,861,864],[101,164,172,176,179,181,182,183,195,255,694,748,749,791,861,864],[101,164,172,176,179,181,182,183,195,255,553,748,749,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,794,861,864],[101,164,172,176,179,181,182,183,195,255,694,749,793,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,748,797,830,861,864],[101,164,172,176,179,181,182,183,195,255,536,749,797,819,820,829,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,748,797,833,861,864],[101,164,172,176,179,181,182,183,195,255,536,749,797,819,820,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,748,797,832,861,864],[101,164,172,176,179,181,182,183,195,255,536,743,746,749,797,861,864],[92,101,164,172,176,179,181,182,183,195,255,554,557,812,813,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,839,861,864],[101,164,172,176,179,181,182,183,195,255,838,861,864],[101,164,172,176,179,181,182,183,195,255,536,749,797,798,819,820,861,864],[101,164,172,176,179,181,182,183,195,255,749,797,819,848,861,864],[101,164,172,176,179,181,182,183,195,255,536,749,797,819,861,864],[101,164,172,176,179,181,182,183,195,255,536,749,797,819,826,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,748,819,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,803,813,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,813,829,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,820,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,813,818,861,864],[92,101,164,172,176,179,181,182,183,195,255,536,748,818,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,838,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,837,861,864],[92,101,164,172,176,179,181,182,183,195,255,536,546,817,837,861,864],[101,164,172,176,179,181,182,183,195,255,817,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,845,861,864],[92,101,164,172,176,179,181,182,183,195,255,844,861,864],[101,164,172,176,179,181,182,183,195,255,748,861,864],[92,101,164,172,176,179,181,182,183,195,255,748,861,864],[92,101,164,172,176,179,181,182,183,195,255,748,799,803,845,846,847,861,864],[92,101,164,172,176,179,181,182,183,195,255,803,861,864],[92,101,164,172,176,179,181,182,183,195,255,546,748,803,861,864],[92,101,164,172,176,179,181,182,183,195,255,536,799,803,817,861,864],[92,101,164,172,176,179,181,182,183,195,255,694,749,861,864],[101,164,172,176,179,181,182,183,195,255,694,746,861,864],[101,164,172,176,179,181,182,183,195,255,694,748,798,861,864],[101,164,172,176,179,181,182,183,195,255,683,694,748,803,804,861,864],[101,164,172,176,179,181,182,183,195,255,694,748,797,861,864],[92,101,164,172,176,179,181,182,183,195,255,554,743,746,748,861,864],[101,164,172,176,179,181,182,183,195,255,554,736,743,744,745,861,864],[101,164,172,176,179,181,182,183,195,255,748,797,861,864],[101,164,172,176,179,181,182,183,195,255,861,864],[101,164,172,176,179,181,182,183,195,255,748,803,861,864],[101,164,172,176,179,181,182,183,195,212,255,615,660,861,864],[92,101,164,172,176,179,181,182,183,195,255,683,694,861,864]],"fileInfos":[{"version":"bcd24271a113971ba9eb71ff8cb01bc6b0f872a85c23fdbe5d93065b375933cd","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f88bedbeb09c6f5a6645cb24c7c55f1aa22d19ae96c8e6959cbd8b85a707bc6","impliedFormat":1},{"version":"7fe93b39b810eadd916be8db880dd7f0f7012a5cc6ffb62de8f62a2117fa6f1f","impliedFormat":1},{"version":"bb0074cc08b84a2374af33d8bf044b80851ccc9e719a5e202eacf40db2c31600","impliedFormat":1},{"version":"1a7daebe4f45fb03d9ec53d60008fbf9ac45a697fdc89e4ce218bc94b94f94d6","impliedFormat":1},{"version":"f94b133a3cb14a288803be545ac2683e0d0ff6661bcd37e31aaaec54fc382aed","impliedFormat":1},{"version":"f59d0650799f8782fd74cf73c19223730c6d1b9198671b1c5b3a38e1188b5953","impliedFormat":1},{"version":"8a15b4607d9a499e2dbeed9ec0d3c0d7372c850b2d5f1fb259e8f6d41d468a84","impliedFormat":1},{"version":"26e0fe14baee4e127f4365d1ae0b276f400562e45e19e35fd2d4c296684715e6","impliedFormat":1},{"version":"1e9332c23e9a907175e0ffc6a49e236f97b48838cc8aec9ce7e4cec21e544b65","impliedFormat":1},{"version":"3753fbc1113dc511214802a2342280a8b284ab9094f6420e7aa171e868679f91","impliedFormat":1},{"version":"999ca32883495a866aa5737fe1babc764a469e4cde6ee6b136a4b9ae68853e4b","impliedFormat":1},{"version":"17f13ecb98cbc39243f2eee1f16d45cd8ec4706b03ee314f1915f1a8b42f6984","impliedFormat":1},{"version":"d6b1eba8496bdd0eed6fc8a685768fe01b2da4a0388b5fe7df558290bffcf32f","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f57fc4404ff020bc45b9c620aff2b40f700b95fe31164024c453a5e3c163c54","impliedFormat":1},{"version":"eadcffda2aa84802c73938e589b9e58248d74c59cb7fcbca6474e3435ac15504","affectsGlobalScope":true,"impliedFormat":1},{"version":"105ba8ff7ba746404fe1a2e189d1d3d2e0eb29a08c18dded791af02f29fb4711","affectsGlobalScope":true,"impliedFormat":1},{"version":"00343ca5b2e3d48fa5df1db6e32ea2a59afab09590274a6cccb1dbae82e60c7c","affectsGlobalScope":true,"impliedFormat":1},{"version":"ebd9f816d4002697cb2864bea1f0b70a103124e18a8cd9645eeccc09bdf80ab4","affectsGlobalScope":true,"impliedFormat":1},{"version":"2c1afac30a01772cd2a9a298a7ce7706b5892e447bb46bdbeef720f7b5da77ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"7b0225f483e4fa685625ebe43dd584bb7973bbd84e66a6ba7bbe175ee1048b4f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c0a4b8ac6ce74679c1da2b3795296f5896e31c38e888469a8e0f99dc3305de60","affectsGlobalScope":true,"impliedFormat":1},{"version":"3084a7b5f569088e0146533a00830e206565de65cae2239509168b11434cd84f","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5079c53f0f141a0698faa903e76cb41cd664e3efb01cc17a5c46ec2eb0bef42","affectsGlobalScope":true,"impliedFormat":1},{"version":"32cafbc484dea6b0ab62cf8473182bbcb23020d70845b406f80b7526f38ae862","affectsGlobalScope":true,"impliedFormat":1},{"version":"fca4cdcb6d6c5ef18a869003d02c9f0fd95df8cfaf6eb431cd3376bc034cad36","affectsGlobalScope":true,"impliedFormat":1},{"version":"b93ec88115de9a9dc1b602291b85baf825c85666bf25985cc5f698073892b467","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5c06dcc3fe849fcb297c247865a161f995cc29de7aa823afdd75aaaddc1419b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b77e16112127a4b169ef0b8c3a4d730edf459c5f25fe52d5e436a6919206c4d7","affectsGlobalScope":true,"impliedFormat":1},{"version":"fbffd9337146eff822c7c00acbb78b01ea7ea23987f6c961eba689349e744f8c","affectsGlobalScope":true,"impliedFormat":1},{"version":"a995c0e49b721312f74fdfb89e4ba29bd9824c770bbb4021d74d2bf560e4c6bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"c7b3542146734342e440a84b213384bfa188835537ddbda50d30766f0593aff9","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce6180fa19b1cccd07ee7f7dbb9a367ac19c0ed160573e4686425060b6df7f57","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f02e2476bccb9dbe21280d6090f0df17d2f66b74711489415a8aa4df73c9675","affectsGlobalScope":true,"impliedFormat":1},{"version":"45e3ab34c1c013c8ab2dc1ba4c80c780744b13b5676800ae2e3be27ae862c40c","affectsGlobalScope":true,"impliedFormat":1},{"version":"805c86f6cca8d7702a62a844856dbaa2a3fd2abef0536e65d48732441dde5b5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e42e397f1a5a77994f0185fd1466520691456c772d06bf843e5084ceb879a0ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"f4c2b41f90c95b1c532ecc874bd3c111865793b23aebcc1c3cbbabcd5d76ffb0","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab26191cfad5b66afa11b8bf935ef1cd88fabfcb28d30b2dfa6fad877d050332","affectsGlobalScope":true,"impliedFormat":1},{"version":"2088bc26531e38fb05eedac2951480db5309f6be3fa4a08d2221abb0f5b4200d","affectsGlobalScope":true,"impliedFormat":1},{"version":"cb9d366c425fea79716a8fb3af0d78e6b22ebbab3bd64d25063b42dc9f531c1e","affectsGlobalScope":true,"impliedFormat":1},{"version":"500934a8089c26d57ebdb688fc9757389bb6207a3c8f0674d68efa900d2abb34","affectsGlobalScope":true,"impliedFormat":1},{"version":"689da16f46e647cef0d64b0def88910e818a5877ca5379ede156ca3afb780ac3","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc21cc8b6fee4f4c2440d08035b7ea3c06b3511314c8bab6bef7a92de58a2593","affectsGlobalScope":true,"impliedFormat":1},{"version":"7ca53d13d2957003abb47922a71866ba7cb2068f8d154877c596d63c359fed25","affectsGlobalScope":true,"impliedFormat":1},{"version":"54725f8c4df3d900cb4dac84b64689ce29548da0b4e9b7c2de61d41c79293611","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5594bc3076ac29e6c1ebda77939bc4c8833de72f654b6e376862c0473199323","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f3eb332c2d73e729f3364fcc0c2b375e72a121e8157d25a82d67a138c83a95c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6f4427f9642ce8d500970e4e69d1397f64072ab73b97e476b4002a646ac743b1","affectsGlobalScope":true,"impliedFormat":1},{"version":"48915f327cd1dea4d7bd358d9dc7732f58f9e1626a29cc0c05c8c692419d9bb7","affectsGlobalScope":true,"impliedFormat":1},{"version":"b7bf9377723203b5a6a4b920164df22d56a43f593269ba6ae1fdc97774b68855","affectsGlobalScope":true,"impliedFormat":1},{"version":"db9709688f82c9e5f65a119c64d835f906efe5f559d08b11642d56eb85b79357","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b25b8c874acd1a4cf8444c3617e037d444d19080ac9f634b405583fd10ce1f7","affectsGlobalScope":true,"impliedFormat":1},{"version":"37be57d7c90cf1f8112ee2636a068d8fd181289f82b744160ec56a7dc158a9f5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a917a49ac94cd26b754ab84e113369a75d1a47a710661d7cd25e961cc797065f","affectsGlobalScope":true,"impliedFormat":1},{"version":"6d3261badeb7843d157ef3e6f5d1427d0eeb0af0cf9df84a62cfd29fd47ac86e","affectsGlobalScope":true,"impliedFormat":1},{"version":"195daca651dde22f2167ac0d0a05e215308119a3100f5e6268e8317d05a92526","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b11e4285cd2bb164a4dc09248bdec69e9842517db4ca47c1ba913011e44ff2f","affectsGlobalScope":true,"impliedFormat":1},{"version":"0508571a52475e245b02bc50fa1394065a0a3d05277fbf5120c3784b85651799","affectsGlobalScope":true,"impliedFormat":1},{"version":"8f9af488f510c3015af3cc8c267a9e9d96c4dd38a1fdff0e11dc5a544711415b","affectsGlobalScope":true,"impliedFormat":1},{"version":"fc611fea8d30ea72c6bbfb599c9b4d393ce22e2f5bfef2172534781e7d138104","affectsGlobalScope":true,"impliedFormat":1},{"version":"0bd714129fca875f7d4c477a1a392200b0bcd13fb2e80928cd334b63830ea047","affectsGlobalScope":true,"impliedFormat":1},{"version":"e2c9037ae6cd2c52d80ceef0b3c5ffdb488627d71529cf4f63776daf11161c9a","affectsGlobalScope":true,"impliedFormat":1},{"version":"135d5cf4d345f59f1a9caadfafcd858d3d9cc68290db616cc85797224448cccc","affectsGlobalScope":true,"impliedFormat":1},{"version":"bc238c3f81c2984751932b6aab223cd5b830e0ac6cad76389e5e9d2ffc03287d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4a07f9b76d361f572620927e5735b77d6d2101c23cdd94383eb5b706e7b36357","affectsGlobalScope":true,"impliedFormat":1},{"version":"7c4e8dc6ab834cc6baa0227e030606d29e3e8449a9f67cdf5605ea5493c4db29","affectsGlobalScope":true,"impliedFormat":1},{"version":"de7ba0fd02e06cd9a5bd4ab441ed0e122735786e67dde1e849cced1cd8b46b78","affectsGlobalScope":true,"impliedFormat":1},{"version":"6148e4e88d720a06855071c3db02069434142a8332cf9c182cda551adedf3156","affectsGlobalScope":true,"impliedFormat":1},{"version":"d63dba625b108316a40c95a4425f8d4294e0deeccfd6c7e59d819efa19e23409","affectsGlobalScope":true,"impliedFormat":1},{"version":"0568d6befee03dd435bed4fc25c4e46865b24bdcb8c563fdc21f580a2c301904","affectsGlobalScope":true,"impliedFormat":1},{"version":"30d62269b05b584741f19a5369852d5d34895aa2ac4fd948956f886d15f9cc0d","affectsGlobalScope":true,"impliedFormat":1},{"version":"f128dae7c44d8f35ee42e0a437000a57c9f06cc04f8b4fb42eebf44954d53dc8","affectsGlobalScope":true,"impliedFormat":1},{"version":"ffbe6d7b295306b2ba88030f65b74c107d8d99bdcf596ea99c62a02f606108b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"996fb27b15277369c68a4ba46ed138b4e9e839a02fb4ec756f7997629242fd9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"79b712591b270d4778c89706ca2cfc56ddb8c3f895840e477388f1710dc5eda9","affectsGlobalScope":true,"impliedFormat":1},{"version":"20884846cef428b992b9bd032e70a4ef88e349263f63aeddf04dda837a7dba26","affectsGlobalScope":true,"impliedFormat":1},{"version":"5fcab789c73a97cd43828ee3cc94a61264cf24d4c44472ce64ced0e0f148bdb2","affectsGlobalScope":true,"impliedFormat":1},{"version":"db59a81f070c1880ad645b2c0275022baa6a0c4f0acdc58d29d349c6efcf0903","affectsGlobalScope":true,"impliedFormat":1},{"version":"673294292640f5722b700e7d814e17aaf7d93f83a48a2c9b38f33cbc940ad8b0","affectsGlobalScope":true,"impliedFormat":1},{"version":"d786b48f934cbca483b3c6d0a798cb43bbb4ada283e76fb22c28e53ae05b9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ecb8e347cb6b2a8927c09b86263663289418df375f5e68e11a0ae683776978f","affectsGlobalScope":true,"impliedFormat":1},{"version":"142efd4ce210576f777dc34df121777be89eda476942d6d6663b03dcb53be3ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"379bc41580c2d774f82e828c70308f24a005b490c25ba34d679d84bcf05c3d9d","affectsGlobalScope":true,"impliedFormat":1},{"version":"ed484fb2aa8a1a23d0277056ec3336e0a0b52f9b8d6a961f338a642faf43235d","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ffedae1d1c2d53fdbca1c96d3c7dda544281f7d262f99b6880634f8fd8d9820","affectsGlobalScope":true,"impliedFormat":1},{"version":"83a730b125d477dd264df8ba479afab27a3dae7152b005c214ab94dc7ee44fd3","affectsGlobalScope":true,"impliedFormat":1},{"version":"1ce14b81c5cc821994aa8ec1d42b220dd41b27fcc06373bce3958af7421b77d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3a048b3e9302ef9a34ef4ebb9aecfb28b66abb3bce577206a79fee559c230da","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"dc0a7f107690ee5cd8afc8dbf05c4df78085471ce16bdd9881642ec738bc81fe","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"21da358700a3893281ce0c517a7a30cbd46be020d9f0c3f2834d0a8ad1f5fc75","impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"0ccdaa19852d25ecd84eec365c3bfa16e7859cadecf6e9ca6d0dbbbee439743f","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc2110f7decca6bfb9392e30421cfa1436479e4a6756e8fec6cbc22625d4f881","affectsGlobalScope":true,"impliedFormat":1},{"version":"096116f8fedc1765d5bd6ef360c257b4a9048e5415054b3bf3c41b07f8951b0b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5e01375c9e124a83b52ee4b3244ed1a4d214a6cfb54ac73e164a823a4a7860a","affectsGlobalScope":true,"impliedFormat":1},{"version":"f90ae2bbce1505e67f2f6502392e318f5714bae82d2d969185c4a6cecc8af2fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b58e207b93a8f1c88bbf2a95ddc686ac83962b13830fe8ad3f404ffc7051fb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fefabcb2b06736a66d2904074d56268753654805e829989a46a0161cd8412c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"c18a99f01eb788d849ad032b31cafd49de0b19e083fe775370834c5675d7df8e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5247874c2a23b9a62d178ae84f2db6a1d54e6c9a2e7e057e178cc5eea13757fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"074de5b2fdead0165a2757e3aaef20f27a6347b1c36adea27d51456795b37682","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"4137ebf04166f3a325f056aa56101adc75e9dceb30404a1844eb8604d89770e2","impliedFormat":1},{"version":"ccab02f3920fc75c01174c47fcf67882a11daf16baf9e81701d0a94636e94556","impliedFormat":1},{"version":"3e11fce78ad8c0e1d1db4ba5f0652285509be3acdd519529bc8fcef85f7dafd9","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"1cc2a09e1a61a5222d4174ab358a9f9de5e906afe79dbf7363d871a7edda3955","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"b64d4d1c5f877f9c666e98e833f0205edb9384acc46e98a1fef344f64d6aba44","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"c9381908473a1c92cb8c516b184e75f4d226dad95c3a85a5af35f670064d9a2f","impliedFormat":1},{"version":"c3f5289820990ab66b70c7fb5b63cb674001009ff84b13de40619619a9c8175f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3275d55fac10b799c9546804126239baf020d220136163f763b55a74e50e750","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa68a0a3b7cb32c00e39ee3cd31f8f15b80cac97dce51b6ee7fc14a1e8deb30b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c36e755bced82df7fb6ce8169265d0a7bb046ab4e2cb6d0da0cb72b22033e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7a93de4ff8a63bafe62ba86b89af1df0ccb5e40bb85b0c67d6bbcfdcf96bf3d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"90e85f9bc549dfe2b5749b45fe734144e96cd5d04b38eae244028794e142a77e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0a5deeb610b2a50a6350bd23df6490036a1773a8a71d70f2f9549ab009e67ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"d2ae155afe8a01cc0ae612d99117cf8ef16692ba7c4366590156fdec1bcf2d8c","impliedFormat":1},{"version":"3f5e5d9be35913db9fea42a63f3df0b7e3c8703b97670a2125587b4dbbd56d7c","impliedFormat":1},{"version":"8caeb65fdc3bfe0d13f86f67324fcb2d858ed1c55f1f0cce892eb1acfb9f3239","impliedFormat":1},{"version":"57c23df0b5f7a8e26363a3849b0bc7763f6b241207157c8e40089d1df4116f35","affectsGlobalScope":true,"impliedFormat":1},{"version":"3b8bc0c17b54081b0878673989216229e575d67a10874e84566a21025a2461ee","impliedFormat":1},{"version":"5b0db5a58b73498792a29bfebc333438e61906fef75da898b410e24e52229e6f","impliedFormat":1},{"version":"dbe055b2b29a7bab2c1ca8f259436306adb43f469dca7e639a02cd3695d3f621","impliedFormat":1},{"version":"1678b04557dca52feab73cc67610918a7f5e25bfdba3e7fa081acd625d93106d","impliedFormat":1},{"version":"e3905f6902f0b69e5eefc230daa69fdd4ab707a973ec2d086d65af1b3ea47ef0","impliedFormat":1},{"version":"2ea729503db9793f2691162fec3dd1118cab62e96d025f8eeb376d43ec293395","impliedFormat":1},{"version":"9ec87fea42b92894b0f209931a880789d43c3397d09dd99c631ae40a2f7071d1","impliedFormat":1},{"version":"c68e88cdfadfb6c8ba5fc38e58a3a166b0beae77b1f05b7d921150a32a5ffb8d","impliedFormat":1},{"version":"2bc7aa4fba46df0bd495425a7c8201437a7d465f83854fac859df2d67f664df3","impliedFormat":1},{"version":"41d17e1ad9a002feb11c8cdd2777e5bbc0cdb1e3f595d237e4dded0b6949983b","impliedFormat":1},{"version":"07e4e61e946a9c15045539ecd5f5d2d02e7aab6fa82567826857e09cf0f37c2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c4714ccc29149efb8777a1da0b04b8d2258f5d13ddbf4cd3c3d361fb531ac86","impliedFormat":1},{"version":"3ff275f84f89f8a7c0543da838f9da9614201abc4ce74c533029825adfb4433d","impliedFormat":1},{"version":"0eb5d0cbf09de5d34542b977fd6a933bb2e0817bffe8e1a541b2f1ad1b9af1ff","impliedFormat":1},{"version":"f9713757bcdfa4d58b48c0fb249e752c94a3eee8bf4532b906094246ac49ef88","impliedFormat":1},{"version":"2c2bdaa1d8ead9f68628d6d9d250e46ee8e81aa4898b4769a36956ae15e060fe","impliedFormat":1},{"version":"c32c840c62d8bd7aeb3147aa6754cd2d922b990a6b6634530cb2ebdce5adc8e9","impliedFormat":1},{"version":"e1c1a0b4d1ead0de9eca52203aeb1f771f21e6238d6fcd15aa56ac2a02f1b7bf","impliedFormat":1},{"version":"82b91e4e42e6c41bc7fc1b6c2dc5eba6a2ba98375eb1f210e6ff6bba2d54177e","impliedFormat":1},{"version":"6fe28249ac0c7bc19a79aa9264baf00efbd080e868dbe1d3052033ad1c64f206","affectsGlobalScope":true,"impliedFormat":1},{"version":"cbed824fec91efefc7bbdcb8b43d1a531fdbebd0e2ef19481501ff365a93cb70","impliedFormat":1},{"version":"d0716593b3f2b0451bcf0c24cfa86dec2235c325c89f201934248b7c742715fc","impliedFormat":1},{"version":"ec501101c2a96133a6c695f934c8f6642149cc728571b29cbb7b770984c1088e","impliedFormat":1},{"version":"b214ebcf76c51b115453f69729ee8aa7b7f8eccdae2a922b568a45c2d7ff52f7","impliedFormat":1},{"version":"429c9cdfa7d126255779efd7e6d9057ced2d69c81859bbab32073bad52e9ba76","impliedFormat":1},{"version":"2991bca2cc0f0628a278df2a2ccdb8d6cbcb700f3761abbed62bba137d5b1790","impliedFormat":1},{"version":"ce8653341224f8b45ff46d2a06f2cacb96f841f768a886c9d8dd8ec0878b11bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"230763250f20449fa7b3c9273e1967adb0023dc890d4be1553faca658ee65971","impliedFormat":1},{"version":"c3e9078b60cb329d1221f5878e88cecfa3e74460550e605a58fcfb41a66029ff","impliedFormat":1},{"version":"a74edb3bab7394a9dbde529d60632be590def2f5f01024dbd85441587fbfbbe0","impliedFormat":1},{"version":"0ea59f7d3e51440baa64f429253759b106cfcbaf51e474cae606e02265b37cf8","impliedFormat":1},{"version":"bc18a1991ba681f03e13285fa1d7b99b03b67ee671b7bc936254467177543890","impliedFormat":1},{"version":"00049ccc87f3f37726db03c01ca68fe74fd9c0109b68c29eb9923ebec2c76b13","impliedFormat":1},{"version":"fa94bbf532b7af8f394b95fa310980d6e20bd2d4c871c6a6cb9f70f03750a44b","impliedFormat":1},{"version":"68d3f35108e2608b1f2f28b36d19d7055f31c4465cc5692cbd06c716a9fe7973","impliedFormat":1},{"version":"a6d543044570fbeed13a7f9925a868081cd2b14ef59cdd9da6ae76d41cab03d3","affectsGlobalScope":true,"impliedFormat":1},{"version":"7fa2214bb0d64701bc6f9ce8cde2fd2ff8c571e0b23065fa04a8a5a6beb91511","impliedFormat":1},{"version":"f1c93e046fb3d9b7f8249629f4b63dc068dd839b824dd0aa39a5e68476dc9420","impliedFormat":1},{"version":"eab2f3179607acb3d44b2db2a76dd7d621c5039b145dc160a1ee733963f9d2f5","impliedFormat":1},{"version":"841983e39bd4cbb463be385e92fda11057cab368bf27100a801c492f1d86cbaa","impliedFormat":1},{"version":"6f5383b3df1cdf4ff1aa7fb0850f77042b5786b5e65ec9a9b6be56ebfe4d9036","impliedFormat":1},{"version":"62fc21ed9ccbd83bd1166de277a4b5daaa8d15b5fa614c75610d20f3b73fba87","impliedFormat":1},{"version":"e4156ddb25aa0e3b5303d372f26957b36778f0f6bbd4326359269873295e3058","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc1b433a84cae05ddc5672d4823170af78606ad21ecef60dbc4570190cbf1357","impliedFormat":1},{"version":"9d3821bc75c59577e52643324cec92fc2145642e8d17cf7ee07a3181f21d985d","impliedFormat":1},{"version":"7f78cfb2b343838612c192cb251746e3a7c62ac7675726a47e130d9b213f6580","impliedFormat":1},{"version":"201db9cf1687fab1adf5282fcba861f382b32303dc4f67c89d59655e78a25461","impliedFormat":1},{"version":"c77fb31bc17fd241d3922a9f88c59e3361cdf76d1328ba9412fc6bf7310b638d","impliedFormat":1},{"version":"0a20eaf2e4b1e3c1e1f87f7bccb0c936375b23b022baeea750519b7c9bc6ce83","impliedFormat":1},{"version":"b484ec11ba00e3a2235562a41898d55372ccabe607986c6fa4f4aba72093749f","impliedFormat":1},{"version":"a16b91b27bd6b706c687c88cbc8a7d4ee98e5ed6043026d6b84bda923c0aed67","impliedFormat":1},{"version":"694b812e0ed11285e8822cf8131e3ce7083a500b3b1d185fff9ed1089677bd0a","impliedFormat":1},{"version":"99ab6d0d660ce4d21efb52288a39fd35bb3f556980ec5463b1ae8f304a3bbc85","impliedFormat":1},{"version":"6eeded8c7e352be6e0efb83f4935ec752513c4d22043b52522b90849a49a3a11","impliedFormat":1},{"version":"6c1ad90050ffbb151cacc68e2d06ea1a26a945659391e32651f5d42b86fd7f2c","impliedFormat":1},{"version":"55cdbeebe76a1fa18bbd7e7bf73350a2173926bd3085bb050cf5a5397025ee4e","impliedFormat":1},{"version":"2beff543f6e9a9701df88daeee3cdd70a34b4a1c11cb4c734472195a5cb2af54","impliedFormat":1},{"version":"2e07abf27aa06353d46f4448c0bbac73431f6065eef7113128a5cd804d0c384d","impliedFormat":1},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1},{"version":"42bc0e1a903408137c3df2b06dfd7e402cdab5bbfa5fcfb871b22ebfdb30bd0b","impliedFormat":1},{"version":"9894dafe342b976d251aac58e616ac6df8db91fb9d98934ff9dd103e9e82578f","impliedFormat":1},{"version":"413df52d4ea14472c2fa5bee62f7a40abd1eb49be0b9722ee01ee4e52e63beb2","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"446a50749b24d14deac6f8843e057a6355dd6437d1fac4f9e5ce4a5071f34bff","impliedFormat":1},{"version":"182e9fcbe08ac7c012e0a6e2b5798b4352470be29a64fdc114d23c2bab7d5106","impliedFormat":1},{"version":"2f4e6b4d39426a1b85ecf4bdeb9dddbf4d9b3397d95d8555d46f925c9519ec7d","impliedFormat":1},{"version":"78a2869ad0cbf3f9045dda08c0d4562b7e1b2bfe07b19e0db072f5c3c56e9584","impliedFormat":1},{"version":"89d5d28d4f57e000b836ac273079be1b75710e28ce14750d081fb420d37e2ca5","impliedFormat":1},{"version":"fd4e24ccff3966390600d7f5d6aa1fed5a512e92ada735ea5fbc933d313ad3d3","impliedFormat":1},{"version":"b7cddfe1aa6b86b5fad3c9ccb30d05b3ccb165aebbf112f48d2d8a5f69dd98b1","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"ad0d1d75d129b1c80f911be438d6b61bfa8703930a8ff2be2f0e1f8a91841c64","impliedFormat":1},{"version":"bd2c7ada3dee03653d3f601011d30072194bc3970cd93208f9588fbdc0c69347","impliedFormat":1},{"version":"e480da45d32313e7174b265674da504f075f59ef326852f0c5a5d863b438ae85","impliedFormat":1},{"version":"ad54850f61fcf5d014e11be80d2f46fea9265cfa7e77456da876f7833ef81769","impliedFormat":1},{"version":"6f7c9e8bd2b5b6a080b07080065f94900bd3c7e5ebbd3047bc33fcce2fab1dd8","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"da5950ee2a90721df6f3fba45f5d05308f7e4c35835392215dd2cd404505e2de","impliedFormat":1},{"version":"ce75b1aebb33d510ff28af960a9221410a3eaf7f18fc5f21f9404075fba77256","impliedFormat":1},{"version":"f42d5fed19610d485c646a0c430e768115567d078c7fc855c57b0c578b3d6cd3","impliedFormat":1},{"version":"ee8df1cb8d0faaca4013a1b442e99130769ce06f438d18d510fed95890067563","impliedFormat":1},{"version":"d5630f2ad9b4541e5ce891648121022f9412ecdca1820baa1f0104f70fd7eff7","impliedFormat":1},{"version":"4d15375ab13497104bc8fe56fdef2b5fd6853f29255737d23a33fa306ff7fd69","impliedFormat":1},{"version":"2cd3fc1d0d6a1e85baffd2d4f50f5efb192b5446eef567e97c94765402f0aad4","impliedFormat":1},{"version":"e4cbf2f1e89ecccaddd2c045e600ae41b732295953fb06247c7dcbc2d281ed30","impliedFormat":1},{"version":"6dcedaef57dff0d79a05ab0ab602cde74db803d1e765468bf91263786a383e1b","impliedFormat":1},{"version":"8c1697d90c394a6fd955b98eae01238eff628e129b987a68aea10f898a48e7da","impliedFormat":1},{"version":"7580e62139cb2b44a0270c8d01abcbfcba2819a02514a527342447fa69b34ef1","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"f374cb24e93e7798c4d9e83ff872fa52d2cdb36306392b840a6ddf46cb925cb6","impliedFormat":1},{"version":"d10d63718e1646c2279e3b33831f82c60e31f622b2b7020f1196409ca4c09242","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"148679c6d0f449210a96e7d2e562d589e56fcde87f843a92808b3ff103f1a774","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"02436d7e9ead85e09a2f8e27d5f47d9464bced31738dec138ca735390815c9f0","impliedFormat":1},{"version":"f8d5ff8eafd37499f2b6a98659dd9b45a321de186b8db6b6142faed0fea3de77","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"a22dd55aa4d39906252000ab8e8a1b83b195eef7f4274eb51e457c1f11cf6580","impliedFormat":1},{"version":"540cc83ab772a2c6bc509fe1354f314825b5dba3669efdfbe4693ecd3048e34f","impliedFormat":1},{"version":"121b0696021ab885c570bbeb331be8ad82c6efe2f3b93a6e63874901bebc13e3","impliedFormat":1},{"version":"612d9da66bb046a9c1e2e8d026245ded881fc4b9f98cbfae714415d57ee0ae0b","impliedFormat":1},{"version":"32c2ad9494dad5d11b0564a619fee18f388db6c1e9e2cd3c360b3122549691eb","impliedFormat":1},{"version":"6c301d40aec56a74ec7bd7324e31a728dadf9bfba3e96def02938d3d973534ec","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"aa14cee20aa0db79f8df101fc027d929aec10feb5b8a8da3b9af3895d05b7ba2","impliedFormat":1},{"version":"493c700ac3bd317177b2eb913805c87fe60d4e8af4fb39c41f04ba81fae7e170","impliedFormat":1},{"version":"aeb554d876c6b8c818da2e118d8b11e1e559adbe6bf606cc9a611c1b6c09f670","impliedFormat":1},{"version":"acf5a2ac47b59ca07afa9abbd2b31d001bf7448b041927befae2ea5b1951d9f9","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"d71291eff1e19d8762a908ba947e891af44749f3a2cbc5bd2ec4b72f72ea795f","impliedFormat":1},{"version":"c0480e03db4b816dff2682b347c95f2177699525c54e7e6f6aa8ded890b76be7","impliedFormat":1},{"version":"25a5f6fd3a2243c859eddc99ab5fba11d970af2fe7a5df9c32b7668f76f97b01","impliedFormat":1},{"version":"8d207e1f9d2c30d6f77dfa693f3827c3fbf0d89240297e10bdfe1041d433df68","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"d682336018141807fb602709e2d95a192828fcb8d5ba06dda3833a8ea98f69e3","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"4fbd3116e00ed3a6410499924b6403cc9367fdca303e34838129b328058ede40","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"8c70ddc0c22d85e56011d49fddfaae3405eb53d47b59327b9dd589e82df672e7","impliedFormat":1},{"version":"2f9c89cbb29d362290531b48880a4024f258c6033aaeb7e59fbc62db26819650","impliedFormat":1},{"version":"a365c4d3bed3be4e4e20793c999c51f5cd7e6792322f14650949d827fbcd170f","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"273782b8454e78f6a8b30d2cfbf6860499c930595095fcc1689637115f0eddda","affectsGlobalScope":true,"impliedFormat":1},{"version":"3fbdd025f9d4d820414417eeb4107ffa0078d454a033b506e22d3a23bc3d9c41","affectsGlobalScope":true,"impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a8f8e6ab2fa07b45251f403548b78eaf2022f3c2254df3dc186cb2671fe4996d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"9f9bb6755a8ce32d656ffa4763a8144aa4f274d6b69b59d7c32811031467216e","impliedFormat":1},{"version":"5c32bdfbd2d65e8fffbb9fbda04d7165e9181b08dad61154961852366deb7540","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"0c05e9842ec4f8b7bfebfd3ca61604bb8c914ba8da9b5337c4f25da427a005f2","impliedFormat":1},{"version":"faed7a5153215dbd6ebe76dfdcc0af0cfe760f7362bed43284be544308b114cf","impliedFormat":1},{"version":"7029e566b8df176f703fb59fd437a38670c7a0e02c58b2d66dfb5b2e2b2defdb","impliedFormat":1},{"version":"7f2aa4d4989a82530aaac3f72b3dceca90e9c25bee0b1a327e8a08a1262435ad","impliedFormat":1},{"version":"d96b39301d0ded3f1a27b47759676a33a02f6f5049bfcbde81e533fd10f50dcb","impliedFormat":1},{"version":"e9f147ecca73d9346a4c073432843c159ccbe50bdcb678a78f6da10eae2cecf4","impliedFormat":1},{"version":"de061f7d72bd65c06fc1419f841dfdcb29a8e22fe6fa527d1e6eb20b897d4de0","impliedFormat":1},{"version":"663beafc2446079574570cba86e9b15f986f908ddb1b01274509970126fee945","impliedFormat":1},{"version":"a3102887d5058bf4cb5b37fa6964c09e9527c42053b3b5c642b89878620748de","impliedFormat":1},{"version":"0aaaa1727edd29673d85c9b26d7ca4d54e5407a48586903c51b48b7f7d196f61","impliedFormat":1},{"version":"d35bca0b261bff02635758c48e8ab99c61c420d0dfabbcf467e847171d876b7d","impliedFormat":1},{"version":"3bc12c40d90c342ff88a3d876996c555ed5cbee5fe8c3308a240b321f401ee46","impliedFormat":1},{"version":"ba130768aae855a5477e9e148e5c879548e6e7ccbcc56fd1934c8a18ea5b7569","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"d38530db0601215d6d767f280e3a3c54b2a83b709e8d9001acb6f61c67e965fc","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"b499af2054a037a162b3b72cd886f48bbf32a3502c865c6e29fac7d2ab3ce0b5","impliedFormat":1},{"version":"b83cb14474fa60c5f3ec660146b97d122f0735627f80d82dd03e8caa39b4388c","impliedFormat":1},{"version":"48773ca557b0319c2ee62ae249cf52a81709e8be139920d6479a66274de7c4ed","impliedFormat":1},{"version":"7274fbffbd7c9589d8d0ffba68157237afd5cecff1e99881ea3399127e60572f","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"8b4327413e5af38cd8cb97c59f48c3c866015d5d642f28518e3a891c469f240e","impliedFormat":1},{"version":"4cceef18d7f088e797a463e90b7a9dad10c6bc667724b7686e3e740ae00122be","impliedFormat":1},{"version":"7ee86fbb3754388e004de0ef9e6505485ddfb3be7640783d6d015711c03d302d","impliedFormat":1},{"version":"cc1954b539604b1e562319119ac7e888172208b32ca873f9a357a92c826bd046","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"bb8f2dbc03533abca2066ce4655c119bff353dd4514375beb93c08590c03e023","impliedFormat":1},{"version":"706dd95827e7ebaabda91d5db2b755233e0952d98570e9c032b0f066a15c1177","affectsGlobalScope":true,"impliedFormat":1},{"version":"0b103e9abfe82d14c0ad06a55d9f91d6747154ef7cacc73cf27ecad2bfb3afcf","impliedFormat":1},{"version":"990b8fad2327b77e6920cc792af320e8867e68f02ce849b12c0a6ab9a1aebb09","impliedFormat":1},{"version":"5eb8cd1cb0c9143d74a8190b577c522720878c31aef67d866fcd29973f83e955","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"a6805fcafed712aea7759f8bc731014f9d22738c1d6ef9d43b8091d1d48346d5","impliedFormat":1},{"version":"c49469a5349b3cc1965710b5b0f98ed6c028686aa8450bcb3796728873eb923e","impliedFormat":1},{"version":"4a889f2c763edb4d55cb624257272ac10d04a1cad2ed2948b10ed4a7fda2a428","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"d88ea80a6447d7391f52352ec97e56b52ebec934a4a4af6e2464cfd8b39c3ba8","impliedFormat":1},{"version":"142617b3cdf902b69c6464c9fbd942b60ab3e733ca18c032b19e0f7e2adbefe8","impliedFormat":1},{"version":"0b603555f1881f87256ffd6344d3e3ed6d466c2e701eabf381f28be8c2125892","impliedFormat":1},{"version":"897e4f7662488e3ecc79e743bdd3b78f13bdb69a97851afa5b440c4211e32ea9","impliedFormat":1},{"version":"e2e1c6d3b2d93add5200bd7bc1a8cccb4e446836b2111ece45db8683a2c765de","impliedFormat":1},{"version":"251b03d5cd243854ce870d9a9a39f491faf69898c5d6b5eee28cc7649c57417b","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"2c4de79f406d137390608e8c0a44fba2ff8e00bacfcae7c9d1781fef10e9440d","impliedFormat":1},{"version":"07ba23a10465791be5d22deaf5ef7de7658774ddff53721e5ea17fedea1bc721","impliedFormat":1},{"version":"dca8c645c5afeb03b1ecedbf16323f33e7d0afaa6256c8e047e6e38087a97f53","impliedFormat":1},{"version":"775f181bd4a533d6f8b5e55ec1d9f1624559720ae8a70e9432258da26b38d27c","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"9109a1291dd4b9f1541bea81ee11c247a2ca9e1ea89f87f13aa1811c3c069616","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"622694a8522b46f6310c2a9b5d2530dde1e2854cb5829354e6d1ff8f371cf469","impliedFormat":1},{"version":"cd8ce8d68567f62dd580b3c3c37777ac3f5b81944c7417f5ea83030eab533385","impliedFormat":1},{"version":"e374d1eaa05b7dc38580062942ac8351ce79cbe11f6dbce4946a582a5680582d","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"a9e6c0ff3f8186fccd05752cf75fc94e147c02645087ac6de5cc16403323d870","impliedFormat":1},{"version":"49af4b52f0d4d2304c5f2c6fe5fab3e153e0acc38830d0202821b877c097dd02","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"e68b8e5a1df7c1be2bc105141456ecba70215806e1c28bfbc5c12bfce4be6e68","impliedFormat":1},{"version":"511c8f02329808d47d00b859c532ae9115590048b17325a946c74dac48428650","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"b5f9e66625783eefcbe3d2da074b2e7ba2066d61ce3fc6ef4f22805ad946cab4","impliedFormat":1},{"version":"e37115962d284b9f7a37c2bdd2add50f88365dde41f5e0ff591ffc48a8ec7575","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"f89488602bec98a142072fae7ea5ba99431a569ff580c64b7be39896474799d8","impliedFormat":1},{"version":"bbbc47961f39a57df103cf4ca3bb8f8732b4b6678a18225a0aa76d59c466956c","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"2ffb043dc5163458e473b7010859f86e01dc4edffcae0a93d885d028b426a546","impliedFormat":1},{"version":"c8f004e6036aa1c764ad4ec543cf89a5c1893a9535c80ef3f2b653e370de45e6","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"b064c36f35de7387d71c599bfcf28875849a1dbc733e82bd26cae3d1cd060521","impliedFormat":1},{"version":"05c7280d72f3ed26f346cbe7cbbbb002fb7f15739197cbbee6ab3fd1a6cb9347","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"04b7b2e0832dfd3c31e81df3975e8d8fda28e7ff999b0aa2932608a8f6661d5c","impliedFormat":1},{"version":"ca2d34c6ed5cbd3070b8b6f32f42ae54adcc6499c1e4b99f0a5798b3f27cc653","impliedFormat":1},{"version":"9ec68995e66dd6b9dac834bf5ae85fde802714ea2e82151a5d1d53ef01b463ef","impliedFormat":1},{"version":"5c4d626b4902f2ef8a1cc146d761d276cef988016dc674e3b98fbad70e64bc9f","impliedFormat":1},{"version":"fdfaa0aad899524962e2955287b5b991ffe3be50f64e02eb60c933ca44644a94","impliedFormat":1},{"version":"53c972a0f9bc3a4ec70fff7314123ea8cfcf75b3703046f767d2dc1eea87b2fb","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"7d73b24e7bf31dfb8a931ca6c4245f6bb0814dfae17e4b60c9e194a631fe5f7b","impliedFormat":1},{"version":"d130c5f73768de51402351d5dc7d1b36eaec980ca697846e53156e4ea9911476","impliedFormat":1},{"version":"413586add0cfe7369b64979d4ec2ed56c3f771c0667fbde1bf1f10063ede0b08","impliedFormat":1},{"version":"06472528e998d152375ad3bd8ebcb69ff4694fd8d2effaf60a9d9f25a37a097a","impliedFormat":1},{"version":"7303b45138d2511035056a5901a1490ebdcbf055cbb1276f8629c5121cbe733e","impliedFormat":1},{"version":"27f874cd5327507eeff699a74567f60c1215b94509f4308633a7b01922471ed2","impliedFormat":1},{"version":"a401617604fa1f6ce437b81689563dfdc377069e4c58465dbd8d16069aede0a5","impliedFormat":1},{"version":"2c6cf04bc525caf6546e859e8ef10bfb9573837ec0bc5ec7b53a7b1b8ca72781","impliedFormat":1},{"version":"8695dec09ad439b0ceef3776ea68a232e381135b516878f0901ed2ea114fd0fe","impliedFormat":1},{"version":"304b44b1e97dd4c94697c3313df89a578dca4930a104454c99863f1784a54357","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"87cc05fe13108f02e12da7e3efd8e360fef78d96a0c9e11408ea1b1b9fb3e03d","impliedFormat":1},{"version":"1abbf67c218d23c2ce76887caac2df6c7dab3d97ba2b65348432b876f510002a","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"c06ef3b2569b1c1ad99fcd7fe5fba8d466e2619da5375dfa940a94e0feea899b","impliedFormat":1},{"version":"f7d628893c9fa52ba3ab01bcb5e79191636c4331ee5667ecc6373cbccff8ae12","impliedFormat":1},{"version":"1d879125d1ec570bf04bc1f362fdbe0cb538315c7ac4bcfcdf0c1e9670846aa6","impliedFormat":1},{"version":"8bd496cf710d4873d15e4891a5dbf945673e3321ca74cf75187e347fd5ed295e","impliedFormat":1},{"version":"a6dba407fc287f1e25454e75028c91bbc00675f2d1c4e8b3edcc36c08611a486","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"e91f7b1344577a02f051b9b471f33044fef8334a76dc9e1de003d17595a5219b","impliedFormat":1},{"version":"c0723195c85e19656d6b5b9fdb81d3f3403c1ae4679e722c6ea058c516b38d12","impliedFormat":1},{"version":"186eea74805194f04e41038fc5eca653788b9dedbab7c2d7d17e10139622dd92","impliedFormat":1},{"version":"71d9eb4c4e99456b78ae182fb20a5dfc20eb1667f091dbb9335b3c017dd1c783","impliedFormat":1},{"version":"cfa846a7b7847a1d973605fbb8c91f47f3a0f0643c18ac05c47077ebc72e71c7","impliedFormat":1},{"version":"1594da19968752a22b2ac48c2d0e60575700e745c577a8a4a676b841238ad5bb","impliedFormat":1},{"version":"e0cee12109e0a10a4c3d6769fcc7644b7c1ea7f52365bea51728f5af29f8a137","impliedFormat":1},{"version":"7d4254b4c6c67a29d5e7f65e67d72540480ac2cfb041ca484847f5ae70480b62","impliedFormat":1},{"version":"3536968defef8a75514f547ead5e2e9c1e984820290ec9b00c5fdfb6ef786535","impliedFormat":1},{"version":"d83773870080c30a230e322ce13a9c6f3398e8dacea4ea8a83e26370f3bac23e","impliedFormat":1},{"version":"dcfeaf98d66314fec29a9076c4290e45d0b196a65827becc19138e9c7b855f37","impliedFormat":1},{"version":"6849fe9210fe4946d5f085bfed36758f33dc6ae15a751338d178dd4daa017c46","impliedFormat":1},{"version":"888cda0fa66d7f74e985a3f7b1af1f64b8ff03eb3d5e80d051c3cbdeb7f32ab7","impliedFormat":1},{"version":"60681e13f3545be5e9477acb752b741eae6eaf4cc01658a25ec05bff8b82a2ef","impliedFormat":1},{"version":"ffae4e1e06aa848a1e4bcef162cd1c48e5909b26223515981310af9c036bdfc7","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"34e16eb7c31768a11a08aebcfb3d70d7b8f0b016197e98d8419e566ceae6d6c8","impliedFormat":1},{"version":"f94ec1f7e4b709d26960306c9082a7a1b728a6e13089346aa48ba57c74cbf47e","impliedFormat":1},{"version":"9a11cb4033405e96c247cd5aa29790212aaffdd127869e8a5219103f0b389fd5","impliedFormat":1},{"version":"01479d9d5a5dda16d529b91811375187f61a06e74be294a35ecce77e0b9e8d6c","impliedFormat":1},{"version":"aff5213585cb72e94054dfe17250ff315f3569b3919d1ef1ad235f37c4ee894e","impliedFormat":1},{"version":"fb2ea35e1be6388d722d7725e2b49c697d34d9c890c3b96758faaeb86d35cef8","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"1a4dc28334a926d90ba6a2d811ba0ff6c22775fcc13679521f034c124269fd40","impliedFormat":1},{"version":"f05315ff85714f0b87cc0b54bcd3dde2716e5a6b99aedcc19cad02bf2403e08c","impliedFormat":1},{"version":"5fad3b31fc17a5bc58095118a8b160f5260964787c52e7eb51e3d4fcf5d4a6f0","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"456006a6975b26c0a1785feddae165f6d307e2d601ffde27e21fc4a790e448a4","impliedFormat":1},{"version":"c857e0aae3f5f444abd791ec81206020fbcc1223e187316677e026d1c1d6fe08","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"1fe0d18b111e1145a7e7601855bccd4ca20f24e3b9a5aba6bb1fa9d1a7059170","impliedFormat":1},{"version":"5632c3c26d420c063eebe64c45b1248b9492a67bf44f1d0c57e9dc8f6cf449bb","impliedFormat":1},{"version":"0df5aa619ab12993a39ea6dae062ee46eadbb4d738916460e636ada52bced75b","impliedFormat":1},{"version":"8fca3039857709484e5893c05c1f9126ab7451fa6c29e19bb8c2411a2e937345","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"10ab7be91f87ebe8916b62cf28af2e45b5601fc7b0e311adf838f912c6b31dd8","impliedFormat":1},{"version":"bc636fbc08e0979ceb7eb0731a33000283d77a33b62e1f71ee65be50394e40ba","impliedFormat":1},{"version":"7e0b7f91c5ab6e33f511efc640d36e6f933510b11be24f98836a20a2dc914c2d","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"75bbd3be047d539988a0ff0b56384ef7a6a25f3b676ad96bee547d44c31622a7","impliedFormat":1},{"version":"42960001a776b089ade681ab5cfddc936e0afb0615133ec1841f3dee89d3e1bf","impliedFormat":1},{"version":"0aedb02516baf3e66b2c1db9fef50666d6ed257edac0f866ea32f1aa05aa474f","impliedFormat":1},{"version":"da47712b394d944328245482603bc6f416d3949b67c9392279caab595076b510","affectsGlobalScope":true,"impliedFormat":1},{"version":"37d0071d8f0a06dc55c2c5e0ec3391affd4fd107c53410bf358196ec0bf3923f","impliedFormat":1},{"version":"b213dad76ca37fd552274c9499056e1c0d9c1bd38a55bb7f68b22ba6b84c3ad7","impliedFormat":1},{"version":"56ccb49443bfb72e5952f7012f0de1a8679f9f75fc93a5c1ac0bafb28725fc5f","impliedFormat":1},{"version":"20fa37b636fdcc1746ea0738f733d0aed17890d1cd7cb1b2f37010222c23f13e","impliedFormat":1},{"version":"d90b9f1520366d713a73bd30c5a9eb0040d0fb6076aff370796bc776fd705943","impliedFormat":1},{"version":"bc03c3c352f689e38c0ddd50c39b1e65d59273991bfc8858a9e3c0ebb79c023b","impliedFormat":1},{"version":"19df3488557c2fc9b4d8f0bac0fd20fb59aa19dec67c81f93813951a81a867f8","affectsGlobalScope":true,"impliedFormat":1},{"version":"b25350193e103ae90423c5418ddb0ad1168dc9c393c9295ef34980b990030617","affectsGlobalScope":true,"impliedFormat":1},{"version":"bef86adb77316505c6b471da1d9b8c9e428867c2566270e8894d4d773a1c4dc2","impliedFormat":1},{"version":"5a49adaef698b7ad7e6127949fa1b0bbd3d46b7cbd11c54e392a4dcdd51f5190","impliedFormat":1},{"version":"96171c03c2e7f314d66d38acd581f9667439845865b7f85da8df598ff9617476","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"2489bf04d77dc025ba67f49f1a56eb24b9db477d5ff88123d887e163ed1776aa","impliedFormat":1},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"0b77b819b5417775fccb20c678293cf614c054a5b1a65421a5b933a9124ba998","impliedFormat":1},{"version":"e1f6076688a95bd82deaac740fccbe3cdea0d8a22057cccc9c5bce4398bdd33b","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"b1f1d57fde8247599731b24a733395c880a6561ec0c882efaaf20d7df968c5af","impliedFormat":1},{"version":"6715dc4eb59c8ea9abe2b78c235ed331dc710a06fe56798868dbc4d40cd1b707","impliedFormat":1},{"version":"35e6379c3f7cb27b111ad4c1aa69538fd8e788ab737b8ff7596a1b40e96f4f90","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"5a3ea721d03a361ccbdd7390ccd75f6e84cbca3a3f01f4b331ecc9af31890c49","impliedFormat":1},{"version":"e7dfaee4af38d45b1cab8a1ee0b3bc1f85ddcf64545ed391d675d78ae6526274","affectsGlobalScope":true,"impliedFormat":1},{"version":"e8daa443eaf9a27fd382cc1f8ebe30330c0f4d89511cfb469166874806751d35","impliedFormat":1},{"version":"af48e58339188d5737b608d41411a9c054685413d8ae88b8c1d0d9bfabdf6e7e","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"1de8c302fd35220d8f29dea378a4ae45199dc8ff83ca9923aca1400f2b28848a","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"332248ee37cca52903572e66c11bef755ccc6e235835e63d3c3e60ddda3e9b93","impliedFormat":1},{"version":"94e8cc88ae2ef3d920bb3bdc369f48436db123aa2dc07f683309ad8c9968a1e1","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"320f4091e33548b554d2214ce5fc31c96631b513dffa806e2e3a60766c8c49d9","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"d90d5f524de38889d1e1dbc2aeef00060d779f8688c02766ddb9ca195e4a713d","impliedFormat":1},{"version":"07ed3ddab975995eea41b22f3010506fb9f5fb301d04820b07d7a1aee5477d7c","impliedFormat":1},{"version":"969d8b0965849f4bae7cab0ba90bd1e1220e95999c2c6f01117fa7500901c017","impliedFormat":1},{"version":"6ec840ee5e2bc103f557fe38b1d585ee250540468713d7634ee066de372bf332","impliedFormat":1},{"version":"b0309e1eda99a9e76f87c18992d9c3689b0938266242835dd4611f2b69efe456","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"6ceb10ca57943be87ff9debe978f4ab73593c0c85ee802c051a93fc96aaf7a20","impliedFormat":1},{"version":"1de3ffe0cc28a9fe2ac761ece075826836b5a02f340b412510a59ba1d41a505a","impliedFormat":1},{"version":"e46d6cc08d243d8d0d83986f609d830991f00450fb234f5b2f861648c42dc0d8","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"ff863d17c6c659440f7c5c536e4db7762d8c2565547b2608f36b798a743606ca","impliedFormat":1},{"version":"5412ad0043cd60d1f1406fc12cb4fb987e9a734decbdd4db6f6acf71791e36fe","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"e297c0a524edee7677939122f90027bfbe5f2698939d9a85728e5044b39c7124","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"bc9ee0192f056b3d5527bcd78dc3f9e527a9ba2bdc0a2c296fbc9027147df4b2","impliedFormat":1},{"version":"b62381cae176db34f003cc6172ee8f3e0122014889d66391aa73698105cf4934","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"30d8da250766efa99490fc02801047c2c6d72dd0da1bba6581c7e80d1d8842a4","impliedFormat":1},{"version":"03566202f5553bd2d9de22dfab0c61aa163cabb64f0223c08431fb3fc8f70280","impliedFormat":1},{"version":"41eb514d9ce0a6e87957f08a4b7af70d93f87637f37dee706e2d92a6601c25a9","impliedFormat":1},{"version":"e7765aa8bcb74a38b3230d212b4547686eb9796621ffb4367a104451c3f9614f","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"7bd01f0f28cd3aeb2046274d85208e245965f6f2948edf4f7b2057bcf9f22ccc","impliedFormat":99},{"version":"d2f2cf2b8cc92bea913cda4a076e0f790b23a21e84f989d12f0116a7fe3906e0","impliedFormat":99},{"version":"6de125ea94866c736c6d58d68eb15272cf7d1020a5b459fea1c660027eca9a90","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5b20bc288ee49989c95b20847fc93b96bf61cc0845598897a6a53a967dd7d07","affectsGlobalScope":true,"impliedFormat":1},{"version":"064ac1c2ac4b2867c2ceaa74bbdce0cb6a4c16e7c31a6497097159c18f74aa7c","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"d3b315763d91265d6b0e7e7fa93cfdb8a80ce7cdd2d9f55ba0f37a22db00bdb8","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},{"version":"bb6d9c2b075a5c675259c75950c5f2359b17bf031b8cbdff8c04aee66a6a514f","affectsGlobalScope":true},"7ad303e40d4fddf44f156129e397511953a71481c5cfd86b1862649aaaf240cc",{"version":"168aa44771e55cda628eeb5fbb6f626f6f263d827967b119e8c06abd7f1362ec","signature":"435a1e418e8338be3f39614b96b81a9aa2700bc8c27bc6b98f064ff9ce17c363"},{"version":"3b89216a7e38a454985ad17bb2ff85792837dc812f2a89fa5f60ad0a2e216fa7","impliedFormat":99},{"version":"16fe60bb544cfedfd2b5bb2f7d0b3957be7978706d57d9f06edc9c0c8dbdba23","impliedFormat":99},{"version":"82179358c2d9d7347f1602dc9300039a2250e483137b38ebf31d4d2e5519c181","impliedFormat":99},{"version":"c73fdf42528325dd17940937ed787b15ae3445c6a2dae1a2b74bc4d87d337ca2","impliedFormat":99},{"version":"e8e17dfef3cfa9f0847ac93dd535a9896af7fb57c1a1b164484bb1b0ee4a25d8","impliedFormat":99},{"version":"51d2ffea2d1ee4a81c775938588c1e16620281adb60cbc26579a2fc6baa10bd2","impliedFormat":99},{"version":"148debd12783ded0a60d115daeacd8136f77757ae89a05c4e18de6dd77646fd2","impliedFormat":99},{"version":"0088b02dca63c47b273a140d0a3944bdc6dc2eb765fff0ca98e3c3a2786b3a5a","impliedFormat":99},{"version":"a651d06b780fa354231f19b040cbcde484bede3218885752b4f9e9a8f72d3b5f","impliedFormat":99},{"version":"06e26f75bed4c8389a8a63f0e6d6a9068038873dc95d8d1338e8c370a0ae8bc3","impliedFormat":99},{"version":"a2155e2675fd1af52b0b70779371c28611cdd1076b29d0f68bf93b983e5ddce0","impliedFormat":99},{"version":"a413e4b0b99280e1e58f5fe7b2b585e8a9be4996df8c58585399c9e2ca8a683e","impliedFormat":99},{"version":"609ab2c225766bc0851251c1db0fd5492673e190074045d21dc5dc7c3c46d785","impliedFormat":99},{"version":"c074e054c9db79055d37d7d70131e9a3234b8186773b3edb617c13f80bcf8774","impliedFormat":99},{"version":"7d3e062a778b8f5ea4f0cac7e925e31f88e6739812ebc5f827474324a4048f14","impliedFormat":99},{"version":"7f3857dc5cfe1e5e977edb14e931d9939a952e8e41997263a927f8f0299ea652","impliedFormat":99},{"version":"3559624d0102d10d7765c292c60ccbc229541534db32061e06df88bfe1064636","impliedFormat":99},{"version":"5a9834c603c65aee5cba0c1d6b3c7aee85cdc7862832a23165c6aa4139c165f2","impliedFormat":99},{"version":"a7d7b5fa83cd7b3b4c2aa73bc29e7cbd53d5690b74f6fb39a5558af0a94967ba","impliedFormat":99},{"version":"4e003c868b0d8f8ad200b96cbc653e18e513fa23e1c19c4fe3cc25d4394efc47","impliedFormat":99},{"version":"605450898939e8abce51e8085a41b60640278337a969c33cd6b169e7c4f9c3f2","impliedFormat":99},{"version":"e0864480ea083087d705f9405bd6bf59b795e8474c3447f0d6413b2bce535a09","impliedFormat":99},{"version":"e67cbea16f1994af89efd700542dbf3828a46a52b29e4d67e801bd7869dc103c","impliedFormat":99},{"version":"f582b0fcbf1eea9b318ab92fb89ea9ab2ebb84f9b60af89328a91155e1afce72","impliedFormat":99},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"e3cf0611709328b449ec13f8c436712d62003620ce480139fae46ce001c2ee9f","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"3fd8a5aefd8c3feb3936ca66f5aa89dff7bf6e6537b4158dbd0f6e0d65ed3b9e","impliedFormat":1},{"version":"a18642ddf216f162052a16cba0944892c4c4c977d3306a87cb673d46abbb0cbf","impliedFormat":1},{"version":"41c41c6e90133bb2a14f7561f29944771886e5535945b2b372e2f6ed6987746e","impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":99},{"version":"960bd764c62ac43edc24eaa2af958a4b4f1fa5d27df5237e176d0143b36a39c6","affectsGlobalScope":true,"impliedFormat":99},{"version":"f093d4bd6a9267be5f8ecbfbca19f4f3359b3839883206150c5d833606569e84","impliedFormat":99},{"version":"59f8dc89b9e724a6a667f52cdf4b90b6816ae6c9842ce176d38fcc973669009e","affectsGlobalScope":true,"impliedFormat":99},{"version":"4a13397dffad4475c45c70fde584c925fe8c9218b3c7ab94397b68fc434f63b6","impliedFormat":99},{"version":"2faebfa830ae4cfbfb58e48b0ec20a2a63882d776f0ca36ec7155d45cf1b7f2d","impliedFormat":99},{"version":"b478fad6cb2c66bfbfc027983240b416a7733013f878056ba92cf809020018a0","impliedFormat":99},{"version":"c76c02846ba7d40b9b3488f0e8d75d02cbdee2f0bc5fcd55dd3bd2e1457646ea","impliedFormat":99},{"version":"4ead13a482c539b77394b2a97e3b877b809eac596390371cea490286f53b996a","impliedFormat":99},{"version":"06db2f8ba1d1dfacf04529cb731081ab23f133f29c7608ebdfbcab356996827c","impliedFormat":99},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"3a582c6e8906f5b094ccf0de6cc6f4f8a54b05a34f52517aba5c9c7f704f6b28","impliedFormat":99},{"version":"ef13c73d6157a32933c612d476c1524dd674cf5b9a88571d7d6a0d147544d529","impliedFormat":99},{"version":"3b0a56d056d81a011e484b9c05d5e430711aaecd561a788bad1d0498aad782c7","impliedFormat":99},{"version":"0528f6d21f7a02d4092895090d2dd86104bd5a3e79eced96d5a1a7dd90943d17","impliedFormat":99},{"version":"427fe2004642504828c1476d0af4270e6ad4db6de78c0b5da3e4c5ca95052a99","impliedFormat":1},{"version":"2eeffcee5c1661ddca53353929558037b8cf305ffb86a803512982f99bcab50d","impliedFormat":99},{"version":"9afb4cb864d297e4092a79ee2871b5d3143ea14153f62ef0bb04ede25f432030","affectsGlobalScope":true,"impliedFormat":99},{"version":"5c935b7fc4ddc1410ea1cd7cd4e35ed106a6e4920dd27a9480a40fd224359dc3","affectsGlobalScope":true,"impliedFormat":99},{"version":"b5ce343886d23392be9c8280e9f24a87f1d7d3667f6672c2fe4aa61fa4ece7d4","impliedFormat":99},{"version":"72ce5b734c05da85c85a6f6dc05823b051d6aa41acaedeeb1d17c72f3b4efa72","impliedFormat":99},{"version":"b0857bb28fd5236ace84280f79a25093f919fd0eff13e47cc26ea03de60a7294","impliedFormat":99},{"version":"5e43e0824f10cd8c48e7a8c5c673638488925a12c31f0f9e0957965c290eb14c","impliedFormat":99},{"version":"9443967db823b66d1682be7fc66392be7c7924e10c3e54900f456341e94591a6","impliedFormat":99},{"version":"424f71d1fae96ac2e878af92345bb87bea1d29f757228fbc190133b305643f2c","impliedFormat":99},{"version":"61bb64660ee150f3ab618340e15cca0a81664801bede7c966ca0eca3a952fe63","impliedFormat":99},{"version":"42a12f2faa483c9b48195ed794d22698162274e755f6e07219c2351c4f08d732","impliedFormat":99},{"version":"ec0c42bb0f465e4993f2bc68a6ce9df9a2dcbc7b83e21748f82f1b69561938e3","impliedFormat":99},{"version":"f50ff37a9cbbe74475f426474d9827083c7c2c138a954d28f1690df338f69291","impliedFormat":99},{"version":"61fd6c17235d530c40f543dd7c40afab091d91c1ef890baeed30db6d82b04b28","impliedFormat":99},{"version":"bcbd3becd08b4515225880abea0dbfbbf0d1181ce3af8f18f72f61edbe4febfb","impliedFormat":99},{"version":"091767bc841f937654ed597d49e023ed59850355e746ae1a6f20ab31076ee1fb","impliedFormat":99},{"version":"19c6d6135af59693698d384050b45a8a049493500add442f58e4bd7c8a255ab6","impliedFormat":99},{"version":"6a0dba12d55314638a8c51108b20fe2f68f1364a619d098918bda91c22dec154","impliedFormat":99},{"version":"8124828a11be7db984fcdab052fd4ff756b18edcfa8d71118b55388176210923","impliedFormat":99},{"version":"ed9bb55ddcbebd5cb3eee991f57ff21438546ee40ee1c310281bd12a6c7cf65b","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"6987dfb4b0c4e02112cc4e548e7a77b3d9ddfeffa8c8a2db13ceac361a4567d9","impliedFormat":99},{"version":"5e2ba3d18d78aebbde1f34bde356e41e9c76eeaeaeee56a37036596a9eff4211","impliedFormat":99},{"version":"8280ae8ccc0493b32d1742d585357ab9f0a508ea050af25a5a20d64010d0a5cf","impliedFormat":99},{"version":"7adfd9f9056ecd4ae6c65fde2a98654960c662714c73f048478959d04c09e144","impliedFormat":99},{"version":"437b7613a30a2fcde463f7b707c6d5567a8823fbc51de50b8641bf5b1d126fad","impliedFormat":99},{"version":"63ea959e28c110923f495576e614fb8b36c09b6828b467b2c7cd7f03b03ccf9f","impliedFormat":99},{"version":"1601a95dbb33059fc3d12638ed2a9aecff899e339c5c0f3a0b28768866d385b4","impliedFormat":99},{"version":"56fc978580577d30f4c2cdb5b1eb9217b66ed66537dd27141256f426e4b8dd68","impliedFormat":99},{"version":"2c5413050a2580becf9d82dd7e3006b95623e96f145356bf73230cd635352f70","impliedFormat":99},{"version":"860bedc71ead192ea4a0ea5ef4686e65724d14b391ebd1a6671a7044e6bd8e15","impliedFormat":99},{"version":"7c0a845bee4a084cbb8654709f48e5f13e2f6d45e5e2dde7c57cadf79fd9e3d5","impliedFormat":99},{"version":"07ad8a597ac75084e3dd9f9fadf5e8d7ccdcfe2f0c94ea0cf1cd8aa027a6c46e","impliedFormat":99},{"version":"94ddb4a2bb0c69e8efea22c58c2b6f84017eba469a4e433f5396ea8619d051cb","impliedFormat":99},{"version":"064499a671b662b25675beccdd04fb0bdebb6bd49bdb90d448e4b1ce3db20526","impliedFormat":99},{"version":"7bbff6783e96c691a41a7cf12dd5486b8166a01b0c57d071dbcfca55c9525ec4","impliedFormat":99},{"version":"ae7d986f19db00cd62ce8573307f910ec2103d7fc30df09cedeec3cabec13082","signature":"4b96dd19fd2949d28ce80e913412b0026dc421e5bf6c31d87c7b5eb11b5753b4"},{"version":"ae77d81a5541a8abb938a0efedf9ac4bea36fb3a24cc28cfa11c598863aba571","impliedFormat":1},{"version":"f329dfad7970297cbf07ddc8fce2ad4a24e2a3855917c661922ef86eb24dd1f1","impliedFormat":1},{"version":"841784cfa9046a2b3e453d638ea5c3e53680eb8225a45db1c13813f6ea4095e5","affectsGlobalScope":true,"impliedFormat":1},{"version":"646ef1cff0ec3cf8e96adb1848357788f244b217345944c2be2942a62764b771","impliedFormat":1},{"version":"3cfb7c0c642b19fb75132154040bb7cd840f0002f9955b14154e69611b9b3f81","impliedFormat":1},{"version":"8387ec1601cf6b8948672537cf8d430431ba0d87b1f9537b4597c1ab8d3ade5b","impliedFormat":1},{"version":"d16f1c460b1ca9158e030fdf3641e1de11135e0c7169d3e8cf17cc4cc35d5e64","impliedFormat":1},{"version":"a934063af84f8117b8ce51851c1af2b76efe960aa4c7b48d0343a1b15c01aedf","impliedFormat":1},{"version":"e3c5ad476eb2fca8505aee5bdfdf9bf11760df5d0f9545db23f12a5c4d72a718","impliedFormat":1},{"version":"462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094","impliedFormat":1},{"version":"5923d8facbac6ecf7c84739a5c701a57af94a6f6648d6229a6c768cf28f0f8cb","impliedFormat":1},{"version":"d0570ce419fb38287e7b39c910b468becb5b2278cf33b1000a3d3e82a46ecae2","impliedFormat":1},{"version":"3aca7f4260dad9dcc0a0333654cb3cde6664d34a553ec06c953bce11151764d7","impliedFormat":1},{"version":"a0a6f0095f25f08a7129bc4d7cb8438039ec422dc341218d274e1e5131115988","impliedFormat":1},{"version":"b58f396fe4cfe5a0e4d594996bc8c1bfe25496fbc66cf169d41ac3c139418c77","impliedFormat":1},{"version":"45785e608b3d380c79e21957a6d1467e1206ac0281644e43e8ed6498808ace72","impliedFormat":1},{"version":"bece27602416508ba946868ad34d09997911016dbd6893fb884633017f74e2c5","impliedFormat":1},{"version":"2a90177ebaef25de89351de964c2c601ab54d6e3a157cba60d9cd3eaf5a5ee1a","impliedFormat":1},{"version":"82200e963d3c767976a5a9f41ecf8c65eca14a6b33dcbe00214fcbe959698c46","impliedFormat":1},{"version":"b4966c503c08bbd9e834037a8ab60e5f53c5fd1092e8873c4a1c344806acdab2","impliedFormat":1},{"version":"3d3208d0f061e4836dd5f144425781c172987c430f7eaee483fadaa3c5780f9f","impliedFormat":1},{"version":"34a8a5b4c21e7a6d07d3b6bce72371da300ec1aed58961067e13f1f4dc849712","impliedFormat":1},{"version":"4ffba3c5848b4fe62ee59b754fd5f256ad9656a0db6d37b9a2a8cb40dfc7ac21","impliedFormat":99},{"version":"c76c02846ba7d40b9b3488f0e8d75d02cbdee2f0bc5fcd55dd3bd2e1457646ea","impliedFormat":99},{"version":"32b35cf0dc3a1b1a7118b61c34ce2ad1a29695851679f9ec34e0776f2ece2a69","impliedFormat":99},{"version":"b413fbc6658fe2774f8bf9a15cf4c53e586fc38a2d5256b3b9647da242c14389","impliedFormat":99},{"version":"59e5e964b84fdb2378e9455e4e59405030e4ed2b4c6f891ce395f17796af3cbb","impliedFormat":99},{"version":"c30a41267fc04c6518b17e55dcb2b810f267af4314b0b6d7df1c33a76ce1b330","impliedFormat":1},{"version":"72422d0bac4076912385d0c10911b82e4694fc106e2d70added091f88f0824ba","impliedFormat":1},{"version":"da251b82c25bee1d93f9fd80c5a61d945da4f708ca21285541d7aff83ecb8200","impliedFormat":1},{"version":"64db14db2bf37ac089766fdb3c7e1160fabc10e9929bc2deeede7237e4419fc8","impliedFormat":1},{"version":"98b94085c9f78eba36d3d2314affe973e8994f99864b8708122750788825c771","impliedFormat":1},{"version":"90ba95a763101bb61b8a799731a2ed60b5016b8135c1a2d5186862d4b534d4a1","impliedFormat":99},{"version":"ad763fa0c24ede2b818eb6598c12dd581451f94688fb9ed963beba20d513a7ec","signature":"90ec9100c29e008c3d9194acd818e2cfa6dc6e177154bc8e10c5959aa35619ed"},{"version":"b7ca2f47522d4ea41e65ff92c4c6dd9c4c8260da7c456a7631a9c88dc056b4d0","impliedFormat":1},{"version":"4f01e4d0959f9125b89e5737eb1ca2bfa69fd6b7d6126eba22feb8b505b00cde","impliedFormat":1},{"version":"4363a1adb9c77f2ed1ca383a41fbab1afadd35d485c018b2f84e834edde6a2c7","impliedFormat":1},{"version":"1d6458533adb99938d041a93e73c51d6c00e65f84724e9585e3cc8940b25523f","impliedFormat":1},{"version":"b0878fbd194bdc4d49fc9c42bfeeb25650842fe1412c88e283dc80854b019768","impliedFormat":1},{"version":"a892ea0b88d9d19281e99d61baba3155200acced679b8af290f86f695b589b16","impliedFormat":1},{"version":"03b42e83b3bcdf5973d28641d72b81979e3ce200318e4b46feb8347a1828cd5d","impliedFormat":1},{"version":"8a3d57426cd8fb0d59f6ca86f62e05dde8bfd769de3ba45a1a4b2265d84bac5a","impliedFormat":1},{"version":"afc6e1f323b476fdf274e61dab70f26550a1be2353e061ab34e6eed180d349b6","impliedFormat":1},{"version":"7c14483430d839976481fe42e26207f5092f797e1a4190823086f02cd09c113c","impliedFormat":1},{"version":"828a3bea78921789cbd015e968b5b09b671f19b1c14c4bbf3490b58fbf7d6841","impliedFormat":1},{"version":"69759c42e48938a714ee2f002fe5679a7ab56f0b5f29d571e4c31a5398d038fe","impliedFormat":1},{"version":"6e5e666fa6adeb60774b576084eeff65181a40443166f0a46ae9ba0829300fcb","impliedFormat":1},{"version":"1a4d43bdc0f2e240395fd204e597349411c1141dd08f5114c37d6268c3c9d577","impliedFormat":1},{"version":"874e58f8d945c7ac25599128a40ec9615aa67546e91ca12cbf12f97f6baf54ff","impliedFormat":1},{"version":"da2627da8d01662eb137ccd84af7ffa8c94cf2b2547d4970f17802324e54defc","impliedFormat":1},{"version":"07af06b740c01ed0473ebdd3f2911c8e4f5ebf4094291d31db7c1ab24ff559aa","impliedFormat":1},{"version":"ba1450574b1962fcf595fc53362b4d684c76603da5f45b44bc4c7eeed5de045b","impliedFormat":1},{"version":"b7903668ee9558d758c64c15d66a89ed328fee5ac629b2077415f0b6ca2f41bc","impliedFormat":1},{"version":"c7628425ee3076c4530b4074f7d48f012577a59f5ddade39cea236d6405c36ba","impliedFormat":1},{"version":"28c8aff998cc623ab0864a26e2eb1a31da8eb04e59f31fa80f02ec78eb225bcd","impliedFormat":1},{"version":"78d542989bdf7b6ba5410d5a884c0ab5ec54aa9ce46916d34267f885fcf65270","impliedFormat":1},{"version":"4d95060af2775a3a86db5ab47ca7a0ed146d1f6f13e71d96f7ac3b321718a832","impliedFormat":1},{"version":"6708cd298541a89c2abf66cceffc6c661f8ee31c013f98ddb58d2ec4407d0876","impliedFormat":1},{"version":"2e90928c29c445563409d89a834662c2ba6a660204fb3d4dc181914e77f8e29d","impliedFormat":1},{"version":"84be1b8b8011c2aab613901b83309d017d57f6e1c2450dfda11f7b107953286a","impliedFormat":1},{"version":"d7af890ef486b4734d206a66b215ebc09f6743b7fb2f3c79f2fb8716d1912d27","impliedFormat":1},{"version":"7e82c1d070c866eaf448ac7f820403d4e1b86112de582901178906317efc35ad","impliedFormat":1},{"version":"c5c4f547338457f4e8e2bec09f661af14ee6e157c7dc711ccca321ab476dbc6d","impliedFormat":1},{"version":"223e233cb645b44fa058320425293e68c5c00744920fc31f55f7df37b32f11ad","impliedFormat":1},{"version":"1394fe4da1ab8ab3ea2f2b0fcbfd7ccbb8f65f5581f98d10b037c91194141b03","impliedFormat":1},{"version":"086d9e59a579981bdf4f3bfa6e8e893570e5005f7219292bf7d90c153066cdfc","impliedFormat":1},{"version":"1ea59d0d71022de8ea1c98a3f88d452ad5701c7f85e74ddaa0b3b9a34ed0e81c","impliedFormat":1},{"version":"cd66a32437a555f7eb63490509a038d1122467f77fe7a114986186d156363215","impliedFormat":1},{"version":"f53d243499acfacc46e882bbf0bf1ae93ecea350e6c22066a062520b94055e47","impliedFormat":1},{"version":"65522e30a02d2720811b11b658c976bff99b553436d99bafd80944acba5b33b4","impliedFormat":1},{"version":"76b3244ec0b2f5b09b4ebf0c7419260813820f128d2b592b07ea59622038e45c","impliedFormat":1},{"version":"66eb7e876b49beff61e33f746f87b6e586382b49f3de21d54d41313aadb27ee6","impliedFormat":1},{"version":"69e8dc4b276b4d431f5517cd6507f209669691c9fb2f97933e7dbd5619fd07b7","impliedFormat":1},{"version":"361a647c06cec2e7437fa5d7cdf07a0dcce3247d93fbf3b6de1dc75139ff5700","impliedFormat":1},{"version":"fe5726291be816d0c89213057cd0c411bb9e39e315ed7e1987adc873f0e26856","impliedFormat":1},{"version":"1b76990de23762eb038e8d80b3f9c810974a7ed2335caa97262c5b752760f11a","impliedFormat":1},{"version":"5e050e05fe99cd06f2d4ad70e73aa4a72961d0df99525e9cad4a78fa588f387b","impliedFormat":1},{"version":"4ff327e8b16da9d54347b548f85675e35a1dc1076f2c22b2858e276771010dd2","impliedFormat":1},{"version":"f767787945b5c51c0c488f50b3b3aeb2804dfd2ddafcb61125d8d8857c339f5a","impliedFormat":1},{"version":"14ab21a9aeff5710d1d1262459a6d49fb42bed835aa0f4cfc36b75aa36faddcd","impliedFormat":1},{"version":"ba3c4682491b477c63716864a035b2cfdd727e64ec3a61f2ca0c9af3c0116cfd","affectsGlobalScope":true,"impliedFormat":1},{"version":"b222d32836d745e1e021bb10f6a0f4a562dd42206203060a8539a6b9f16523f0","impliedFormat":1},{"version":"a3f6d8995864820a0207b7ef4ce1ed6a8dd2fccc7e70d015da15034807c38e1c","impliedFormat":1},{"version":"651df11341eff0b769fb83af75b1872e6cedf406674c5eaa2650551aceb5a816","impliedFormat":1},{"version":"774a466295d26eddab911b9f567040364e7b7d0eb8003ad3bfc92b97eeecf066","signature":"f15b4a91c10bf30ff3708a5e3968c0a52ca4e86a4b9a5b1c4fc8e9b5f1292f21"},{"version":"1e9d4c3b066e0228765cef074a9bf49d0b8d3af461c97f2c511e8f7110b56235","signature":"e59faabf094dd75dcf08847ca1b8ae16daf269ca02c744c521b38ed5d297578c"},{"version":"48d3d3a869cf85c67c62d87c031946cb9ae89fc59d3d0b274d17c88097e9847d","signature":"16e6aa6706cf2bfdef5a587057b3b672099a2dd478584f26aacc4dd07336d0a0"},{"version":"a6a2173b6c4ebb031d158b06004ef4db7678ea8df66be27b317973460591c433","signature":"7bf23ce970d42624e8d08ff91d0a8dbe0063953e9038c2c75e827a0770da33cf"},{"version":"9dfe98d745dfc706198852e032c3c936092a4484951872c0d18639cf2ab698cf","signature":"326009db2b0a0f3290cbde2271f91e51869148a3e286ccc3ac41559f6c830642"},{"version":"9f01d110e167ddcfaa01b236040a12699b6ed7a2070d7c362f4403f092fed008","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"030523c514236148e0f25975ef74b2490c391cb9596cd668728f7db5b0702e08","signature":"411166ca21d7ae80b737bb1c7e9900e897ac7b27c06647063730ba8207522e46"},{"version":"ca52a606c31ddaae5f7c4055fccb3563cf7e85601c96dbf28b7d0795d2d21af0","signature":"82d82e5e9d2c282a1dddebbcc73d9ff5c89f60859cd8769d0f6eae7b76f3f4d1"},{"version":"41d8a2df75ff7b6ee4c82ef8ce52032aa272358a5f9187a6b5986849200ed411","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f25d11b3d781ff8db8614eeee12b43f81ad05389fb25acf10b8c3473b0e2a1d0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"78b3214dba4e86c41e4cdf5de5dcc853806ff74fad5df3dc1087cdbe19ad00e0","signature":"5c4de1b33c2b0c5b0b823fca5ba9e5dde3c995aa8a2c42db12b2e8b64f408805"},{"version":"9980dd11b1e848c16ad84eae18f4e7bfa3331f81c2617533b0e3bba61cb9ab3f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"45b82fd88270dd3d0aff39220b341eca789f77c82813dc6695b2058479d8dc28","signature":"3d67e5bdef7b1039301b51fc5ba5303c133b863f30e0de39c8aa1db5d897f1b6"},{"version":"915d1bc5f4c3b9cc8e25964dcd29b3c00ffb36ab2c8b12b2472533b3113485a9","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f212078a0ecefabbbbc627b89e28c871b559c1c57a4dcae32c8d7c2b6c4f0a00","signature":"b495b6770d46b4ebe3dbe0c1f6d64e8b50ea14ea8d2b63377813ae35672a4535"},{"version":"d237e0ff6755c658dc810e02b3a290f593f0ee2708eccbf1e2cd65c512c8de4a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cadbf01db107d2e9cceb5ecdd3f8f0084d996f07f2fca714409247deb26f0b85","signature":"ed905f29ff05cd1d3cf260949314ca896356f5c8bc011fdee9d75cde0404c6a0"},{"version":"a0563dec1ee31e5e8150681234665a122d20bb748d51a4b841ba1bcee31697a7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ea7a4796f350344bbf39b19518d78169bd66a819f787b0a4d1a6ad642dbce7a2","signature":"8f6ef9414b1d23dc0a927160ed27770f93c1fe749a9af0e626f5a0e8918d5fc1"},{"version":"428631c15a500daa23f33c9e1ca427423fe56515e811133ff5e80891238fb242","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cc5bfa36a8f86eea23a84b78f27ba0f842426cefb5a13d4b53b80a331aaaa268","signature":"3dcde6c75b6c7fc3bc7fca2ca1273467974a8b54061edd648ed854bc4c5d842a"},{"version":"fcc8fb0967eda9256921f3cfc521d49f3d473232d4c754c09de1d50faa996e35","signature":"37e053a2b54ed97cab1c0e0e6a0ed4d610d6011b24e4cef7633007fb573468e1"},{"version":"463efb47c0879fb7e518ca8d209ffc49e2d682d0c47eaf30d074a1032290ac50","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a794572bfb0885ec1de20b701e498a415c0480e4fb6c20dbaa125b374ae691cb","signature":"46d3c81ba82aaaeabab3baa813cbb84b4c9808ec7ad495ad5fdc27d9358aa314"},{"version":"dd26e8bac53b883eef657e5c7961170c27944f68825e82a43625c5ccc51c2937","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"016e4353e0414c98c2ed2eb75df1509a38b457ddf000e9ff98125f82ea42c335","signature":"767c0245753b6e027a6f0550eb7b69bbd09d3f758aa1e9a07dcfe0992c0f629e"},{"version":"3ef6579bdd5883302539b5370c5670331f3584dff9e2ef14bab39991f00a29cb","signature":"1e894cd9840e407527a3f67119371f90d7ae5f8179905de7776a345ed4af6d32"},{"version":"e34737b913a2f8775f45e495e660016e0ab0fda68c579f2024d1b596dde2d81c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6677681f753ead74430e87a3398cc92c183570992c77804fc4215178ffc86116","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3b642738f6cd019494d207949a63b67b6ad9013bb3f1fb5f8fa36687879db5c4","signature":"f583f64899de9c7804febba31f2808aad09101b0d3342e42793c8588ed608f63"},{"version":"35a644763f50424937b17109eb552ddc767270a5526a0a827810ab86585dc53a","signature":"1b4159a10366adf4fd777a6bd595b4b846f1d3d37d07498c3498fd4b71e0f813"},{"version":"12fc2285fa58f8178c3fce7cf56795d11bdbc3974e264f31ff3cdcb689fd5813","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c81045d6db59c1c83c84797bc1f78422a691ced9ca9a3554e2f8ef9aacc7dcfa","signature":"441f2bb4f78eb9fb86d27aceee4d92908b532110aa6b7fac80adede58bab926d"},{"version":"56d498db61c90d4706b23d1b2235e8e8fb4c525a16c19c33cabe50812506a134","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"bce05280900b4eb3333d10064531569c9fcfce6aa7aeca1ca03b2120539892a5","signature":"c96264be178c5e42597043c462b21cbc073618f43d5e1e88bd32a516068a2380"},{"version":"26f7e680445f38787829c77c194f3df7741657f8e80e7b51588dfe74da7b2c7f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f5a22523635ca6f47c20b386b010ca1258aa19af5f4299f8752809c599315bcf","signature":"48c3a71b54800c134fcff4becfedca8347ff86645e004f8754d1e2b1385d1e9c"},{"version":"487012655811883dddb922cd44d08642d753adc1df21ba652e11ac7030aeede2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4f69209ce0e934946c859c4cc6248ef4a2dc528f5baf9b4fcdea5cf3e08d9d38","signature":"e8f8aa08e63443a0cc63ce2f9fc9582addd622528fad2ecfc413b2a91c688fac"},{"version":"041b81f9c2cc3f95588f5ef2da13fce1d895ddd5160979c84c01aaa8873145b4","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ad15d915012090304670ff62dd5e48d7694c11787f4884f51cf80f873dd40aa4","signature":"ef3c092bb7ed970d2273e55a61b12bc4741bca2219e1fe703350550c99ca6f42"},{"version":"cad40fd88fb3c219fc234d0d56bf87e8d3ceb86505f11e3714fc21c6d761cd59","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a51d9c3485a1f3fc48f7f04f771cd827828082e49c12754d7e5708719675449e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fb5ce2f104fde415289b4febf27da970e40afefe42b87b15db28818ca9f94132","signature":"ccefe690307d556c5e44dd7ffb7d23faa0a3388926925ed0352107cd2efc4d9c"},{"version":"6e568ff38a9d7070783b26b39b93df658c7466032f4ffa22431dfe2808b7a8bd","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3613723853e81414c2e1220e3e0a696a997cc49e4390060b6ac7fd91aec150e4","signature":"0334a8b41901a52fc9195c66c97cee6aaaecc5aae79b9fc31f245a5df48b3ecc"},{"version":"e4bba7f9edd598a76e24e59f3d47d09cb0295d6c52c8a89e8fa04dc6e6428b16","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"082900437524e8bc6903f0d26479f590af1814cc5080fc502e07d38c951500be","signature":"24564adc2074d75d218bfe8b711580df8848afbd3ce2890184cd98e76529d387"},{"version":"34eb44fd7813a487540b6afa04da3175388b1841aa3612569e8aa07652363b69","signature":"9bada675e959a3571ad60f30eaee87c92b05fea94beca1f7fd212dc65734177a"},{"version":"a76367ab8ebbbfb0db994a3d394b71b72c3b72fb69542222ace317e4b61f74ca","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cbeab042293806df5cb4e45ba714036477971cb40a2539b20c331a632c1a2c46","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"0f55b5907842f16784dca83f2c82ac05e1e8740f2ec866c95289e05061384e4b","signature":"93bf045e7f996840ff1a3e1fa340836585224d394c52de4e98ff79f8be816dd1"},{"version":"7c8545a7c4ec6978ebb4af07475d76007eb084c4f7f45aeba7ee817eeb4316e1","signature":"37eeb4730a8634d70c51bd1933939e66cba31f30600246487f3568cee7742b9c"},{"version":"852c367110c2934ed35a33ad276111aa9b3016ec92b36b86a37b43642ad9458d","signature":"92a24950b269736d532ea9daf0cdd8ea7361b70095f3468e59afea80af884516"},{"version":"5a2cdf6adeec348bbc876221be4367e8adff0bb78a5680ebd7d71e5c3bad6cc0","impliedFormat":99},{"version":"e004826eac62081f867c66dabd92d3ef7d126d93a70430a2c88429228c3ecc50","impliedFormat":99},{"version":"38d6857b58d2ac42442e396311c542062d4f0dad40f2adb496dd5fd0756ee400","impliedFormat":99},{"version":"34b7d1e2d15845cf08bcf5e3c01adbb92cea1ec27564ee249ba486cdfb28526c","impliedFormat":99},{"version":"0d0861810ff9d344ab37f055edbc4d14e68e2fa18e113ce8cc33aef9bb500b5f","signature":"2791178671f71d9b41ed9ef814e549d85cc6a77c61e2f8a87ec25f2176d9a3fc"},{"version":"992404964e9cefb3143cde9bc8e5eb5e7010c3a207f5bfa52df49287fce758be","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"968043e1fd7bb3c6b1c22d3341ede621d16a628b2bc4c35d1e43f0064ff7c1a6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6d261c7be483027c9d281ba71314350ad8ce66efabf46f7b6099f39fe3b8d218","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c02d45c2a6bc50186c1972f44c31ff22e887b30205ce84607bb4661c0fdc9846","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7fc06e1e53688bb32ccd86e730e08bacd38d9ad7ad006a234211b230825efb2d","signature":"d95aac1823e54b4183acab8f7fe3bec5dd7bd4aa297f56004fbcb972d299e377"},{"version":"fe93c474ab38ac02e30e3af073412b4f92b740152cf3a751fdaee8cbea982341","impliedFormat":1},{"version":"3255b97f3f24af29c79cc1aa88004efb13b6285ebdde0a567bf32e19bb65250d","impliedFormat":1},{"version":"1e00b8bf9e3766c958218cd6144ffe08418286f89ff44ba5a2cc830c03dd22c7","impliedFormat":1},{"version":"cc0e0705b17f5987925bf05b5a7da622a76ad691274a428cf18fb28b33a7a1cf","signature":"01e6799210215286acf67be8c15da37b72af300e0f7f32c7f11535415e25ee88"},{"version":"ab640e52df6129fa178d5c0f2860542954ea38af4b0801a92c3ac09f6a9eec7c","signature":"91212f9905f489a1993df856acac1939544f6166e4cafff1c4f0949e37a8a11d"},{"version":"c3d577953f04c0188d8b9c63b2748b814efda6440336fa49557f0079f5cf748a","impliedFormat":1},{"version":"787fe950e18951b7970ec98cb05b3d0b11fcdfeb2091a7ea481ac9e52bf6c086","impliedFormat":1},{"version":"13ceda04874f09091da1994ba5f58bf1e9439af93336616257691863560b3f13","impliedFormat":1},{"version":"488c53c963104e91a6f2a1f16cbaee1a963f6f4527f0051256740c94ed34d6bb","signature":"fb69d502157f1cf71cb8c737f6909c2e82f2a53b8157f840411444435f5da3d1"},{"version":"7075686875dce9990810c2dfeefc1d3e1dd29cd815389854746fcc457dfbdce7","signature":"c2f4c6ab17d07762713d80c4c29cba3cfffd690fe6c569a17c0be5d0d3e810f5"},{"version":"a941595362ff7e12adee1605aea8495d9bc96cd833d95c87f83cad2b5838165a","signature":"b67fa3b5b051ead6f5048d73c953d289234953f832922ffc4dfe293d5c6bfc98"},{"version":"c98b1727a4c0ccfbd4df609bad278f1af184a069d232f978a327d53110677480","signature":"1556f3a35ddd259c925802c27bac4fe626e489e685fc3ee1f3101169f02f993d"},{"version":"1114a96ff6bdee7270e584688b4c46a5be6e50c47e6d8d26e4a8649556a851c9","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"76cc225f61f545122672c27ff69aa27d1e7578d653c5fe942ebe88601cea0b02","signature":"87d223b2d0fc4ffc6f3bd5bbf3d4e036171c472cffb6a792c31427b714f4f442"},{"version":"06272d55719e7d65de722274ae4593bfe06a90f4924b8807e4e04cbd15fb43c1","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"46b2d594365b3ffa714de5625c0859471e14d1b010e24cce18be153d1f6efa8d","signature":"3ebf64d5f0b695aef10ddeeff762fc0216e05bcd3d9572fb8763859b2d74be41"},{"version":"f3415880499901a01feef00e1b3042f670dabad8b5a131c22994f5f951dbdf2f","signature":"28007b7d2b577a868c587c22500f2ab77490b6390909ffbdd3b04dac98e69a18"},{"version":"37d5e316ab9ae4c7ff7e1856e2ecdf4930f17d7b43523f73843dbf8d6d13a43c","signature":"711d67575686fe3e0ac16b0a6080ab554fa53447a33228be88bfd57323da61bb"},{"version":"58c88a6bf756bcb45f70e3c79ac4e08209093ad4f112df86d57acc500290e067","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fc51205c27f22f7194f3c026cdf19c5b27f19190a97955b9ffce0db45858e42f","signature":"ba90586e9f08bbe0d660358dcee98b83dadce2bfa013ca3e8d93f8d7924a9c66"},{"version":"54c008f175512ea8e8854d138dcf76b2af5e59e6816e82e87a360d76f3c7f820","signature":"34c21c211ba158af8c7cbdf93784d24a472017b37b792b1c2d8ae21c36488729"},{"version":"1eb5c0da9dcb448145de4b74e1b37b6a05da0e4ae0b393c7224af6ee7cccf913","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"8bc24afa3c5fb73fd0dc89f091d2cb65b9d54f7a90b37302e9bd679ba504b0e3","signature":"e5ff90224997311a3ef066d1d0e3a85f3e2b2348a035581ff7099304497d0775"},{"version":"51610870e75caaf1ff890f1fb949366cd7d843b4aa2e734c166bb307a78f33ea","signature":"2ed4659f7cb57cc7471545251d21c6f8ab503526ec15c1a991a3d5be96258c79"},{"version":"de3c85bccb34f80e5cfd3f5e63648f2f8bdc8c9d19a67b0807adb4cfd8793afc","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c064058bfb6150ce094497c75bc491e7f92389c2b1fed5f6923e7a035315d3a1","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c3936ac555912c80003fd9e659007f016ae2bc61fb8b91f696f860d19fd6e9bc","signature":"dc035ffa274dd975386f883b4d99361db8e73edd0fc77b1e4bc0d09be0c5074d"},{"version":"f20b8d5c86e426ce2d1505f4a3114e66411272aa8394e0ad323c0c3b1d1fadff","signature":"b41f35e5ae414583d72cc2aa3a17cc23d40f4b9e221f92acd74ed09bb3daaa63"},{"version":"74b006e51c1fe0198db4fd239bdfb2063fb3b0139bbb3dbaab79f323f42ba6bf","signature":"83057fe16cf05e5bf626fd4e46379506199130438abbd2ae42de6234bb202181"},{"version":"c247b5bb6d297cb7c9607645b1486b2f7b3ee0c05fac97d18e6daf4e3b25ebeb","signature":"5a224c6b95c526ad3f70b7b5894a2fe9b50cb5a9eed53b87806cf1045f8d7bab"},{"version":"dd7a9804bfd52806479d69214126956478367bb3a2c333b589ddcc253aae03d3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"882b28abe64dae4932c83ebb71e4155da340929fe08a2055f3e573ef17f70fc3","impliedFormat":1},{"version":"4a3e425808751200a7709671667ad3d7e7cbfd0a06d469cab42adf06c2601f4a","impliedFormat":1},{"version":"401da46338f5b4f97c2a5f8a0faaace045c51aabd751d2dc704159f64feafe89","impliedFormat":1},{"version":"c705d4594093bcde53fc292c5526aedd3145170ceba73a9476ee97de6a915fe2","impliedFormat":1},{"version":"65399deec596f31712911c2d81964d913370e0d4a04c51df29cc3c99c9ac298b","signature":"c8b6a1356346524d07db6d395ed25c816fb0935840b3f6af9296402392feca76"},{"version":"b44403d97ecd48d2f5ec3f3175a9a9dce873ec5d3797459ed057e7a1ad597d54","signature":"1fcf7139261418de9dce0edd9f8e95a8ace6fd591da1c95fb959e19e7c6f4281"},{"version":"01922ef0992b637b2a096856708c280e6e2b5085d3ff743c27e891e0d3d28ea7","signature":"450f56af343ef42c693dde50c0dbe427f37297afb67c4864f81dd7c69fdfbd8b"},{"version":"02ed4b9c64b599d8a0d9c242c9f7e43fa44ebc4cdda1b8143a29d2bfdcaebb44","signature":"5bfa909232756aeaa1797184b579b7b47f5f6917a0fdf3b2566fe4bc4afc72a8"},{"version":"f4adb32677aa22d47ed1048448f8974667250c8deb8135f321cad0cb4d0d4007","signature":"be914a2abd74279a5ff3c561f641f30569d1a2f7618fcd806cfdb8c1fad34326"},{"version":"15a1cea3d3fd19c8818aaac408d84096485d3f154eac44e129c2a2a2609d85ba","signature":"88a64ff66b36ff55ce621b22e512515acd895e815a065f2897813b5f194521d8"},{"version":"a0e1a608868e8805852e4e9274fc1e3e22573273a292b4e59f19892c495fa239","signature":"0f601b1cf9ca46ef05b387bc05b169852e0a2e3e30babe89a59d21af43187522"},{"version":"4d1b4f2a7c556f22c71dfce1be2455614fb9f838d4b9144a447a03bd514bdd5e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"20375a205b37d0f527f1f3fb6cc5d6c2076c1b57f74b9024f8153e0f3f0289a9","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3557b3416d97219e58a39fecce338b086bd42db6ce7ef701e8265783fbd20c6f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"83f21e09f1a6d980cc7cf83252deccd5d997e67266ae8bd450ef8899fcab1884","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"38757383a22721ebcf7a7430d10cd39967c0f896d758798906d29c8ab8722924","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f4722121739886d9694fe6d74b91f654b2b26459edd6e275fb5ff1a509bbc262","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"1c46f9542d2d4a44df84e20e37348c86abfb57804268f5236874fa8a8b7639f5","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6915db003807400f9c80754166096b1ca5552111f80eb58c311ab561cca84735","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ad42288f8c9ebfd4451e4256a2f091cfc26b958d29db3612c19efbbb476882a2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},"d1986184a09a52db8228cb2bb2a61a8c05c9354e5b93cec8e2628d8579c892d7",{"version":"e37704e8bdf72de83d6ca4620f748ebf6272afc9b748f4e541afffd32b0c2924","affectsGlobalScope":true},{"version":"4332f611f915908b335662f95ac6047288d3bc0b939e06d59e369b96a28eaa70","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},"d1986184a09a52db8228cb2bb2a61a8c05c9354e5b93cec8e2628d8579c892d7",{"version":"ec9db470620906cec5c2b53d821e2917355bfed3fd87cea28eafd5d6d7496459","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"8d7cbeea0454e05a3cdf3370c5df267072c4f1dc6c48a45a9ad750d7890443d7","affectsGlobalScope":true,"impliedFormat":99}],"root":[[559,561],661,695,[746,799],[804,809],813,814,[818,840],[845,865]],"options":{"allowJs":false,"esModuleInterop":true,"jsx":4,"module":99,"skipLibCheck":true,"strict":true,"target":4},"referencedMap":[[864,1],[559,2],[865,3],[861,4],[862,2],[863,5],[560,6],[561,7],[403,2],[566,2],[619,2],[800,2],[801,8],[802,9],[803,10],[679,2],[676,2],[675,2],[670,11],[681,12],[666,13],[677,14],[669,15],[668,16],[678,2],[673,17],[680,2],[674,18],[667,2],[665,19],[664,20],[663,13],[683,21],[662,2],[626,22],[624,2],[161,23],[162,23],[163,24],[101,25],[164,26],[165,27],[166,28],[99,2],[167,29],[168,30],[169,31],[170,32],[171,33],[172,34],[173,34],[174,35],[175,36],[176,37],[177,38],[102,2],[100,2],[178,39],[179,40],[180,41],[220,42],[181,43],[182,44],[183,43],[184,45],[185,46],[186,47],[187,48],[188,48],[189,48],[190,49],[191,50],[192,51],[193,52],[194,53],[195,54],[196,54],[197,55],[198,2],[199,2],[200,56],[201,57],[202,56],[203,58],[204,59],[205,60],[206,61],[207,62],[208,63],[209,64],[210,65],[211,66],[212,67],[213,68],[214,69],[215,70],[216,71],[217,72],[103,43],[104,2],[105,73],[106,74],[107,2],[108,75],[109,2],[152,76],[153,77],[154,78],[155,78],[156,79],[157,2],[158,26],[159,80],[160,77],[218,81],[219,82],[224,83],[488,84],[225,85],[223,86],[490,87],[489,88],[682,84],[221,89],[486,2],[222,90],[90,2],[92,91],[485,84],[255,84],[615,92],[614,2],[627,93],[648,94],[649,95],[647,2],[620,2],[633,96],[632,97],[644,96],[635,98],[637,99],[656,99],[636,100],[617,101],[616,2],[622,102],[623,103],[653,104],[629,105],[631,106],[652,2],[650,105],[630,2],[621,103],[628,2],[625,2],[91,2],[691,107],[693,108],[692,109],[690,110],[689,2],[842,111],[841,2],[843,112],[729,113],[698,114],[708,114],[699,114],[709,114],[700,114],[701,114],[716,114],[715,114],[717,114],[718,114],[710,114],[702,114],[711,114],[703,114],[712,114],[704,114],[706,114],[714,115],[707,114],[713,115],[719,115],[705,114],[720,114],[725,114],[726,114],[721,114],[697,2],[727,2],[723,114],[722,114],[724,114],[728,114],[606,2],[608,116],[607,2],[696,117],[815,118],[735,119],[734,120],[741,121],[743,122],[739,123],[738,124],[742,120],[736,125],[733,126],[744,127],[745,127],[737,128],[731,2],[732,129],[817,130],[816,131],[740,2],[511,132],[516,133],[523,134],[506,135],[259,2],[267,136],[407,137],[410,138],[382,2],[395,139],[402,140],[284,2],[384,2],[265,2],[381,141],[427,142],[266,2],[257,143],[409,144],[411,145],[412,146],[483,147],[376,148],[329,149],[389,150],[390,151],[388,152],[387,2],[383,153],[408,154],[268,155],[453,2],[454,156],[295,157],[269,158],[296,157],[332,157],[235,157],[405,159],[404,2],[394,160],[501,2],[244,2],[522,161],[461,162],[462,163],[458,164],[540,2],[359,2],[463,165],[459,166],[545,167],[544,168],[539,2],[310,2],[362,169],[361,2],[538,170],[460,84],[315,171],[322,172],[324,173],[314,2],[319,174],[321,175],[323,176],[318,177],[316,2],[320,178],[541,2],[537,2],[543,179],[542,2],[313,180],[532,181],[535,182],[303,183],[302,184],[301,185],[548,84],[300,186],[289,2],[550,2],[811,187],[810,2],[551,84],[552,188],[227,2],[391,189],[392,190],[393,191],[231,2],[396,2],[251,192],[226,2],[475,84],[233,193],[474,194],[473,195],[464,2],[465,2],[472,2],[467,2],[470,196],[466,2],[468,197],[471,198],[469,197],[264,2],[261,2],[262,157],[416,2],[421,199],[422,200],[420,201],[418,202],[419,203],[414,2],[481,165],[256,165],[510,204],[517,205],[521,206],[350,207],[349,2],[344,2],[497,208],[505,209],[377,210],[378,211],[456,212],[366,2],[479,213],[354,84],[371,214],[482,215],[367,2],[370,216],[368,2],[480,217],[477,218],[476,2],[478,2],[374,2],[452,219],[239,220],[352,221],[356,222],[372,223],[375,224],[364,225],[357,226],[504,227],[430,228],[348,229],[236,230],[503,231],[232,232],[423,233],[415,2],[424,234],[441,235],[413,2],[440,236],[98,2],[435,237],[260,2],[455,238],[431,2],[245,2],[247,2],[386,2],[439,239],[263,2],[287,240],[373,241],[293,242],[353,2],[438,2],[417,2],[443,243],[444,244],[385,2],[446,245],[448,246],[447,247],[397,2],[437,230],[450,248],[347,249],[436,250],[442,251],[272,2],[276,2],[275,2],[274,2],[279,2],[273,2],[282,2],[281,2],[278,2],[277,2],[280,2],[283,252],[271,2],[339,253],[338,2],[343,254],[340,255],[342,256],[345,254],[341,255],[252,257],[331,258],[500,259],[498,2],[527,260],[529,261],[493,262],[528,263],[240,264],[237,264],[270,2],[254,265],[253,266],[249,267],[250,268],[258,269],[286,269],[297,269],[333,270],[298,270],[242,271],[241,2],[337,272],[336,273],[335,274],[334,275],[243,276],[484,277],[285,278],[492,279],[457,280],[487,281],[491,282],[380,283],[379,284],[360,285],[346,286],[328,287],[330,288],[327,289],[449,290],[351,2],[515,2],[248,291],[451,292],[499,293],[358,2],[288,294],[365,295],[363,296],[290,297],[425,298],[494,2],[291,299],[426,299],[513,2],[512,2],[514,2],[496,2],[495,2],[428,300],[355,2],[325,301],[246,302],[304,2],[230,303],[292,2],[519,84],[229,2],[531,304],[312,84],[525,165],[311,305],[508,306],[309,304],[234,2],[533,307],[307,84],[308,84],[299,2],[228,2],[306,308],[305,309],[294,310],[369,52],[429,52],[445,2],[433,311],[432,2],[317,180],[238,2],[326,84],[502,192],[509,312],[93,84],[96,313],[97,314],[94,84],[95,2],[406,74],[401,315],[400,2],[399,316],[398,2],[507,317],[518,318],[520,319],[524,320],[812,321],[526,322],[530,323],[558,324],[534,324],[557,325],[536,326],[546,327],[547,328],[549,329],[553,330],[556,192],[555,2],[554,331],[730,332],[602,333],[600,334],[601,335],[589,336],[590,334],[597,337],[588,338],[593,339],[603,2],[594,340],[599,341],[605,342],[604,343],[587,344],[595,345],[596,346],[591,347],[598,333],[592,348],[672,349],[671,2],[844,350],[611,351],[574,352],[575,353],[578,354],[567,355],[577,356],[573,357],[565,2],[579,358],[580,359],[568,2],[569,2],[571,360],[570,2],[572,361],[434,362],[586,2],[645,2],[618,2],[88,2],[89,2],[14,2],[15,2],[17,2],[16,2],[2,2],[18,2],[19,2],[20,2],[21,2],[22,2],[23,2],[24,2],[25,2],[3,2],[26,2],[27,2],[4,2],[28,2],[32,2],[29,2],[30,2],[31,2],[33,2],[34,2],[35,2],[5,2],[36,2],[37,2],[38,2],[39,2],[6,2],[43,2],[40,2],[41,2],[42,2],[44,2],[7,2],[45,2],[50,2],[51,2],[46,2],[47,2],[48,2],[49,2],[8,2],[55,2],[52,2],[53,2],[54,2],[56,2],[9,2],[57,2],[58,2],[59,2],[61,2],[60,2],[62,2],[63,2],[10,2],[64,2],[65,2],[66,2],[11,2],[67,2],[68,2],[69,2],[70,2],[71,2],[72,2],[12,2],[73,2],[74,2],[75,2],[76,2],[77,2],[1,2],[78,2],[79,2],[13,2],[80,2],[81,2],[82,2],[83,2],[84,2],[85,2],[86,2],[87,2],[128,363],[140,364],[125,365],[141,366],[150,367],[116,368],[117,369],[115,370],[149,331],[144,371],[148,372],[119,373],[137,374],[118,375],[147,376],[113,377],[114,371],[120,378],[121,2],[127,379],[124,378],[111,380],[151,381],[142,382],[131,383],[130,378],[132,384],[135,385],[129,386],[133,387],[145,331],[122,388],[123,389],[136,390],[112,366],[139,391],[138,378],[126,389],[134,392],[143,2],[110,2],[146,393],[563,394],[613,395],[582,396],[564,394],[562,2],[581,397],[612,2],[610,2],[583,2],[609,398],[576,399],[585,2],[584,400],[655,401],[660,402],[654,403],[646,404],[642,405],[638,406],[651,2],[639,98],[687,407],[684,408],[658,409],[657,410],[640,411],[686,412],[634,2],[641,413],[659,414],[694,415],[688,416],[866,417],[685,2],[643,2],[822,418],[824,419],[823,420],[825,421],[828,422],[827,423],[747,424],[751,425],[750,426],[754,427],[753,426],[755,428],[752,426],[757,429],[756,426],[759,430],[758,426],[761,431],[760,426],[763,432],[765,433],[764,426],[762,426],[768,434],[767,426],[773,435],[772,426],[774,436],[771,426],[770,437],[769,426],[776,426],[777,438],[775,426],[779,439],[778,426],[781,440],[780,426],[783,441],[782,426],[785,442],[784,426],[787,443],[786,426],[788,444],[766,426],[790,445],[789,426],[792,446],[791,447],[795,448],[794,426],[796,449],[793,426],[831,450],[830,451],[834,452],[833,453],[835,454],[832,453],[836,455],[814,456],[840,457],[839,458],[821,459],[849,460],[851,461],[850,462],[852,463],[853,464],[854,465],[855,466],[856,467],[819,468],[857,469],[858,470],[838,471],[837,472],[826,165],[859,473],[845,474],[846,475],[847,476],[848,477],[813,478],[829,479],[820,165],[818,480],[805,481],[806,482],[807,483],[860,484],[808,485],[749,486],[746,487],[798,488],[799,475],[748,489],[804,490],[797,475],[809,489],[661,491],[695,492]],"affectedFilesPendingEmit":[865,863,561,822,824,823,825,828,827,747,751,750,754,753,755,752,757,756,759,758,761,760,763,765,764,762,768,767,773,772,774,771,770,769,776,777,775,779,778,781,780,783,782,785,784,787,786,788,766,790,789,792,791,795,794,796,793,831,830,834,833,835,832,836,814,840,839,821,849,851,850,852,853,854,855,856,819,857,858,838,837,826,859,845,846,847,848,813,829,820,818,805,806,807,860,808,749,746,798,799,748,804,797,809,661,695],"version":"6.0.3"} diff --git a/ingestion/plugins/bluesky.py b/ingestion/plugins/bluesky.py index 4ef60f1c..3c3f933a 100644 --- a/ingestion/plugins/bluesky.py +++ b/ingestion/plugins/bluesky.py @@ -206,11 +206,9 @@ def _nested_value(value, *path: str): def _client(self) -> Client: """Create a public or authenticated ATProto client for the project.""" - from core.plugins.bluesky import Client as BlueskyClient - credentials = self._credentials() if credentials is None: - return BlueskyClient(base_url=PUBLIC_APPVIEW_BASE_URL) + return Client(base_url=PUBLIC_APPVIEW_BASE_URL) return self._authenticated_client_for_credentials(credentials) def _credentials(self) -> BlueskyCredentials | None: @@ -226,11 +224,9 @@ def _authenticated_client_for_credentials( ) -> Client: """Build an authenticated client from a stored credential record.""" - from core.plugins.bluesky import Client as BlueskyClient - if not credentials.has_app_password(): raise RuntimeError("Bluesky credentials are missing an app password.") - client = BlueskyClient(base_url=credentials.client_base_url) + client = Client(base_url=credentials.client_base_url) client.login(login=credentials.handle, password=credentials.get_app_password()) return client diff --git a/ingestion/plugins/mastodon.py b/ingestion/plugins/mastodon.py index 23c9b5d8..77380040 100644 --- a/ingestion/plugins/mastodon.py +++ b/ingestion/plugins/mastodon.py @@ -12,6 +12,7 @@ from django.utils import timezone from django.utils.dateparse import parse_datetime from django.utils.html import strip_tags +from mastodon import Mastodon from ingestion.plugins.base import ContentItem, SourcePlugin from projects.model_support import ( @@ -247,8 +248,6 @@ def _build_content_item(self, status: Any, published_date: datetime) -> ContentI def _client(self): """Create an anonymous or authenticated Mastodon client.""" - from core.plugins.mastodon import Mastodon - credentials = self._credentials() if credentials is None: return Mastodon(api_base_url=self._instance_url()) @@ -278,8 +277,6 @@ def _instance_url(self) -> str: def _authenticated_client_for_credentials(credentials: MastodonCredentials): """Build an authenticated client from a stored credential record.""" - from core.plugins.mastodon import Mastodon - if not credentials.has_access_token(): raise RuntimeError("Mastodon credentials are missing an access token.") return Mastodon( diff --git a/ingestion/tasks.py b/ingestion/tasks.py index 4b956752..dfdeb198 100644 --- a/ingestion/tasks.py +++ b/ingestion/tasks.py @@ -7,8 +7,8 @@ from django.db.models import Q from django.utils import timezone +from content.deduplication import canonicalize_url from content.models import Content -from core.deduplication import canonicalize_url from ingestion.models import IngestionRun, RunStatus from ingestion.plugins import get_plugin_for_source_config from projects.models import SourceConfig @@ -135,11 +135,9 @@ def _match_entity_for_item(plugin, item): def _schedule_content_processing(content: Content) -> None: """Ensure a content row is embedded before it enters the AI pipeline.""" - from core.tasks import ( - assign_content_to_topic_cluster, - process_content, - upsert_content_embedding, - ) + from core.embeddings import upsert_content_embedding + from core.tasks import process_content + from trends.tasks import assign_content_to_topic_cluster upsert_content_embedding(content) assign_content_to_topic_cluster(content.id) diff --git a/ingestion/tests/__init__.py b/ingestion/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ingestion/tests/test_admin.py b/ingestion/tests/test_admin.py new file mode 100644 index 00000000..9245b655 --- /dev/null +++ b/ingestion/tests/test_admin.py @@ -0,0 +1,135 @@ +from datetime import timedelta +from types import SimpleNamespace +from typing import Any, cast + +import pytest +from django.contrib.admin.sites import AdminSite +from django.http import HttpRequest +from django.test import RequestFactory +from django.utils import timezone + +from ingestion.admin import IngestionRunAdmin +from ingestion.models import IngestionRun, RunStatus +from projects.model_support import SourcePluginName +from projects.models import Project + +pytestmark = pytest.mark.django_db + + +def _create_user(user_model: Any, **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +def _request(query_params: dict[str, str] | None = None) -> HttpRequest: + """Build a typed request object for admin actions and filters.""" + + return RequestFactory().get("/admin/", data=query_params or {}) + + +def _context(response: object) -> dict[str, Any]: + """Cast admin changelist extra_context payloads for typed assertions.""" + + return cast(dict[str, Any], response) + + +def _dashboard_stats(response: object) -> list[dict[str, Any]]: + """Return typed dashboard stats rows from a changelist extra_context payload.""" + + return cast(list[dict[str, Any]], _context(response)["dashboard_stats"]) + + +@pytest.fixture +def source_admin_context(django_user_model): + user = _create_user( + django_user_model, username="admin-owner", password="testpass123" + ) + project = Project.objects.create(name="Admin Project", topic_description="Infra") + return SimpleNamespace(user=user, project=project) + + +def test_ingestion_run_display_efficiency_renders_without_django6_format_error( + source_admin_context, +): + run = IngestionRun.objects.create( + project=source_admin_context.project, + plugin_name=SourcePluginName.RSS, + status=RunStatus.SUCCESS, + items_fetched=12, + items_ingested=9, + ) + admin_instance = IngestionRunAdmin(IngestionRun, AdminSite()) + + rendered = admin_instance.display_efficiency(run) + + assert "75%" in rendered + + +def test_ingestion_run_display_duration_handles_running_and_completed( + source_admin_context, +): + running_run = IngestionRun.objects.create( + project=source_admin_context.project, + plugin_name=SourcePluginName.RSS, + status=RunStatus.RUNNING, + items_fetched=0, + items_ingested=0, + ) + completed_run = IngestionRun.objects.create( + project=source_admin_context.project, + plugin_name=SourcePluginName.RSS, + status=RunStatus.SUCCESS, + items_fetched=10, + items_ingested=10, + ) + completed_run.started_at = timezone.now() - timedelta(minutes=3, seconds=5) + completed_run.completed_at = completed_run.started_at + timedelta( + minutes=3, seconds=5 + ) + completed_run.save(update_fields=["started_at", "completed_at"]) + admin_instance = IngestionRunAdmin(IngestionRun, AdminSite()) + + assert admin_instance.display_duration(running_run) == "In Progress..." + assert admin_instance.display_duration(completed_run) == "3m 5s" + + +def test_ingestion_run_admin_status_efficiency_and_dashboard_branches( + source_admin_context, mocker +): + IngestionRun.objects.create( + project=source_admin_context.project, + plugin_name=SourcePluginName.RSS, + status="failed", + items_fetched=0, + items_ingested=0, + ) + running_run = IngestionRun.objects.create( + project=source_admin_context.project, + plugin_name=SourcePluginName.RSS, + status=RunStatus.RUNNING, + items_fetched=5, + items_ingested=5, + ) + admin_instance = IngestionRunAdmin(IngestionRun, AdminSite()) + super_changelist_view = mocker.patch( + "ingestion.admin.ModelAdmin.changelist_view", + side_effect=lambda request, extra_context=None: extra_context, + ) + + response = admin_instance.changelist_view(_request()) + dashboard_stats = _dashboard_stats(response) + + assert "danger" in admin_instance.display_status( + IngestionRun.objects.filter(status="failed").first() + ) + assert ( + admin_instance.display_efficiency( + IngestionRun.objects.filter(status="failed").first() + ) + == "0/0" + ) + assert "info" in admin_instance.display_status(running_run) + super_changelist_view.assert_called_once() + assert dashboard_stats[0]["value"] == "5" + assert dashboard_stats[1]["color"] == "warning" diff --git a/core/tests/test_bluesky.py b/ingestion/tests/test_bluesky.py similarity index 97% rename from core/tests/test_bluesky.py rename to ingestion/tests/test_bluesky.py index f6d6c99e..34129ae3 100644 --- a/core/tests/test_bluesky.py +++ b/ingestion/tests/test_bluesky.py @@ -3,11 +3,9 @@ import pytest -from core.models import ( - Entity, -) -from core.plugins.bluesky import BlueskySourcePlugin +from ingestion.plugins.bluesky import BlueskySourcePlugin from ingestion.plugins.base import ContentItem +from entities.models import Entity from projects.model_support import SourcePluginName from projects.models import BlueskyCredentials, Project, SourceConfig @@ -221,7 +219,7 @@ def test_bluesky_client_uses_authenticated_project_credentials(bluesky_context, credentials.set_app_password("app-password") credentials.save() client = mocker.Mock() - client_cls = mocker.patch("core.plugins.bluesky.Client", return_value=client) + client_cls = mocker.patch("ingestion.plugins.bluesky.Client", return_value=client) plugin = BlueskySourcePlugin(bluesky_context.source_config) @@ -260,7 +258,7 @@ def test_bluesky_verify_credentials_uses_authenticated_session_check( credentials.set_app_password("app-password") credentials.save() client = mocker.Mock() - client_cls = mocker.patch("core.plugins.bluesky.Client", return_value=client) + client_cls = mocker.patch("ingestion.plugins.bluesky.Client", return_value=client) BlueskySourcePlugin.verify_credentials(credentials) diff --git a/core/tests/test_mastodon.py b/ingestion/tests/test_mastodon.py similarity index 97% rename from core/tests/test_mastodon.py rename to ingestion/tests/test_mastodon.py index 24857977..591a1922 100644 --- a/core/tests/test_mastodon.py +++ b/ingestion/tests/test_mastodon.py @@ -3,9 +3,9 @@ import pytest -from core.models import Entity -from core.plugins.mastodon import MastodonSourcePlugin +from ingestion.plugins.mastodon import MastodonSourcePlugin from ingestion.plugins.base import ContentItem +from entities.models import Entity from projects.model_support import SourcePluginName from projects.models import MastodonCredentials, Project, SourceConfig @@ -268,7 +268,9 @@ def test_mastodon_client_uses_authenticated_project_credentials( credentials.set_access_token("access-token") credentials.save() client = mocker.Mock() - mastodon_cls = mocker.patch("core.plugins.mastodon.Mastodon", return_value=client) + mastodon_cls = mocker.patch( + "ingestion.plugins.mastodon.Mastodon", return_value=client + ) plugin = MastodonSourcePlugin(mastodon_context.source_config) @@ -292,7 +294,9 @@ def test_mastodon_verify_credentials_updates_verified_account(mastodon_context, "username": "alice", "url": "https://hachyderm.io/@alice", } - mastodon_cls = mocker.patch("core.plugins.mastodon.Mastodon", return_value=client) + mastodon_cls = mocker.patch( + "ingestion.plugins.mastodon.Mastodon", return_value=client + ) MastodonSourcePlugin.verify_credentials(credentials) diff --git a/core/tests/test_plugin_base.py b/ingestion/tests/test_plugin_base.py similarity index 97% rename from core/tests/test_plugin_base.py rename to ingestion/tests/test_plugin_base.py index e16d83e0..0c208060 100644 --- a/core/tests/test_plugin_base.py +++ b/ingestion/tests/test_plugin_base.py @@ -4,8 +4,9 @@ import pytest -from core.models import Entity, Project -from core.plugins.base import ContentItem, SourcePlugin +from entities.models import Entity +from ingestion.plugins.base import ContentItem, SourcePlugin +from projects.models import Project pytestmark = pytest.mark.django_db diff --git a/core/tests/test_reddit.py b/ingestion/tests/test_reddit.py similarity index 95% rename from core/tests/test_reddit.py rename to ingestion/tests/test_reddit.py index 7fe509c1..cd225856 100644 --- a/core/tests/test_reddit.py +++ b/ingestion/tests/test_reddit.py @@ -3,8 +3,8 @@ import pytest -from core.plugins.reddit import RedditSourcePlugin -from core.plugins.registry import validate_plugin_config +from ingestion.plugins.reddit import RedditSourcePlugin +from ingestion.plugins.registry import validate_plugin_config from projects.model_support import SourcePluginName from projects.models import Project, SourceConfig @@ -113,7 +113,7 @@ def test_reddit_client_builds_praw_client(settings, mocker): settings.REDDIT_CLIENT_SECRET = "client-secret" settings.REDDIT_USER_AGENT = "newsletter-maker-test" reddit_cls = mocker.patch( - "core.plugins.reddit.praw.Reddit", return_value="reddit-client" + "ingestion.plugins.reddit.praw.Reddit", return_value="reddit-client" ) client = RedditSourcePlugin._client() diff --git a/core/tests/test_rss.py b/ingestion/tests/test_rss.py similarity index 90% rename from core/tests/test_rss.py rename to ingestion/tests/test_rss.py index 01fdab3a..a488937b 100644 --- a/core/tests/test_rss.py +++ b/ingestion/tests/test_rss.py @@ -4,7 +4,7 @@ import pytest -from core.plugins.rss import RSSSourcePlugin +from ingestion.plugins.rss import RSSSourcePlugin from projects.model_support import SourcePluginName from projects.models import Project, SourceConfig @@ -46,7 +46,7 @@ def test_rss_fetch_new_content_filters_invalid_and_old_entries(rss_context, mock ), ] ) - mocker.patch("core.plugins.rss.feedparser.parse", return_value=parsed_feed) + mocker.patch("ingestion.plugins.rss.feedparser.parse", return_value=parsed_feed) plugin = RSSSourcePlugin(rss_context.source_config) items = plugin.fetch_new_content(since=datetime(2026, 4, 28, 11, 0, tzinfo=UTC)) @@ -71,7 +71,7 @@ def test_rss_fetch_new_content_uses_title_when_summary_and_description_missing( ) ] ) - mocker.patch("core.plugins.rss.feedparser.parse", return_value=parsed_feed) + mocker.patch("ingestion.plugins.rss.feedparser.parse", return_value=parsed_feed) plugin = RSSSourcePlugin(rss_context.source_config) items = plugin.fetch_new_content(since=None) @@ -82,7 +82,8 @@ def test_rss_fetch_new_content_uses_title_when_summary_and_description_missing( def test_rss_health_check_returns_false_for_empty_feed(rss_context, mocker): mocker.patch( - "core.plugins.rss.feedparser.parse", return_value=SimpleNamespace(entries=[]) + "ingestion.plugins.rss.feedparser.parse", + return_value=SimpleNamespace(entries=[]), ) plugin = RSSSourcePlugin(rss_context.source_config) diff --git a/ingestion/tests/test_tasks.py b/ingestion/tests/test_tasks.py new file mode 100644 index 00000000..3f42e975 --- /dev/null +++ b/ingestion/tests/test_tasks.py @@ -0,0 +1,418 @@ +from datetime import datetime, timezone +from types import SimpleNamespace + +import pytest +from django.db.models import Model + +from content.models import Content +from entities.models import Entity +from ingestion.models import IngestionRun, RunStatus +from ingestion.tasks import _ingest_source_config, run_all_ingestions, run_ingestion +from projects.model_support import SourcePluginName +from projects.models import Project, SourceConfig + +pytestmark = pytest.mark.django_db + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed task assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +@pytest.fixture +def source_plugin_context(django_user_model): + user = django_user_model.objects.create_user( + username="plugin-owner", password="testpass123" + ) + project = Project.objects.create(name="Plugin Project", topic_description="Infra") + entity = Entity.objects.create( + project=project, + name="Example", + type="vendor", + website_url="https://example.com", + ) + return SimpleNamespace(user=user, project=project, entity=entity) + + +def test_run_ingestion_creates_content_from_rss_entries(source_plugin_context, mocker): + upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") + process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + parse_mock = mocker.patch("ingestion.plugins.rss.feedparser.parse") + source_config = SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.RSS, + config={"feed_url": "https://example.com/feed.xml"}, + ) + parse_mock.return_value = SimpleNamespace( + entries=[ + SimpleNamespace( + link="https://example.com/post-1", + title="Example Post", + author="Author", + summary="Summary", + published_parsed=datetime( + 2026, 4, 20, 12, 0, tzinfo=timezone.utc + ).timetuple(), + ) + ] + ) + + result = run_ingestion(_require_pk(source_config)) + + assert result["items_fetched"] == 1 + assert result["items_ingested"] == 1 + content = Content.objects.get(url="https://example.com/post-1") + assert content.project == source_plugin_context.project + assert content.entity == source_plugin_context.entity + upsert_embedding_mock.assert_called_once_with(content) + process_content_delay_mock.assert_called_once_with(_require_pk(content)) + assert ( + SourceConfig.objects.get(pk=_require_pk(source_config)).last_fetched_at + is not None + ) + ingestion_run = IngestionRun.objects.get( + project=source_plugin_context.project, plugin_name=SourcePluginName.RSS + ) + assert ingestion_run.status == RunStatus.SUCCESS + + +def test_run_ingestion_skips_same_source_duplicate_urls(source_plugin_context, mocker): + upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") + process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + parse_mock = mocker.patch("ingestion.plugins.rss.feedparser.parse") + source_config = SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.RSS, + config={"feed_url": "https://example.com/feed.xml"}, + ) + Content.objects.create( + project=source_plugin_context.project, + entity=source_plugin_context.entity, + url="https://example.com/post-1", + title="Existing", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-20T12:00:00Z", + content_text="Existing content", + ) + parse_mock.return_value = SimpleNamespace( + entries=[ + SimpleNamespace( + link="https://example.com/post-1", + title="Duplicate Post", + author="Author", + summary="Summary", + published_parsed=datetime( + 2026, 4, 20, 12, 0, tzinfo=timezone.utc + ).timetuple(), + ) + ] + ) + + result = run_ingestion(_require_pk(source_config)) + + assert result["items_fetched"] == 1 + assert result["items_ingested"] == 0 + upsert_embedding_mock.assert_not_called() + process_content_delay_mock.assert_not_called() + assert Content.objects.filter(url="https://example.com/post-1").count() == 1 + + +def test_ingest_source_config_allows_cross_plugin_duplicate_urls_for_pipeline_dedup( + source_plugin_context, mocker +): + upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") + process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + source_config = SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.REDDIT, + config={"subreddit": "python", "listing": "new", "limit": 5}, + ) + Content.objects.create( + project=source_plugin_context.project, + entity=source_plugin_context.entity, + url="https://example.com/post-1", + canonical_url="https://example.com/post-1", + title="Existing RSS Item", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-20T12:00:00Z", + content_text="Existing content", + ) + plugin = SimpleNamespace( + fetch_new_content=lambda since: [ + SimpleNamespace( + url="https://example.com/post-1", + title="Reddit duplicate that should still enter the pipeline", + author="redditor", + published_date=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), + content_text="A community post linking to the same article.", + source_plugin=SourcePluginName.REDDIT, + source_metadata={}, + ) + ], + match_entity_for_url=lambda url: None, + ) + mocker.patch("ingestion.tasks.get_plugin_for_source_config", return_value=plugin) + + items_fetched, items_ingested = _ingest_source_config(source_config) + + assert items_fetched == 1 + assert items_ingested == 1 + assert Content.objects.filter(project=source_plugin_context.project).count() == 2 + upsert_embedding_mock.assert_called_once() + process_content_delay_mock.assert_called_once() + + +def test_run_ingestion_creates_content_from_reddit_posts(source_plugin_context, mocker): + upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") + process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + reddit_mock = mocker.patch("ingestion.plugins.reddit.praw.Reddit") + source_config = SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.REDDIT, + config={"subreddit": "python", "listing": "new", "limit": 5}, + ) + submission = SimpleNamespace( + id="abc123", + url="https://reddit.com/r/python/comments/abc123/test", + permalink="/r/python/comments/abc123/test", + title="Reddit Post", + selftext="Post body", + author="redditor", + created_utc=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc).timestamp(), + ) + subreddit = SimpleNamespace( + new=lambda limit: iter([submission]), hot=lambda limit: iter([]) + ) + reddit_mock.return_value.subreddit.return_value = subreddit + + result = run_ingestion(_require_pk(source_config)) + + assert result["items_fetched"] == 1 + assert result["items_ingested"] == 1 + content = Content.objects.get(title="Reddit Post") + upsert_embedding_mock.assert_called_once_with(content) + process_content_delay_mock.assert_called_once_with(_require_pk(content)) + assert content.source_plugin == SourcePluginName.REDDIT + assert content.entity is None + + +def test_ingest_source_config_deduplicates_bluesky_posts_by_post_uri( + source_plugin_context, mocker +): + upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") + process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + source_config = SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.BLUESKY, + config={"author_handle": "example.bsky.social"}, + ) + Content.objects.create( + project=source_plugin_context.project, + entity=source_plugin_context.entity, + url="https://example.com/existing-article", + title="Existing Bluesky Post", + author="example.bsky.social", + source_plugin=SourcePluginName.BLUESKY, + published_date="2026-04-20T12:00:00Z", + content_text="Existing content", + source_metadata={"post_uri": "at://did:plc:author/app.bsky.feed.post/abc123"}, + ) + plugin = SimpleNamespace( + fetch_new_content=lambda since: [ + SimpleNamespace( + url="https://example.com/new-canonical-url", + title="Duplicate Bluesky Post", + author="example.bsky.social", + published_date=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), + content_text="Duplicate content", + source_plugin=SourcePluginName.BLUESKY, + source_metadata={ + "author_handle": "example.bsky.social", + "post_uri": "at://did:plc:author/app.bsky.feed.post/abc123", + }, + ) + ], + match_entity_for_item=lambda item: source_plugin_context.entity, + ) + mocker.patch("ingestion.tasks.get_plugin_for_source_config", return_value=plugin) + + items_fetched, items_ingested = _ingest_source_config(source_config) + + assert items_fetched == 1 + assert items_ingested == 0 + assert Content.objects.filter(project=source_plugin_context.project).count() == 1 + upsert_embedding_mock.assert_not_called() + process_content_delay_mock.assert_not_called() + + +def test_ingest_source_config_deduplicates_mastodon_statuses_by_status_uri( + source_plugin_context, mocker +): + upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") + process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + source_config = SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.MASTODON, + config={ + "instance_url": "https://hachyderm.io", + "hashtag": "platformengineering", + }, + ) + Content.objects.create( + project=source_plugin_context.project, + entity=source_plugin_context.entity, + url="https://example.com/existing-article", + title="Existing Mastodon Status", + author="Alice Example", + source_plugin=SourcePluginName.MASTODON, + published_date="2026-04-20T12:00:00Z", + content_text="Existing content", + source_metadata={ + "status_uri": "https://hachyderm.io/users/alice/statuses/abc123" + }, + ) + plugin = SimpleNamespace( + fetch_new_content=lambda since: [ + SimpleNamespace( + url="https://example.com/new-canonical-url", + title="Duplicate Mastodon Status", + author="Alice Example", + published_date=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), + content_text="Duplicate content", + source_plugin=SourcePluginName.MASTODON, + source_metadata={ + "author_acct": "alice@hachyderm.io", + "status_uri": "https://hachyderm.io/users/alice/statuses/abc123", + }, + ) + ], + match_entity_for_item=lambda item: source_plugin_context.entity, + ) + mocker.patch("ingestion.tasks.get_plugin_for_source_config", return_value=plugin) + + items_fetched, items_ingested = _ingest_source_config(source_config) + + assert items_fetched == 1 + assert items_ingested == 0 + assert Content.objects.filter(project=source_plugin_context.project).count() == 1 + upsert_embedding_mock.assert_not_called() + process_content_delay_mock.assert_not_called() + + +def test_run_all_ingestions_enqueues_active_source_configs( + source_plugin_context, mocker +): + delay_mock = mocker.patch("ingestion.tasks.run_ingestion.delay") + active_one = SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.RSS, + config={"feed_url": "https://example.com/feed.xml"}, + ) + active_two = SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.REDDIT, + config={"subreddit": "python"}, + ) + SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.RSS, + config={"feed_url": "https://example.com/inactive.xml"}, + is_active=False, + ) + + enqueued_count = run_all_ingestions() + + assert enqueued_count == 2 + delay_mock.assert_any_call(_require_pk(active_one)) + delay_mock.assert_any_call(_require_pk(active_two)) + assert delay_mock.call_count == 2 + + +def test_run_all_ingestions_executes_inline_when_eager( + source_plugin_context, settings, mocker +): + settings.CELERY_TASK_ALWAYS_EAGER = True + run_ingestion_mock = mocker.patch("ingestion.tasks.run_ingestion") + delay_mock = mocker.patch("ingestion.tasks.run_ingestion.delay") + active_one = SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.RSS, + config={"feed_url": "https://example.com/feed.xml"}, + ) + active_two = SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.REDDIT, + config={"subreddit": "python"}, + ) + + enqueued_count = run_all_ingestions() + + assert enqueued_count == 2 + run_ingestion_mock.assert_any_call(_require_pk(active_one)) + run_ingestion_mock.assert_any_call(_require_pk(active_two)) + assert run_ingestion_mock.call_count == 2 + delay_mock.assert_not_called() + + +def test_run_ingestion_marks_failure_when_plugin_errors(source_plugin_context, mocker): + parse_mock = mocker.patch("ingestion.plugins.rss.feedparser.parse") + source_config = SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.RSS, + config={"feed_url": "https://example.com/feed.xml"}, + ) + parse_mock.side_effect = RuntimeError("feed unavailable") + + with pytest.raises(RuntimeError, match="feed unavailable"): + run_ingestion(_require_pk(source_config)) + + ingestion_run = IngestionRun.objects.get( + project=source_plugin_context.project, plugin_name=SourcePluginName.RSS + ) + assert ingestion_run.status == RunStatus.FAILED + assert ingestion_run.error_message == "feed unavailable" + + +def test_ingest_source_config_truncates_fields_and_processes_inline( + source_plugin_context, settings, mocker +): + settings.CELERY_TASK_ALWAYS_EAGER = True + plugin = mocker.Mock() + plugin.fetch_new_content.return_value = [ + SimpleNamespace( + url="https://example.com/post-long", + title="T" * 600, + author="A" * 300, + source_plugin=SourcePluginName.RSS, + published_date=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), + content_text="Summary", + ) + ] + plugin.match_entity_for_url.return_value = source_plugin_context.entity + source_config = SourceConfig.objects.create( + project=source_plugin_context.project, + plugin_name=SourcePluginName.RSS, + config={"feed_url": "https://example.com/feed.xml"}, + ) + mocker.patch("ingestion.tasks.get_plugin_for_source_config", return_value=plugin) + upsert_mock = mocker.patch("core.embeddings.upsert_content_embedding") + process_mock = mocker.patch("core.tasks.process_content") + delay_mock = mocker.patch("core.tasks.process_content.delay") + + items_fetched, items_ingested = _ingest_source_config(source_config) + + created = Content.objects.get(url="https://example.com/post-long") + assert items_fetched == 1 + assert items_ingested == 1 + assert created.entity == source_plugin_context.entity + assert len(created.title) == 512 + assert len(created.author) == 255 + upsert_mock.assert_called_once_with(created) + process_mock.assert_called_once_with(_require_pk(created)) + delay_mock.assert_not_called() diff --git a/core/newsletter_extraction.py b/newsletters/extraction.py similarity index 100% rename from core/newsletter_extraction.py rename to newsletters/extraction.py diff --git a/newsletters/intake.py b/newsletters/intake.py index 0131d553..d8fa7451 100644 --- a/newsletters/intake.py +++ b/newsletters/intake.py @@ -13,8 +13,8 @@ from django.db.models import Model from django.urls import reverse -from core.newsletter_extraction import extract_newsletter_items from core.settings_types import CoreSettings +from newsletters.extraction import extract_newsletter_items from newsletters.models import IntakeAllowlist, NewsletterIntake from projects.models import Project @@ -265,8 +265,6 @@ def process_inbound_newsletter( ) -> dict[str, Any]: """Persist and route one inbound newsletter message.""" - from core import newsletters as core_newsletters - project = _find_intake_project(recipients) if project is None: return {"status": "ignored", "reason": "no_matching_project"} @@ -297,11 +295,11 @@ def process_inbound_newsletter( ) if allowlist.is_confirmed: - core_newsletters.queue_newsletter_intake(intake_id) + queue_newsletter_intake(intake_id) return {"id": intake_id, "status": intake.status} if allowlist_created: - core_newsletters.send_confirmation_email( + send_confirmation_email( to_email=normalized_sender_email, confirm_url=build_confirmation_url(allowlist.confirmation_token), project_name=project.name, diff --git a/newsletters/signals.py b/newsletters/signals.py index fbd44ba3..6c91d01c 100644 --- a/newsletters/signals.py +++ b/newsletters/signals.py @@ -7,7 +7,7 @@ from anymail.signals import inbound from django.dispatch import receiver -from core.newsletters import process_inbound_newsletter +from newsletters.intake import process_inbound_newsletter def _address_to_string(address) -> str: diff --git a/newsletters/tasks.py b/newsletters/tasks.py index 3125bee8..ba1bb2c3 100644 --- a/newsletters/tasks.py +++ b/newsletters/tasks.py @@ -7,8 +7,9 @@ from django.db.models import Model, Q from django.utils import timezone +from content.deduplication import canonicalize_url from content.models import Content -from core.deduplication import canonicalize_url +from newsletters.extraction import extract_newsletter_items from newsletters.models import IntakeAllowlist, NewsletterIntake, NewsletterIntakeStatus @@ -38,8 +39,6 @@ def _require_pk(instance: Model) -> int: def process_newsletter_intake(intake_id: int): """Convert a stored newsletter email into content rows.""" - from core.newsletters import extract_newsletter_items - intake = NewsletterIntake.objects.select_related("project").get(pk=intake_id) allowlist = IntakeAllowlist.objects.filter( @@ -109,11 +108,9 @@ def process_newsletter_intake(intake_id: int): def _schedule_content_processing(content: Content) -> None: """Ensure a content row is embedded before it enters the AI pipeline.""" - from core.tasks import ( - assign_content_to_topic_cluster, - process_content, - upsert_content_embedding, - ) + from core.embeddings import upsert_content_embedding + from core.tasks import process_content + from trends.tasks import assign_content_to_topic_cluster upsert_content_embedding(content) content_id = _require_pk(content) diff --git a/newsletters/tests/__init__.py b/newsletters/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/newsletters/tests/test_api.py b/newsletters/tests/test_api.py new file mode 100644 index 00000000..a95438fc --- /dev/null +++ b/newsletters/tests/test_api.py @@ -0,0 +1,121 @@ +from typing import Any, cast + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient, APITestCase + +from newsletters.models import IntakeAllowlist +from projects.models import Project, ProjectMembership, ProjectRole + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed API test assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _typed_client(client: object) -> APIClient: + """Cast the DRF test client so Pylance sees APIClient helpers.""" + + return cast(APIClient, client) + + +def _create_user(user_model: type[Any], **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +class NewsletterApiTests(APITestCase): + """Exercise newsletter-owned project API endpoints.""" + + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpass123") + self.other_user = _create_user( + user_model, username="other", password="testpass123" + ) + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + self.other_project = Project.objects.create( + name="Other Project", + topic_description="Frontend", + ) + ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + ProjectMembership.objects.create( + user=self.other_user, + project=self.other_project, + role=ProjectRole.ADMIN, + ) + self.owner_intake_allowlist = IntakeAllowlist.objects.create( + project=self.owner_project, + sender_email="sender@example.com", + ) + _typed_client(self.client).force_authenticate(self.owner) + + def test_intake_allowlist_list_is_scoped_to_request_user_project(self): + other_allowlist = IntakeAllowlist.objects.create( + project=self.other_project, + sender_email="other@example.com", + ) + + response = self.client.get( + reverse( + "v1:project-intake-allowlist-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()), 1) + self.assertEqual( + response.json()[0]["id"], _require_pk(self.owner_intake_allowlist) + ) + self.assertFalse(response.json()[0]["is_confirmed"]) + self.assertNotEqual(response.json()[0]["id"], _require_pk(other_allowlist)) + + def test_intake_allowlist_create_and_delete_manage_project_senders(self): + create_response = self.client.post( + reverse( + "v1:project-intake-allowlist-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + {"sender_email": "new-sender@example.com"}, + format="json", + ) + + self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) + created_allowlist = IntakeAllowlist.objects.get( + project=self.owner_project, + sender_email="new-sender@example.com", + ) + self.assertEqual( + create_response.json()["project"], _require_pk(self.owner_project) + ) + self.assertFalse(create_response.json()["is_confirmed"]) + + delete_response = self.client.delete( + reverse( + "v1:project-intake-allowlist-detail", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(created_allowlist), + }, + ) + ) + + self.assertEqual(delete_response.status_code, status.HTTP_204_NO_CONTENT) + self.assertFalse( + IntakeAllowlist.objects.filter(pk=_require_pk(created_allowlist)).exists() + ) diff --git a/core/tests/test_newsletters.py b/newsletters/tests/test_newsletters.py similarity index 95% rename from core/tests/test_newsletters.py rename to newsletters/tests/test_newsletters.py index 396d4575..bc1163ad 100644 --- a/core/tests/test_newsletters.py +++ b/newsletters/tests/test_newsletters.py @@ -11,19 +11,15 @@ from django.urls import reverse from svix.webhooks import Webhook -from core.models import ( - Content, - IntakeAllowlist, - NewsletterIntake, - NewsletterIntakeStatus, - Project, -) -from core.newsletters import ( +from newsletters.intake import ( extract_newsletter_items, sanitize_newsletter_html, send_confirmation_email, ) -from core.signals import handle_anymail_inbound +from newsletters.models import IntakeAllowlist, NewsletterIntake, NewsletterIntakeStatus +from newsletters.signals import handle_anymail_inbound +from content.models import Content +from projects.models import Project pytestmark = pytest.mark.django_db @@ -131,7 +127,7 @@ def test_handle_anymail_inbound_creates_pending_intake_and_sends_confirmation( settings, mocker, project ): settings.NEWSLETTER_API_BASE_URL = "https://example.com" - send_mock = mocker.patch("core.newsletters.send_confirmation_email") + send_mock = mocker.patch("newsletters.intake.send_confirmation_email") event = SimpleNamespace( message=FakeInboundMessage( envelope_recipient=f"intake+{project.intake_token}@inbox.example.com", @@ -160,8 +156,8 @@ def test_handle_anymail_inbound_creates_pending_intake_and_sends_confirmation( def test_handle_anymail_inbound_queues_confirmed_sender(settings, mocker, project): settings.CELERY_TASK_ALWAYS_EAGER = False - send_mock = mocker.patch("core.newsletters.send_confirmation_email") - delay_mock = mocker.patch("core.tasks.process_newsletter_intake.delay") + send_mock = mocker.patch("newsletters.intake.send_confirmation_email") + delay_mock = mocker.patch("newsletters.tasks.process_newsletter_intake.delay") IntakeAllowlist.objects.create( project=project, sender_email="newsletter@example.com", @@ -352,7 +348,7 @@ def test_confirm_newsletter_sender_confirms_allowlist_and_queues_pending_intakes raw_text="Visit https://example.com/post", message_id="msg-456", ) - delay_mock = mocker.patch("core.tasks.process_newsletter_intake.delay") + delay_mock = mocker.patch("newsletters.tasks.process_newsletter_intake.delay") response = client.get( reverse( @@ -383,10 +379,10 @@ def test_process_newsletter_intake_creates_content_for_confirmed_sender( raw_text="Great article https://example.com/article", message_id="msg-789", ) - upsert_mock = mocker.patch("core.tasks.upsert_content_embedding") + upsert_mock = mocker.patch("core.embeddings.upsert_content_embedding") delay_mock = mocker.patch("core.tasks.process_content.delay") - from core.tasks import process_newsletter_intake + from newsletters.tasks import process_newsletter_intake result = process_newsletter_intake(_require_pk(intake)) diff --git a/newsletters/views.py b/newsletters/views.py index 97c48ff1..6ab7072d 100644 --- a/newsletters/views.py +++ b/newsletters/views.py @@ -5,7 +5,7 @@ from django.utils import timezone from django.views.decorators.http import require_GET -from core.newsletters import queue_newsletter_intake +from newsletters.intake import queue_newsletter_intake from newsletters.models import IntakeAllowlist, NewsletterIntake, NewsletterIntakeStatus diff --git a/pipeline/tests/__init__.py b/pipeline/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pipeline/tests/test_admin.py b/pipeline/tests/test_admin.py new file mode 100644 index 00000000..e5586f03 --- /dev/null +++ b/pipeline/tests/test_admin.py @@ -0,0 +1,340 @@ +from types import SimpleNamespace +from typing import Any, cast +from unittest.mock import ANY, Mock + +import pytest +from django.contrib import messages +from django.contrib.admin.sites import AdminSite +from django.db.models import Model +from django.http import HttpRequest +from django.test import RequestFactory +from django.utils import timezone + +from content.models import Content +from pipeline.models import ReviewQueue, ReviewReason, SkillResult +from pipeline.admin import ReviewQueueAdmin, SkillResultAdmin +from projects.model_support import SourcePluginName +from projects.models import Project + +pytestmark = pytest.mark.django_db + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed admin test assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: Any, **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +def _request(query_params: dict[str, str] | None = None) -> HttpRequest: + """Build a typed request object for admin actions and filters.""" + + return RequestFactory().get("/admin/", data=query_params or {}) + + +def _message_user_mock(admin_instance: Any, mocker: Any) -> Mock: + """Install a mock for ModelAdmin.message_user and return it for assertions.""" + + message_mock = cast(Mock, mocker.Mock()) + admin_instance.message_user = message_mock + return message_mock + + +def _context(response: object) -> dict[str, Any]: + """Cast admin changelist extra_context payloads for typed assertions.""" + + return cast(dict[str, Any], response) + + +def _dashboard_stats(response: object) -> list[dict[str, Any]]: + """Return typed dashboard stats rows from a changelist extra_context payload.""" + + return cast(list[dict[str, Any]], _context(response)["dashboard_stats"]) + + +@pytest.fixture +def source_admin_context(django_user_model): + user = _create_user( + django_user_model, username="admin-owner", password="testpass123" + ) + project = Project.objects.create(name="Admin Project", topic_description="Infra") + return SimpleNamespace(user=user, project=project) + + +def test_review_queue_changelist_view_builds_dashboard_stats( + source_admin_context, mocker +): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/review-item", + title="Review Item", + author="Reviewer", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Review queue content", + ) + ReviewQueue.objects.create( + project=source_admin_context.project, + content=content, + reason=ReviewReason.BORDERLINE_RELEVANCE, + confidence=0.42, + resolved=False, + ) + admin_instance = ReviewQueueAdmin(ReviewQueue, AdminSite()) + mocker.patch.object( + admin_instance, "get_queryset", return_value=ReviewQueue.objects.all() + ) + super_changelist_view = mocker.patch( + "pipeline.admin.ModelAdmin.changelist_view", + side_effect=lambda request, extra_context=None: extra_context, + ) + + response = admin_instance.changelist_view(request=_request()) + dashboard_stats = _dashboard_stats(response) + + super_changelist_view.assert_called_once() + assert dashboard_stats[0]["value"] == 1 + assert dashboard_stats[1]["value"] == "42%" + + +def test_review_queue_display_confidence_renders_without_django6_format_error( + source_admin_context, +): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/review-confidence", + title="Review Confidence", + author="Reviewer", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Review queue content", + ) + review_item = ReviewQueue.objects.create( + project=source_admin_context.project, + content=content, + reason=ReviewReason.BORDERLINE_RELEVANCE, + confidence=0.42, + resolved=False, + ) + admin_instance = ReviewQueueAdmin(ReviewQueue, AdminSite()) + + rendered = admin_instance.display_confidence(review_item) + + assert "42%" in rendered + + +def test_skill_result_admin_helpers_and_dashboard_stats(source_admin_context, mocker): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/skill-result", + title="Skill Result Title For Preview", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Skill result content.", + ) + current_result = SkillResult.objects.create( + content=content, + project=source_admin_context.project, + skill_name="summarization", + status="FAILED", + result_data={"summary": "Draft summary"}, + error_message="boom", + latency_ms=1250, + confidence=0.42, + ) + superseded_result = SkillResult.objects.create( + content=content, + project=source_admin_context.project, + skill_name="relevance_scoring", + status="COMPLETED", + result_data=None, + latency_ms=250, + confidence=0.91, + superseded_by=current_result, + ) + admin_instance = SkillResultAdmin(SkillResult, AdminSite()) + message_user_mock = _message_user_mock(admin_instance, mocker) + super_changelist_view = mocker.patch( + "pipeline.admin.ModelAdmin.changelist_view", + side_effect=lambda request, extra_context=None: extra_context, + ) + + admin_instance.retry_selected_skills( + _request(), SkillResult.objects.filter(pk=current_result.pk) + ) + current_result.refresh_from_db() + response = admin_instance.changelist_view(_request()) + dashboard_stats = _dashboard_stats(response) + + assert current_result.status == "pending" + assert current_result.error_message == "" + message_user_mock.assert_called_once_with( + ANY, + "Successfully reset 1 skills to PENDING for retry.", + messages.SUCCESS, + ) + assert ( + admin_instance.preview_json(current_result) + == f'🔍 Preview' + ) + assert admin_instance.preview_json(superseded_result) == "-" + assert admin_instance.get_content_link(current_result).endswith("...") + assert "● PENDING" in admin_instance.display_status(current_result) + assert admin_instance.display_performance(current_result) == "1250ms / 42%" + assert admin_instance.is_current(current_result) is True + assert admin_instance.is_current(superseded_result) is False + assert "Draft summary" in admin_instance.pretty_result_data(current_result) + assert admin_instance.pretty_result_data(superseded_result) == "No data available" + super_changelist_view.assert_called_once() + assert dashboard_stats[0]["value"] == "750ms" + assert dashboard_stats[1]["value"] == "0.0%" + + +def test_review_queue_actions_update_resolution_and_emit_message( + source_admin_context, mocker +): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/review-action", + title="Review Action", + author="Reviewer", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Review action content.", + ) + approve_item = ReviewQueue.objects.create( + project=source_admin_context.project, + content=content, + reason=ReviewReason.BORDERLINE_RELEVANCE, + confidence=0.5, + resolved=False, + ) + reject_item = ReviewQueue.objects.create( + project=source_admin_context.project, + content=content, + reason=ReviewReason.LOW_CONFIDENCE_CLASSIFICATION, + confidence=0.2, + resolved=False, + ) + admin_instance = ReviewQueueAdmin(ReviewQueue, AdminSite()) + message_user_mock = _message_user_mock(admin_instance, mocker) + + admin_instance.mark_as_approved( + _request(), ReviewQueue.objects.filter(pk=approve_item.pk) + ) + admin_instance.mark_as_rejected( + _request(), ReviewQueue.objects.filter(pk=reject_item.pk) + ) + + approve_item.refresh_from_db() + reject_item.refresh_from_db() + assert approve_item.resolved is True + assert approve_item.resolution == "APPROVED" + assert reject_item.resolved is True + assert reject_item.resolution == "REJECTED" + assert message_user_mock.call_count == 2 + + +def test_skill_result_admin_handles_unknown_status_and_empty_performance( + source_admin_context, +): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/skill-result-empty", + title="", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Skill result content.", + ) + skill_result = SkillResult.objects.create( + content=content, + project=source_admin_context.project, + skill_name="summarization", + status="QUEUED", + result_data={"summary": "Queued summary"}, + latency_ms=None, + confidence=None, + ) + admin_instance = SkillResultAdmin(SkillResult, AdminSite()) + + assert admin_instance.get_content_link(skill_result) == "Untitled" + assert "gray" in admin_instance.display_status(skill_result) + assert admin_instance.display_performance(skill_result) == "- / -" + + +def test_skill_result_changelist_view_uses_warning_and_danger_colors( + source_admin_context, mocker +): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/skill-result-slow", + title="Slow Skill Result", + author="Editor", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Slow skill result content.", + ) + SkillResult.objects.create( + content=content, + project=source_admin_context.project, + skill_name="summarization", + status="failed", + latency_ms=3001, + ) + admin_instance = SkillResultAdmin(SkillResult, AdminSite()) + super_changelist_view = mocker.patch( + "pipeline.admin.ModelAdmin.changelist_view", + side_effect=lambda request, extra_context=None: extra_context, + ) + + response = admin_instance.changelist_view(_request()) + dashboard_stats = _dashboard_stats(response) + + super_changelist_view.assert_called_once() + assert dashboard_stats[0]["color"] == "warning" + assert dashboard_stats[1]["color"] == "danger" + + +@pytest.mark.parametrize( + ("confidence", "expected_color"), + [ + (0.2, "red"), + (0.9, "green"), + ], +) +def test_review_queue_display_confidence_remaining_color_branches( + source_admin_context, + confidence, + expected_color, +): + content = Content.objects.create( + project=source_admin_context.project, + url=f"https://example.com/review-confidence-{confidence}", + title="Review Confidence Remaining", + author="Reviewer", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Review queue content", + ) + review_item = ReviewQueue.objects.create( + project=source_admin_context.project, + content=content, + reason=ReviewReason.BORDERLINE_RELEVANCE, + confidence=confidence, + resolved=False, + ) + admin_instance = ReviewQueueAdmin(ReviewQueue, AdminSite()) + + rendered = admin_instance.display_confidence(review_item) + + assert expected_color in rendered diff --git a/core/tests/test_pipeline.py b/pipeline/tests/test_pipeline.py similarity index 99% rename from core/tests/test_pipeline.py rename to pipeline/tests/test_pipeline.py index a2249866..1bcf6e0f 100644 --- a/core/tests/test_pipeline.py +++ b/pipeline/tests/test_pipeline.py @@ -3,19 +3,7 @@ import pytest from django.db.models import Model -from core.deduplication import canonicalize_url -from core.models import ( - Content, - Entity, - EntityCandidate, - EntityMention, - EntityMentionRole, - Project, - ReviewQueue, - ReviewReason, - SkillResult, - SkillStatus, -) +from content.deduplication import canonicalize_url from core.pipeline import ( CLASSIFICATION_SKILL_NAME, DEDUPLICATION_SKILL_NAME, @@ -41,6 +29,15 @@ run_summarization, ) from core.tasks import process_content +from content.models import Content +from entities.models import ( + Entity, + EntityCandidate, + EntityMention, + EntityMentionRole, +) +from pipeline.models import ReviewQueue, ReviewReason, SkillResult, SkillStatus +from projects.models import Project pytestmark = pytest.mark.django_db @@ -1200,7 +1197,7 @@ def test_run_entity_extraction_persists_mentions_and_candidates( ) pipeline_context.content.save(update_fields=["title", "content_text"]) mocker.patch( - "core.entity_extraction.search_similar_entities_for_content", + "entities.extraction.search_similar_entities_for_content", return_value=[ SimpleNamespace(score=0.91, payload={"entity_id": _require_pk(entity)}) ], diff --git a/projects/admin.py b/projects/admin.py index a412c420..c93e79ca 100644 --- a/projects/admin.py +++ b/projects/admin.py @@ -11,7 +11,7 @@ from import_export.admin import ExportActionMixin from unfold.admin import ModelAdmin -from core.plugins import get_plugin_for_source_config, validate_plugin_config +from ingestion.plugins import get_plugin_for_source_config, validate_plugin_config from projects.models import ( BlueskyCredentials, MastodonCredentials, @@ -190,7 +190,7 @@ def has_stored_credential(self, obj): def verify_selected_credentials(self, request, queryset): """Authenticate the selected Bluesky accounts and report the outcome.""" - from core.plugins.bluesky import BlueskySourcePlugin + from ingestion.plugins.bluesky import BlueskySourcePlugin verified_credentials = [] failed_credentials = [] @@ -279,7 +279,7 @@ def has_stored_credential(self, obj): def verify_selected_credentials(self, request, queryset): """Authenticate the selected Mastodon tokens and report the outcome.""" - from core.plugins.mastodon import MastodonSourcePlugin + from ingestion.plugins.mastodon import MastodonSourcePlugin verified_credentials = [] failed_credentials = [] diff --git a/projects/api.py b/projects/api.py index 0c9bc998..85f074d5 100644 --- a/projects/api.py +++ b/projects/api.py @@ -36,8 +36,8 @@ IsProjectMemberWritable, get_visible_projects_queryset, ) -from core.plugins.bluesky import BlueskySourcePlugin -from core.plugins.mastodon import MastodonSourcePlugin +from ingestion.plugins.bluesky import BlueskySourcePlugin +from ingestion.plugins.mastodon import MastodonSourcePlugin from projects.models import ( BlueskyCredentials, MastodonCredentials, diff --git a/projects/serializers.py b/projects/serializers.py index f7735707..cf974d39 100644 --- a/projects/serializers.py +++ b/projects/serializers.py @@ -4,9 +4,9 @@ from rest_framework import serializers -from core.plugins import validate_plugin_config from core.permissions import get_user_role from core.serializer_mixins import ProjectScopedSerializerMixin +from ingestion.plugins import validate_plugin_config from projects.models import ( BlueskyCredentials, MastodonCredentials, diff --git a/projects/tests/__init__.py b/projects/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/tests/test_admin.py b/projects/tests/test_admin.py new file mode 100644 index 00000000..59954fea --- /dev/null +++ b/projects/tests/test_admin.py @@ -0,0 +1,360 @@ +from datetime import timedelta +from types import SimpleNamespace +from typing import Any, cast +from unittest.mock import ANY, Mock + +import pytest +from django.contrib import messages +from django.contrib.admin.sites import AdminSite +from django.db.models import Model +from django.http import HttpRequest +from django.test import RequestFactory +from django.utils import timezone + +from projects.admin import ( + BlueskyCredentialsAdmin, + BlueskyCredentialsAdminForm, + MastodonCredentialsAdmin, + MastodonCredentialsAdminForm, + ProjectConfigAdmin, + SourceConfigAdmin, +) +from projects.model_support import SourcePluginName +from projects.models import ( + BlueskyCredentials, + MastodonCredentials, + Project, + ProjectConfig, + SourceConfig, +) + +pytestmark = pytest.mark.django_db + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed admin test assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: Any, **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +def _request(query_params: dict[str, str] | None = None) -> HttpRequest: + """Build a typed request object for admin actions and filters.""" + + return RequestFactory().get("/admin/", data=query_params or {}) + + +def _message_user_mock(admin_instance: Any, mocker: Any) -> Mock: + """Install a mock for ModelAdmin.message_user and return it for assertions.""" + + message_mock = cast(Mock, mocker.Mock()) + admin_instance.message_user = message_mock + return message_mock + + +def _context(response: object) -> dict[str, Any]: + """Cast admin changelist extra_context payloads for typed assertions.""" + + return cast(dict[str, Any], response) + + +def _dashboard_stats(response: object) -> list[dict[str, Any]]: + """Return typed dashboard stats rows from a changelist extra_context payload.""" + + return cast(list[dict[str, Any]], _context(response)["dashboard_stats"]) + + +@pytest.fixture +def source_admin_context(django_user_model): + user = _create_user( + django_user_model, username="admin-owner", password="testpass123" + ) + project = Project.objects.create(name="Admin Project", topic_description="Infra") + return SimpleNamespace(user=user, project=project) + + +def test_test_source_connection_reports_success(source_admin_context, mocker): + source_config = SourceConfig.objects.create( + project=source_admin_context.project, + plugin_name=SourcePluginName.RSS, + config={"feed_url": "https://example.com/feed.xml"}, + ) + plugin = mocker.Mock() + plugin.health_check.return_value = True + validate_mock = mocker.patch( + "projects.admin.validate_plugin_config", + return_value={"feed_url": "https://example.com/feed.xml"}, + ) + get_plugin_mock = mocker.patch( + "projects.admin.get_plugin_for_source_config", return_value=plugin + ) + admin_instance = SourceConfigAdmin(SourceConfig, AdminSite()) + message_user_mock = _message_user_mock(admin_instance, mocker) + + admin_instance.test_source_connection( + request=_request(), + queryset=SourceConfig.objects.filter(pk=source_config.pk), + ) + + validate_mock.assert_called_once_with( + SourcePluginName.RSS, {"feed_url": "https://example.com/feed.xml"} + ) + get_plugin_mock.assert_called_once() + plugin.health_check.assert_called_once_with() + message_user_mock.assert_called_once_with( + ANY, + "Connectivity check passed for 1 source(s).", + messages.SUCCESS, + ) + + +def test_project_config_admin_exposes_centroid_toggle_field(source_admin_context): + config = ProjectConfig.objects.create(project=source_admin_context.project) + admin_instance = ProjectConfigAdmin(ProjectConfig, AdminSite()) + + assert "recompute_topic_centroid_on_feedback_save" in admin_instance.list_display + assert "recompute_topic_centroid_on_feedback_save" in admin_instance.list_filter + assert "recompute_topic_centroid_on_feedback_save" in admin_instance.get_fields( + request=_request(), obj=config + ) + + +def test_test_source_connection_reports_failures(source_admin_context, mocker): + source_config = SourceConfig.objects.create( + project=source_admin_context.project, + plugin_name=SourcePluginName.RSS, + config={"feed_url": "https://example.com/feed.xml"}, + ) + mocker.patch( + "projects.admin.validate_plugin_config", + side_effect=ValueError("Missing required config field: feed_url"), + ) + admin_instance = SourceConfigAdmin(SourceConfig, AdminSite()) + message_user_mock = _message_user_mock(admin_instance, mocker) + + admin_instance.test_source_connection( + request=_request(), + queryset=SourceConfig.objects.filter(pk=source_config.pk), + ) + + message_user_mock.assert_called_once_with( + ANY, + "Connectivity check failed for: rss source for Admin Project: Missing required config field: feed_url", + messages.ERROR, + ) + + +def test_source_config_display_health_renders_without_django6_format_html_error( + source_admin_context, +): + source_config = SourceConfig.objects.create( + project=source_admin_context.project, + plugin_name=SourcePluginName.RSS, + config={"feed_url": "https://example.com/feed.xml"}, + is_active=True, + last_fetched_at=timezone.now(), + ) + admin_instance = SourceConfigAdmin(SourceConfig, AdminSite()) + + rendered = admin_instance.display_health(source_config) + + assert "Healthy" in rendered + + +def test_bluesky_credentials_admin_form_encrypts_app_password(source_admin_context): + form = BlueskyCredentialsAdminForm( + data={ + "project": _require_pk(source_admin_context.project), + "handle": "@Alice.BSKY.social", + "credential_input": "app-password", + "pds_url": "https://pds.example.com/xrpc/", + "is_active": True, + } + ) + + assert form.is_valid(), form.errors + credentials = form.save() + + assert credentials.handle == "alice.bsky.social" + assert credentials.pds_url == "https://pds.example.com" + assert credentials.has_app_password() is True + assert credentials.get_app_password() == "app-password" + + +def test_verify_selected_bluesky_credentials_reports_success( + source_admin_context, mocker +): + credentials = BlueskyCredentials.objects.create( + project=source_admin_context.project, + handle="alice.bsky.social", + app_password_encrypted="ciphertext", + ) + verify_mock = mocker.patch( + "ingestion.plugins.bluesky.BlueskySourcePlugin.verify_credentials" + ) + admin_instance = BlueskyCredentialsAdmin(BlueskyCredentials, AdminSite()) + message_user_mock = _message_user_mock(admin_instance, mocker) + + admin_instance.verify_selected_credentials( + request=_request(), + queryset=BlueskyCredentials.objects.filter(pk=credentials.pk), + ) + + verify_mock.assert_called_once_with(credentials) + message_user_mock.assert_called_once_with( + ANY, + "Credential verification passed for 1 account(s).", + messages.SUCCESS, + ) + + +def test_verify_selected_bluesky_credentials_reports_failures( + source_admin_context, mocker +): + credentials = BlueskyCredentials.objects.create( + project=source_admin_context.project, + handle="alice.bsky.social", + app_password_encrypted="ciphertext", + ) + mocker.patch( + "ingestion.plugins.bluesky.BlueskySourcePlugin.verify_credentials", + side_effect=RuntimeError("bad login"), + ) + admin_instance = BlueskyCredentialsAdmin(BlueskyCredentials, AdminSite()) + message_user_mock = _message_user_mock(admin_instance, mocker) + + admin_instance.verify_selected_credentials( + request=_request(), + queryset=BlueskyCredentials.objects.filter(pk=credentials.pk), + ) + + message_user_mock.assert_called_once_with( + ANY, + "Credential verification failed for: Bluesky credentials for Admin Project: bad login", + messages.ERROR, + ) + + +def test_mastodon_credentials_admin_form_encrypts_access_token(source_admin_context): + form = MastodonCredentialsAdminForm( + data={ + "project": _require_pk(source_admin_context.project), + "instance_url": "https://hachyderm.io/@alice/", + "account_acct": "@Alice", + "credential_input": "access-token", + "is_active": True, + } + ) + + assert form.is_valid(), form.errors + credentials = form.save() + + assert credentials.instance_url == "https://hachyderm.io" + assert credentials.account_acct == "alice@hachyderm.io" + assert credentials.has_access_token() is True + assert credentials.get_access_token() == "access-token" + + +def test_verify_selected_mastodon_credentials_reports_success( + source_admin_context, mocker +): + credentials = MastodonCredentials.objects.create( + project=source_admin_context.project, + instance_url="https://hachyderm.io", + account_acct="alice@hachyderm.io", + access_token_encrypted="ciphertext", + ) + verify_mock = mocker.patch( + "ingestion.plugins.mastodon.MastodonSourcePlugin.verify_credentials" + ) + admin_instance = MastodonCredentialsAdmin(MastodonCredentials, AdminSite()) + message_user_mock = _message_user_mock(admin_instance, mocker) + + admin_instance.verify_selected_credentials( + request=_request(), + queryset=MastodonCredentials.objects.filter(pk=credentials.pk), + ) + + verify_mock.assert_called_once_with(credentials) + message_user_mock.assert_called_once_with( + ANY, + "Credential verification passed for 1 account(s).", + messages.SUCCESS, + ) + + +def test_verify_selected_mastodon_credentials_reports_failures( + source_admin_context, mocker +): + credentials = MastodonCredentials.objects.create( + project=source_admin_context.project, + instance_url="https://hachyderm.io", + account_acct="alice@hachyderm.io", + access_token_encrypted="ciphertext", + ) + mocker.patch( + "ingestion.plugins.mastodon.MastodonSourcePlugin.verify_credentials", + side_effect=RuntimeError("bad token"), + ) + admin_instance = MastodonCredentialsAdmin(MastodonCredentials, AdminSite()) + message_user_mock = _message_user_mock(admin_instance, mocker) + + admin_instance.verify_selected_credentials( + request=_request(), + queryset=MastodonCredentials.objects.filter(pk=credentials.pk), + ) + + message_user_mock.assert_called_once_with( + ANY, + "Credential verification failed for: Mastodon credentials for Admin Project: bad token", + messages.ERROR, + ) + + +def test_source_config_admin_health_pretty_config_and_dashboard_branches( + source_admin_context, mocker +): + stale_config = SourceConfig.objects.create( + project=source_admin_context.project, + plugin_name=SourcePluginName.RSS, + config={"feed_url": "https://example.com/stale.xml"}, + is_active=True, + last_fetched_at=timezone.now() - timedelta(days=2), + ) + paused_config = SourceConfig.objects.create( + project=source_admin_context.project, + plugin_name=SourcePluginName.REDDIT, + config={}, + is_active=False, + ) + never_run_config = SourceConfig.objects.create( + project=source_admin_context.project, + plugin_name=SourcePluginName.RSS, + config={}, + is_active=True, + last_fetched_at=None, + ) + admin_instance = SourceConfigAdmin(SourceConfig, AdminSite()) + super_changelist_view = mocker.patch( + "projects.admin.ModelAdmin.changelist_view", + side_effect=lambda request, extra_context=None: extra_context, + ) + + response = admin_instance.changelist_view(_request()) + dashboard_stats = _dashboard_stats(response) + + assert "Stale" in admin_instance.display_health(stale_config) + assert "Paused" in admin_instance.display_health(paused_config) + assert "Never Run" in admin_instance.display_health(never_run_config) + assert admin_instance.pretty_config(paused_config) == "Empty" + super_changelist_view.assert_called_once() + assert dashboard_stats[0]["color"] == "warning" + assert dashboard_stats[1]["value"] == 2 diff --git a/projects/tests/test_api.py b/projects/tests/test_api.py new file mode 100644 index 00000000..d344b760 --- /dev/null +++ b/projects/tests/test_api.py @@ -0,0 +1,371 @@ +from typing import Any, cast +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient, APITestCase + +from projects.model_support import SourcePluginName +from projects.models import ( + BlueskyCredentials, + MastodonCredentials, + Project, + ProjectMembership, + ProjectRole, +) + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed API test assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _typed_client(client: object) -> APIClient: + """Cast the DRF test client so Pylance sees APIClient helpers.""" + + return cast(APIClient, client) + + +def _create_user(user_model: type[Any], **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +class ProjectApiTests(APITestCase): + """Exercise project-owned project and credential API endpoints.""" + + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpass123") + self.other_user = _create_user( + user_model, username="other", password="testpass123" + ) + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + self.other_project = Project.objects.create( + name="Other Project", + topic_description="Frontend", + ) + ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + ProjectMembership.objects.create( + user=self.other_user, + project=self.other_project, + role=ProjectRole.ADMIN, + ) + _typed_client(self.client).force_authenticate(self.owner) + + def assert_standardized_validation_error( + self, payload: dict[str, object], attr: str + ): + """Assert the repo-standardized validation payload shape.""" + + self.assertEqual(payload["type"], "validation_error") + errors = cast(list[dict[str, object]], payload["errors"]) + self.assertTrue(any(error["attr"] == attr for error in errors)) + + def test_project_list_requires_authentication(self): + _typed_client(self.client).force_authenticate(user=None) + + response = self.client.get(reverse("v1:project-list"), HTTP_HOST="localhost") + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual( + response.json(), + { + "type": "client_error", + "errors": [ + { + "code": "not_authenticated", + "detail": "Authentication credentials were not provided.", + "attr": None, + } + ], + }, + ) + + def test_project_list_is_scoped_to_request_user_memberships(self): + BlueskyCredentials.objects.create( + project=self.owner_project, + handle="owner-project.bsky.social", + is_active=True, + last_error="", + ) + + response = self.client.get(reverse("v1:project-list")) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]["id"], _require_pk(self.owner_project)) + self.assertEqual(response.json()[0]["user_role"], ProjectRole.ADMIN) + self.assertEqual( + response.json()[0]["intake_token"], self.owner_project.intake_token + ) + self.assertFalse(response.json()[0]["intake_enabled"]) + self.assertTrue(response.json()[0]["has_bluesky_credentials"]) + self.assertEqual( + response.json()[0]["bluesky_handle"], "owner-project.bsky.social" + ) + self.assertTrue(response.json()[0]["bluesky_is_active"]) + self.assertEqual(response.json()[0]["bluesky_last_error"], "") + + def test_project_rotate_intake_token_returns_updated_project(self): + original_token = self.owner_project.intake_token + + response = self.client.post( + reverse( + "v1:project-rotate-intake-token", + kwargs={"id": _require_pk(self.owner_project)}, + ), + format="json", + ) + + self.owner_project.refresh_from_db() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertNotEqual(self.owner_project.intake_token, original_token) + self.assertEqual( + response.json()["intake_token"], self.owner_project.intake_token + ) + + def test_bluesky_credentials_list_create_and_update_hide_stored_password(self): + list_response = self.client.get( + reverse( + "v1:project-bluesky-credentials-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + + self.assertEqual(list_response.status_code, status.HTTP_200_OK) + self.assertEqual(list_response.json(), []) + + create_response = self.client.post( + reverse( + "v1:project-bluesky-credentials-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "handle": "@Owner.Project.BSKY.social", + "pds_url": "https://pds.example.com/xrpc/", + "is_active": True, + "app_password": "app-password", + }, + format="json", + ) + + self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) + credentials = BlueskyCredentials.objects.get(project=self.owner_project) + self.assertEqual(credentials.handle, "owner.project.bsky.social") + self.assertEqual(credentials.pds_url, "https://pds.example.com") + self.assertEqual(credentials.get_app_password(), "app-password") + self.assertTrue(create_response.json()["has_stored_credential"]) + self.assertNotIn("app_password", create_response.json()) + + update_response = self.client.patch( + reverse( + "v1:project-bluesky-credentials-detail", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(credentials), + }, + ), + { + "handle": "updated.bsky.social", + "pds_url": "", + "is_active": False, + }, + format="json", + ) + + credentials.refresh_from_db() + self.assertEqual(update_response.status_code, status.HTTP_200_OK) + self.assertEqual(credentials.handle, "updated.bsky.social") + self.assertFalse(credentials.is_active) + self.assertEqual(credentials.get_app_password(), "app-password") + + def test_bluesky_credentials_create_requires_app_password(self): + response = self.client.post( + reverse( + "v1:project-bluesky-credentials-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "handle": "owner.bsky.social", + "pds_url": "", + "is_active": True, + "app_password": "", + }, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assert_standardized_validation_error(response.json(), "app_password") + + def test_verify_bluesky_credentials_requires_project_credentials(self): + response = self.client.post( + reverse( + "v1:project-verify-bluesky-credentials", + kwargs={"id": _require_pk(self.owner_project)}, + ), + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assert_standardized_validation_error( + response.json(), "bluesky_credentials" + ) + + @patch("ingestion.plugins.bluesky.BlueskySourcePlugin.verify_credentials") + def test_verify_bluesky_credentials_verifies_project_account(self, verify_mock): + credentials = BlueskyCredentials( + project=self.owner_project, handle="project.bsky.social" + ) + credentials.set_app_password("app-password") + credentials.save() + + response = self.client.post( + reverse( + "v1:project-verify-bluesky-credentials", + kwargs={"id": _require_pk(self.owner_project)}, + ), + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + verify_mock.assert_called_once() + verified_credentials = verify_mock.call_args.args[0] + self.assertEqual(verified_credentials, credentials) + self.assertEqual(response.json()["status"], "verified") + self.assertEqual(response.json()["handle"], "project.bsky.social") + self.assertEqual(response.json()["last_error"], "") + + @patch("core.api.logger.exception") + @patch( + "ingestion.plugins.bluesky.BlueskySourcePlugin.verify_credentials", + side_effect=RuntimeError("bad login"), + ) + def test_verify_bluesky_credentials_surfaces_verification_errors( + self, _verify_mock, logger_exception_mock + ): + credentials = BlueskyCredentials( + project=self.owner_project, handle="project.bsky.social" + ) + credentials.set_app_password("app-password") + credentials.save() + + response = self.client.post( + reverse( + "v1:project-verify-bluesky-credentials", + kwargs={"id": _require_pk(self.owner_project)}, + ), + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assert_standardized_validation_error( + response.json(), "bluesky_credentials" + ) + self.assertNotIn("bad login", str(response.json())) + logger_exception_mock.assert_called_once_with( + "Bluesky credential verification failed for project id=%s", + _require_pk(self.owner_project), + ) + + def test_verify_mastodon_credentials_requires_configured_project_credentials(self): + response = self.client.post( + reverse( + "v1:project-verify-mastodon-credentials", + kwargs={"id": _require_pk(self.owner_project)}, + ), + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assert_standardized_validation_error( + response.json(), "mastodon_credentials" + ) + + @patch("ingestion.plugins.mastodon.MastodonSourcePlugin.verify_credentials") + def test_verify_mastodon_credentials_verifies_project_account(self, verify_mock): + credentials = MastodonCredentials( + project=self.owner_project, + instance_url="https://hachyderm.io", + account_acct="alice@hachyderm.io", + ) + credentials.set_access_token("access-token") + credentials.save() + + response = self.client.post( + reverse( + "v1:project-verify-mastodon-credentials", + kwargs={"id": _require_pk(self.owner_project)}, + ), + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + verify_mock.assert_called_once() + verified_credentials = verify_mock.call_args.args[0] + self.assertEqual(verified_credentials, credentials) + self.assertEqual(response.json()["status"], "verified") + self.assertEqual(response.json()["account_acct"], "alice@hachyderm.io") + self.assertEqual(response.json()["instance_url"], "https://hachyderm.io") + self.assertEqual(response.json()["last_error"], "") + + @patch("core.api.logger.exception") + @patch( + "ingestion.plugins.mastodon.MastodonSourcePlugin.verify_credentials", + side_effect=RuntimeError("bad token"), + ) + def test_verify_mastodon_credentials_surfaces_verification_errors( + self, _verify_mock, logger_exception_mock + ): + credentials = MastodonCredentials( + project=self.owner_project, + instance_url="https://hachyderm.io", + account_acct="alice@hachyderm.io", + ) + credentials.set_access_token("access-token") + credentials.save() + + response = self.client.post( + reverse( + "v1:project-verify-mastodon-credentials", + kwargs={"id": _require_pk(self.owner_project)}, + ), + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assert_standardized_validation_error( + response.json(), "mastodon_credentials" + ) + self.assertNotIn("bad token", str(response.json())) + logger_exception_mock.assert_called_once_with( + "Mastodon credential verification failed for project id=%s", + _require_pk(self.owner_project), + ) + + def test_source_config_create_validates_plugin_config(self): + response = self.client.post( + reverse( + "v1:project-source-config-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + {"plugin_name": SourcePluginName.RSS, "config": {}}, + format="json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assert_standardized_validation_error(response.json(), "config") diff --git a/core/tests/test_invitations.py b/projects/tests/test_invitations.py similarity index 100% rename from core/tests/test_invitations.py rename to projects/tests/test_invitations.py diff --git a/core/tests/test_membership.py b/projects/tests/test_membership.py similarity index 100% rename from core/tests/test_membership.py rename to projects/tests/test_membership.py diff --git a/pytest.ini b/pytest.ini index bb4c4d61..6b5db55c 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,15 @@ [pytest] DJANGO_SETTINGS_MODULE = newsletter_maker.settings -addopts = --disable-plugin-autoload -p pytest_django.plugin -p pytest_mock +addopts = --disable-plugin-autoload --import-mode=importlib -p pytest_django.plugin -p pytest_mock python_files = test_*.py testpaths = + content/tests core/tests + entities/tests + ingestion/tests + newsletters/tests + pipeline/tests + projects/tests tests + trends/tests + users/tests diff --git a/trends/tests/test_admin.py b/trends/tests/test_admin.py new file mode 100644 index 00000000..18ce818f --- /dev/null +++ b/trends/tests/test_admin.py @@ -0,0 +1,153 @@ +from datetime import timedelta +from types import SimpleNamespace +from typing import Any, cast + +import pytest +from django.contrib.admin.sites import AdminSite +from django.db.models import Model +from django.http import HttpRequest +from django.test import RequestFactory +from django.utils import timezone + +from projects.models import Project +from trends.admin import TopicCentroidSnapshotAdmin +from trends.models import TopicCentroidSnapshot + +pytestmark = pytest.mark.django_db + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed admin test assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: Any, **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +def _request(query_params: dict[str, str] | None = None) -> HttpRequest: + """Build a typed request object for admin actions and filters.""" + + return RequestFactory().get("/admin/", data=query_params or {}) + + +def _context(response: object) -> dict[str, Any]: + """Cast admin changelist extra_context payloads for typed assertions.""" + + return cast(dict[str, Any], response) + + +def _dashboard_stats(response: object) -> list[dict[str, Any]]: + """Return typed dashboard stats rows from a changelist extra_context payload.""" + + return cast(list[dict[str, Any]], _context(response)["dashboard_stats"]) + + +def _drilldowns(response: object) -> list[dict[str, Any]]: + """Return typed centroid drilldowns from a changelist extra_context payload.""" + + return cast(list[dict[str, Any]], _context(response)["centroid_project_drilldowns"]) + + +@pytest.fixture +def source_admin_context(django_user_model): + user = _create_user( + django_user_model, + username="admin-owner", + password="testpass123", + ) + project = Project.objects.create(name="Admin Project", topic_description="Infra") + return SimpleNamespace(user=user, project=project) + + +def test_topic_centroid_snapshot_admin_renders_drift_fields(source_admin_context): + snapshot = TopicCentroidSnapshot.objects.create( + project=source_admin_context.project, + centroid_active=True, + centroid_vector=[1.0, 0.0], + feedback_count=15, + upvote_count=12, + downvote_count=3, + drift_from_previous=0.125, + drift_from_week_ago=0.4, + ) + admin_instance = TopicCentroidSnapshotAdmin(TopicCentroidSnapshot, AdminSite()) + + assert admin_instance.display_drift_from_previous(snapshot) == "12.5%" + assert admin_instance.display_drift_from_week_ago(snapshot) == "40.0%" + + +def test_topic_centroid_snapshot_admin_changelist_view_builds_dashboard_stats( + source_admin_context, mocker +): + second_project = Project.objects.create( + name="Second Admin Project", + topic_description="Analytics", + ) + fixed_now = timezone.now() + recent_snapshot = TopicCentroidSnapshot.objects.create( + project=source_admin_context.project, + centroid_active=True, + centroid_vector=[1.0, 0.0], + feedback_count=18, + upvote_count=14, + downvote_count=4, + drift_from_previous=0.1, + drift_from_week_ago=0.2, + ) + stale_snapshot = TopicCentroidSnapshot.objects.create( + project=second_project, + centroid_active=False, + centroid_vector=[], + feedback_count=2, + upvote_count=1, + downvote_count=1, + ) + TopicCentroidSnapshot.objects.filter(pk=recent_snapshot.pk).update( + computed_at=fixed_now - timedelta(hours=6) + ) + TopicCentroidSnapshot.objects.filter(pk=stale_snapshot.pk).update( + computed_at=fixed_now - timedelta(days=2) + ) + admin_instance = TopicCentroidSnapshotAdmin(TopicCentroidSnapshot, AdminSite()) + mocker.patch.object( + admin_instance, + "get_queryset", + return_value=TopicCentroidSnapshot.objects.all(), + ) + super_changelist_view = mocker.patch( + "django.contrib.admin.options.ModelAdmin.changelist_view", + side_effect=lambda request, extra_context=None: extra_context, + ) + mocker.patch("trends.admin.timezone.now", return_value=fixed_now) + + response = admin_instance.changelist_view(request=_request()) + dashboard_stats = _dashboard_stats(response) + centroid_project_drilldowns = _drilldowns(response) + + super_changelist_view.assert_called_once() + assert ( + admin_instance.list_before_template + == "admin/topic_centroid_snapshot_changelist_widget.html" + ) + assert dashboard_stats[0]["value"] == "1 / 2" + assert dashboard_stats[0]["color"] == "warning" + assert dashboard_stats[1]["value"] == "10.0%" + assert dashboard_stats[1]["color"] == "success" + assert dashboard_stats[2]["value"] == "20.0%" + assert dashboard_stats[2]["color"] == "warning" + assert dashboard_stats[3]["value"] == "6h ago" + assert dashboard_stats[3]["color"] == "success" + assert len(centroid_project_drilldowns) == 2 + assert centroid_project_drilldowns[0]["project_name"] == "Admin Project" + assert centroid_project_drilldowns[0]["href"] == ( + "/admin/trends/topiccentroidsnapshot/?project__id__exact=" + f"{_require_pk(source_admin_context.project)}" + ) + assert centroid_project_drilldowns[0]["drift_from_previous"] == "10.0%" diff --git a/trends/tests/test_api.py b/trends/tests/test_api.py new file mode 100644 index 00000000..531eaedd --- /dev/null +++ b/trends/tests/test_api.py @@ -0,0 +1,394 @@ +from typing import Any, cast + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient, APITestCase + +from content.models import Content +from entities.models import Entity +from projects.model_support import SourcePluginName +from projects.models import Project, ProjectConfig, ProjectMembership, ProjectRole +from trends.models import ( + ContentClusterMembership, + ThemeSuggestion, + ThemeSuggestionStatus, + TopicCentroidSnapshot, + TopicCluster, + TopicVelocitySnapshot, +) + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed API test assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _typed_client(client: object) -> APIClient: + """Cast the DRF test client so Pylance sees APIClient helpers.""" + + return cast(APIClient, client) + + +def _create_user(user_model: type[Any], **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +class TrendsApiTests(APITestCase): + """Exercise project-scoped trends API endpoints from the trends app.""" + + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpass123") + self.other_user = _create_user( + user_model, + username="other", + password="testpass123", + ) + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + self.other_project = Project.objects.create( + name="Other Project", + topic_description="Frontend", + ) + ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + ProjectMembership.objects.create( + user=self.other_user, + project=self.other_project, + role=ProjectRole.ADMIN, + ) + self.owner_entity = Entity.objects.create( + project=self.owner_project, + name="Owner Entity", + type="individual", + ) + self.other_entity = Entity.objects.create( + project=self.other_project, + name="Other Entity", + type="vendor", + ) + self.owner_content = Content.objects.create( + project=self.owner_project, + url="https://example.com/owner", + title="Owner Content", + author="Owner Author", + entity=self.owner_entity, + source_plugin="rss", + published_date="2026-04-21T00:00:00Z", + content_text="Owner content text", + ) + self.other_content = Content.objects.create( + project=self.other_project, + url="https://example.com/other", + title="Other Content", + author="Other Author", + entity=self.other_entity, + source_plugin="rss", + published_date="2026-04-21T00:00:00Z", + content_text="Other content text", + ) + ProjectConfig.objects.create(project=self.owner_project) + self.owner_topic_centroid_snapshot = TopicCentroidSnapshot.objects.create( + project=self.owner_project, + centroid_active=True, + centroid_vector=[1.0, 0.0], + feedback_count=10, + upvote_count=8, + downvote_count=2, + drift_from_previous=0.1, + drift_from_week_ago=0.2, + ) + _typed_client(self.client).force_authenticate(self.owner) + + def test_topic_centroid_summary_action_returns_latest_snapshot_and_averages(self): + older_snapshot = TopicCentroidSnapshot.objects.create( + project=self.owner_project, + centroid_active=True, + centroid_vector=[0.0, 1.0], + feedback_count=8, + upvote_count=6, + downvote_count=2, + drift_from_previous=0.2, + drift_from_week_ago=0.3, + ) + TopicCentroidSnapshot.objects.filter(pk=older_snapshot.pk).update( + computed_at="2026-04-20T00:00:00Z" + ) + + response = self.client.get( + reverse( + "v1:project-topic-centroid-snapshot-summary", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["project"], _require_pk(self.owner_project)) + self.assertEqual(response.json()["snapshot_count"], 2) + self.assertEqual(response.json()["active_snapshot_count"], 2) + self.assertEqual( + response.json()["latest_snapshot"]["id"], + _require_pk(self.owner_topic_centroid_snapshot), + ) + self.assertAlmostEqual(response.json()["avg_drift_from_previous"], 0.15) + self.assertAlmostEqual(response.json()["avg_drift_from_week_ago"], 0.25) + + def test_topic_cluster_list_returns_current_velocity_annotation(self): + cluster = TopicCluster.objects.create( + project=self.owner_project, + first_seen_at="2026-04-22T00:00:00Z", + last_seen_at="2026-04-24T00:00:00Z", + is_active=True, + member_count=3, + dominant_entity=self.owner_entity, + ) + TopicVelocitySnapshot.objects.create( + cluster=cluster, + project=self.owner_project, + window_count=3, + trailing_mean=1.0, + trailing_stddev=0.5, + z_score=2.0, + velocity_score=0.9, + ) + + response = self.client.get( + reverse( + "v1:project-topic-cluster-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]["id"], _require_pk(cluster)) + self.assertAlmostEqual(response.json()[0]["velocity_score"], 0.9) + + def test_topic_cluster_detail_and_velocity_history_action_return_memberships(self): + cluster = TopicCluster.objects.create( + project=self.owner_project, + first_seen_at="2026-04-22T00:00:00Z", + last_seen_at="2026-04-24T00:00:00Z", + is_active=True, + member_count=1, + dominant_entity=self.owner_entity, + ) + ContentClusterMembership.objects.create( + content=self.owner_content, + cluster=cluster, + project=self.owner_project, + similarity=0.92, + ) + first_snapshot = TopicVelocitySnapshot.objects.create( + cluster=cluster, + project=self.owner_project, + window_count=2, + trailing_mean=1.0, + trailing_stddev=0.2, + z_score=1.5, + velocity_score=0.75, + ) + second_snapshot = TopicVelocitySnapshot.objects.create( + cluster=cluster, + project=self.owner_project, + window_count=3, + trailing_mean=1.0, + trailing_stddev=0.3, + z_score=3.0, + velocity_score=1.0, + ) + TopicVelocitySnapshot.objects.filter(pk=first_snapshot.pk).update( + computed_at="2026-04-23T00:00:00Z" + ) + TopicVelocitySnapshot.objects.filter(pk=second_snapshot.pk).update( + computed_at="2026-04-24T00:00:00Z" + ) + + detail_response = self.client.get( + reverse( + "v1:project-topic-cluster-detail", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(cluster), + }, + ) + ) + history_response = self.client.get( + reverse( + "v1:project-topic-cluster-velocity-history", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(cluster), + }, + ), + {"limit": 1}, + ) + + self.assertEqual(detail_response.status_code, status.HTTP_200_OK) + self.assertEqual(detail_response.json()["id"], _require_pk(cluster)) + self.assertEqual(len(detail_response.json()["memberships"]), 1) + self.assertEqual( + detail_response.json()["memberships"][0]["content"]["id"], + _require_pk(self.owner_content), + ) + self.assertEqual(len(detail_response.json()["velocity_history"]), 2) + self.assertEqual( + detail_response.json()["velocity_history"][0]["id"], + _require_pk(second_snapshot), + ) + + self.assertEqual(history_response.status_code, status.HTTP_200_OK) + self.assertEqual(len(history_response.json()), 1) + self.assertEqual(history_response.json()[0]["id"], _require_pk(second_snapshot)) + + def test_theme_suggestion_list_is_scoped_to_project(self): + cluster = TopicCluster.objects.create( + project=self.owner_project, + first_seen_at="2026-04-22T00:00:00Z", + last_seen_at="2026-04-24T00:00:00Z", + is_active=True, + member_count=3, + dominant_entity=self.owner_entity, + ) + suggestion = ThemeSuggestion.objects.create( + project=self.owner_project, + cluster=cluster, + title="Owner Theme", + pitch="Owner pitch", + why_it_matters="Owner why", + suggested_angle="Owner angle", + velocity_at_creation=0.9, + novelty_score=0.8, + ) + other_cluster = TopicCluster.objects.create( + project=self.other_project, + first_seen_at="2026-04-22T00:00:00Z", + last_seen_at="2026-04-24T00:00:00Z", + is_active=True, + member_count=3, + dominant_entity=self.other_entity, + ) + ThemeSuggestion.objects.create( + project=self.other_project, + cluster=other_cluster, + title="Other Theme", + pitch="Other pitch", + why_it_matters="Other why", + suggested_angle="Other angle", + velocity_at_creation=0.8, + novelty_score=0.7, + ) + + response = self.client.get( + reverse( + "v1:project-theme-suggestion-list", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]["id"], _require_pk(suggestion)) + self.assertEqual(response.json()[0]["status"], ThemeSuggestionStatus.PENDING) + + def test_theme_suggestion_accept_and_dismiss_actions_update_workflow_fields(self): + cluster = TopicCluster.objects.create( + project=self.owner_project, + first_seen_at="2026-04-22T00:00:00Z", + last_seen_at="2026-04-24T00:00:00Z", + is_active=True, + member_count=3, + dominant_entity=self.owner_entity, + ) + promoted_content = Content.objects.create( + project=self.owner_project, + url="https://example.com/promoted-by-theme", + title="Promoted by Theme", + author="Owner Author", + entity=self.owner_entity, + source_plugin=SourcePluginName.RSS, + published_date="2026-04-24T00:00:00Z", + content_text="Promoted content text", + ) + ContentClusterMembership.objects.create( + content=promoted_content, + cluster=cluster, + project=self.owner_project, + similarity=0.94, + ) + accept_suggestion = ThemeSuggestion.objects.create( + project=self.owner_project, + cluster=cluster, + title="Accept Theme", + pitch="Pitch", + why_it_matters="Why", + suggested_angle="Angle", + velocity_at_creation=0.9, + novelty_score=0.8, + ) + dismiss_suggestion = ThemeSuggestion.objects.create( + project=self.owner_project, + cluster=cluster, + title="Dismiss Theme", + pitch="Pitch", + why_it_matters="Why", + suggested_angle="Angle", + velocity_at_creation=0.7, + novelty_score=0.75, + ) + + accept_response = self.client.post( + reverse( + "v1:project-theme-suggestion-accept", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(accept_suggestion), + }, + ), + format="json", + ) + dismiss_response = self.client.post( + reverse( + "v1:project-theme-suggestion-dismiss", + kwargs={ + "project_id": _require_pk(self.owner_project), + "pk": _require_pk(dismiss_suggestion), + }, + ), + {"reason": "already covered"}, + format="json", + ) + + accept_suggestion.refresh_from_db() + dismiss_suggestion.refresh_from_db() + promoted_content.refresh_from_db() + self.assertEqual(accept_response.status_code, status.HTTP_200_OK) + self.assertEqual(accept_suggestion.status, ThemeSuggestionStatus.ACCEPTED) + self.assertEqual(accept_suggestion.decided_by, self.owner) + self.assertIsNotNone(accept_suggestion.decided_at) + self.assertEqual(promoted_content.newsletter_promotion_theme, accept_suggestion) + self.assertEqual(promoted_content.newsletter_promotion_by, self.owner) + self.assertIsNotNone(promoted_content.newsletter_promotion_at) + self.assertEqual(len(accept_response.json()["promoted_contents"]), 1) + self.assertEqual( + accept_response.json()["promoted_contents"][0]["id"], + _require_pk(promoted_content), + ) + + self.assertEqual(dismiss_response.status_code, status.HTTP_200_OK) + self.assertEqual(dismiss_suggestion.status, ThemeSuggestionStatus.DISMISSED) + self.assertEqual(dismiss_suggestion.dismissal_reason, "already covered") + self.assertEqual(dismiss_suggestion.decided_by, self.owner) diff --git a/trends/tests/test_tasks.py b/trends/tests/test_tasks.py new file mode 100644 index 00000000..dbc77696 --- /dev/null +++ b/trends/tests/test_tasks.py @@ -0,0 +1,696 @@ +from datetime import datetime, timedelta, timezone +from types import SimpleNamespace + +import pytest +from django.db.models import Model + +from content.models import Content, FeedbackType, UserFeedback +from entities.models import Entity, EntityMention, EntityMentionRole +from projects.model_support import SourcePluginName +from projects.models import Project +from trends.models import ( + ContentClusterMembership, + ThemeSuggestion, + ThemeSuggestionStatus, + TopicCentroidSnapshot, + TopicCluster, + TopicVelocitySnapshot, +) +from trends.tasks import ( + TOPIC_CENTROID_MIN_UPVOTES, + accept_theme_suggestion, + assign_content_to_topic_cluster, + generate_theme_suggestions, + queue_topic_centroid_recompute, + recompute_topic_centroid, + recompute_topic_clusters, + recompute_topic_velocity, + run_all_topic_centroid_recomputations, + run_all_topic_cluster_recomputations, +) + +pytestmark = pytest.mark.django_db + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed task assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +@pytest.fixture +def source_plugin_context(django_user_model): + user = django_user_model.objects.create_user( + username="plugin-owner", + password="testpass123", + ) + project = Project.objects.create(name="Plugin Project", topic_description="Infra") + entity = Entity.objects.create( + project=project, + name="Example", + type="vendor", + website_url="https://example.com", + ) + return SimpleNamespace(user=user, project=project, entity=entity) + + +def test_recompute_topic_centroid_upserts_weighted_normalized_centroid( + source_plugin_context, mocker +): + project = source_plugin_context.project + mocker.patch("content.signals.queue_topic_centroid_recompute") + upsert_mock = mocker.patch("trends.tasks.upsert_topic_centroid") + delete_mock = mocker.patch("trends.tasks.delete_topic_centroid") + mocker.patch("trends.tasks.embed_text", return_value=[1.0, -0.25]) + + upvote_contents = [] + for index in range(TOPIC_CENTROID_MIN_UPVOTES): + upvote_contents.append( + Content.objects.create( + project=project, + entity=source_plugin_context.entity, + url=f"https://example.com/upvote-{index}", + title=f"Upvote {index}", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-20T12:00:00Z", + content_text="Manual content body", + ) + ) + downvote_content = Content.objects.create( + project=project, + entity=source_plugin_context.entity, + url="https://example.com/downvote", + title="Downvote", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-20T12:00:00Z", + content_text="Manual content body", + ) + for content in upvote_contents: + UserFeedback.objects.create( + project=project, + content=content, + user=source_plugin_context.user, + feedback_type=FeedbackType.UPVOTE, + ) + second_user = source_plugin_context.user.__class__.objects.create_user( + username="downvote-owner", + password="testpass123", + ) + UserFeedback.objects.create( + project=project, + content=downvote_content, + user=second_user, + feedback_type=FeedbackType.DOWNVOTE, + ) + + result = recompute_topic_centroid(project.id) + snapshot = TopicCentroidSnapshot.objects.get(project=project) + + assert result["centroid_active"] is True + delete_mock.assert_not_called() + upsert_mock.assert_called_once() + centroid_vector = upsert_mock.call_args.args[1] + assert centroid_vector[0] > 0.9 + assert centroid_vector[1] < 0.0 + assert snapshot.centroid_active is True + assert snapshot.feedback_count == TOPIC_CENTROID_MIN_UPVOTES + 1 + assert snapshot.upvote_count == TOPIC_CENTROID_MIN_UPVOTES + assert snapshot.downvote_count == 1 + assert snapshot.centroid_vector == pytest.approx(centroid_vector) + assert snapshot.drift_from_previous is None + assert snapshot.drift_from_week_ago is None + + +def test_recompute_topic_centroid_persists_drift_from_previous_and_week_old_snapshot( + source_plugin_context, mocker +): + project = source_plugin_context.project + mocker.patch("content.signals.queue_topic_centroid_recompute") + upsert_mock = mocker.patch("trends.tasks.upsert_topic_centroid") + delete_mock = mocker.patch("trends.tasks.delete_topic_centroid") + mocker.patch("trends.tasks.embed_text", return_value=[1.0, 0.0]) + + recent_snapshot = TopicCentroidSnapshot.objects.create( + project=project, + centroid_active=True, + centroid_vector=[1.0, 0.0], + feedback_count=12, + upvote_count=12, + downvote_count=0, + ) + older_snapshot = TopicCentroidSnapshot.objects.create( + project=project, + centroid_active=True, + centroid_vector=[0.0, 1.0], + feedback_count=12, + upvote_count=12, + downvote_count=0, + ) + TopicCentroidSnapshot.objects.filter(pk=recent_snapshot.pk).update( + computed_at=datetime(2026, 4, 28, 12, 0, tzinfo=timezone.utc) + ) + TopicCentroidSnapshot.objects.filter(pk=older_snapshot.pk).update( + computed_at=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc) + ) + + for index in range(TOPIC_CENTROID_MIN_UPVOTES): + content = Content.objects.create( + project=project, + entity=source_plugin_context.entity, + url=f"https://example.com/drift-upvote-{index}", + title=f"Drift Upvote {index}", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-20T12:00:00Z", + content_text="Manual content body", + ) + UserFeedback.objects.create( + project=project, + content=content, + user=source_plugin_context.user, + feedback_type=FeedbackType.UPVOTE, + ) + + result = recompute_topic_centroid(project.id) + snapshot = TopicCentroidSnapshot.objects.filter(project=project).latest( + "computed_at" + ) + + assert result["centroid_active"] is True + delete_mock.assert_not_called() + upsert_mock.assert_called_once() + assert snapshot.centroid_active is True + assert snapshot.drift_from_previous == pytest.approx(0.0) + assert snapshot.drift_from_week_ago == pytest.approx(1.0) + + +def test_recompute_topic_centroid_disables_centroid_below_minimum_upvotes( + source_plugin_context, mocker +): + project = source_plugin_context.project + mocker.patch("content.signals.queue_topic_centroid_recompute") + upsert_mock = mocker.patch("trends.tasks.upsert_topic_centroid") + delete_mock = mocker.patch("trends.tasks.delete_topic_centroid") + for index in range(TOPIC_CENTROID_MIN_UPVOTES - 1): + content = Content.objects.create( + project=project, + entity=source_plugin_context.entity, + url=f"https://example.com/too-few-{index}", + title=f"Too Few {index}", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-20T12:00:00Z", + content_text="Manual content body", + ) + UserFeedback.objects.create( + project=project, + content=content, + user=source_plugin_context.user, + feedback_type=FeedbackType.UPVOTE, + ) + + result = recompute_topic_centroid(project.id) + snapshot = TopicCentroidSnapshot.objects.get(project=project) + + assert result["centroid_active"] is False + delete_mock.assert_called_once_with(project.id) + upsert_mock.assert_not_called() + assert snapshot.centroid_active is False + assert snapshot.centroid_vector == [] + assert snapshot.upvote_count == TOPIC_CENTROID_MIN_UPVOTES - 1 + assert snapshot.drift_from_previous is None + + +def test_run_all_topic_centroid_recomputations_enqueues_all_projects( + source_plugin_context, mocker +): + delay_mock = mocker.patch("trends.tasks.recompute_topic_centroid.delay") + other_project = Project.objects.create( + name="Other Centroid Project", + topic_description="Security", + ) + + enqueued_count = run_all_topic_centroid_recomputations() + + assert enqueued_count == 2 + delay_mock.assert_any_call(source_plugin_context.project.id) + delay_mock.assert_any_call(_require_pk(other_project)) + assert delay_mock.call_count == 2 + + +def test_run_all_topic_centroid_recomputations_executes_inline_when_eager( + source_plugin_context, settings, mocker +): + settings.CELERY_TASK_ALWAYS_EAGER = True + recompute_mock = mocker.patch("trends.tasks.recompute_topic_centroid") + delay_mock = mocker.patch("trends.tasks.recompute_topic_centroid.delay") + other_project = Project.objects.create( + name="Inline Centroid Project", + topic_description="Platform", + ) + + enqueued_count = run_all_topic_centroid_recomputations() + + assert enqueued_count == 2 + recompute_mock.assert_any_call(source_plugin_context.project.id) + recompute_mock.assert_any_call(_require_pk(other_project)) + assert recompute_mock.call_count == 2 + delay_mock.assert_not_called() + + +def test_run_all_topic_cluster_recomputations_enqueues_all_projects( + source_plugin_context, mocker +): + delay_mock = mocker.patch("trends.tasks.recompute_topic_clusters.delay") + other_project = Project.objects.create( + name="Other Cluster Project", + topic_description="Security", + ) + + enqueued_count = run_all_topic_cluster_recomputations() + + assert enqueued_count == 2 + delay_mock.assert_any_call(source_plugin_context.project.id) + delay_mock.assert_any_call(_require_pk(other_project)) + assert delay_mock.call_count == 2 + + +def test_queue_topic_centroid_recompute_enqueues_background_task( + source_plugin_context, mocker +): + cache_add_mock = mocker.patch("trends.tasks.cache.add", return_value=True) + delay_mock = mocker.patch("trends.tasks.recompute_topic_centroid.delay") + + queued = queue_topic_centroid_recompute(source_plugin_context.project.id) + + assert queued is True + cache_add_mock.assert_called_once() + delay_mock.assert_called_once_with(source_plugin_context.project.id) + + +def test_queue_topic_centroid_recompute_skips_duplicate_queue_attempts( + source_plugin_context, mocker +): + mocker.patch("trends.tasks.cache.add", return_value=False) + delay_mock = mocker.patch("trends.tasks.recompute_topic_centroid.delay") + + queued = queue_topic_centroid_recompute(source_plugin_context.project.id) + + assert queued is False + delay_mock.assert_not_called() + + +def test_recompute_topic_clusters_groups_recent_similar_content( + source_plugin_context, mocker +): + project = source_plugin_context.project + second_entity = Entity.objects.create( + project=project, + name="Secondary Entity", + type="vendor", + ) + vector_lookup = { + "Trend 1": [1.0, 0.0], + "Trend 2": [0.99, 0.01], + "Trend 3": [0.98, 0.02], + "Trend 4": [0.97, 0.03], + "Outlier": [0.0, 1.0], + } + mocker.patch( + "trends.tasks.embed_text", + side_effect=lambda text: vector_lookup[text.split("\n\n", 1)[0]], + ) + delay_mock = mocker.patch("trends.tasks.recompute_topic_velocity.delay") + + clustered_contents = [] + for index in range(4): + content = Content.objects.create( + project=project, + entity=source_plugin_context.entity, + url=f"https://example.com/trend-{index}", + title=f"Trend {index + 1}", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date=f"2026-04-2{index}T12:00:00Z", + content_text="Clusterable trend content", + ) + clustered_contents.append(content) + EntityMention.objects.create( + project=project, + content=content, + entity=source_plugin_context.entity, + role=EntityMentionRole.SUBJECT, + ) + outlier = Content.objects.create( + project=project, + entity=second_entity, + url="https://example.com/outlier", + title="Outlier", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-24T12:00:00Z", + content_text="Outlier trend content", + ) + + result = recompute_topic_clusters(project.id) + + cluster = TopicCluster.objects.get(project=project, is_active=True) + memberships = list( + ContentClusterMembership.objects.filter(cluster=cluster).values_list( + "content_id", flat=True + ) + ) + + assert result["contents_considered"] == 5 + assert result["clusters_updated"] == 1 + assert cluster.member_count == 4 + assert cluster.dominant_entity == source_plugin_context.entity + assert set(memberships) == {_require_pk(content) for content in clustered_contents} + assert _require_pk(outlier) not in memberships + delay_mock.assert_called_once_with(project.id) + + +def test_assign_content_to_topic_cluster_adds_similar_content_to_existing_cluster( + source_plugin_context, mocker +): + project = source_plugin_context.project + vector_lookup = { + "Cluster 1": [1.0, 0.0], + "Cluster 2": [0.99, 0.01], + "Cluster 3": [0.98, 0.02], + "Candidate": [0.97, 0.03], + } + mocker.patch( + "trends.tasks.embed_text", + side_effect=lambda text: vector_lookup[text.split("\n\n", 1)[0]], + ) + + existing_contents = [] + for index in range(3): + content = Content.objects.create( + project=project, + entity=source_plugin_context.entity, + url=f"https://example.com/cluster-{index}", + title=f"Cluster {index + 1}", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date=f"2026-04-2{index}T12:00:00Z", + content_text="Existing cluster content", + ) + existing_contents.append(content) + cluster = TopicCluster.objects.create( + project=project, + first_seen_at=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), + last_seen_at=datetime(2026, 4, 22, 12, 0, tzinfo=timezone.utc), + is_active=True, + member_count=3, + dominant_entity=source_plugin_context.entity, + ) + ContentClusterMembership.objects.bulk_create( + [ + ContentClusterMembership( + content=content, + cluster=cluster, + project=project, + similarity=0.9, + ) + for content in existing_contents + ] + ) + candidate = Content.objects.create( + project=project, + entity=source_plugin_context.entity, + url="https://example.com/candidate", + title="Candidate", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-24T12:00:00Z", + content_text="New similar cluster content", + ) + + result = assign_content_to_topic_cluster(_require_pk(candidate)) + + cluster.refresh_from_db() + membership = ContentClusterMembership.objects.get(content=candidate) + assert result["assigned"] is True + assert result["cluster_id"] == _require_pk(cluster) + assert membership.cluster == cluster + assert cluster.member_count == 4 + assert cluster.is_active is True + + +def test_recompute_topic_velocity_detects_synthetic_burst( + source_plugin_context, mocker +): + project = source_plugin_context.project + fixed_now = datetime(2026, 4, 30, 12, 0, tzinfo=timezone.utc) + mocker.patch("trends.tasks.timezone.now", return_value=fixed_now) + cluster = TopicCluster.objects.create( + project=project, + first_seen_at=fixed_now - timedelta(days=8), + last_seen_at=fixed_now, + is_active=True, + member_count=11, + dominant_entity=source_plugin_context.entity, + ) + + membership_rows = [] + for offset in range(1, 8): + content = Content.objects.create( + project=project, + entity=source_plugin_context.entity, + url=f"https://example.com/baseline-{offset}", + title=f"Baseline {offset}", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date=fixed_now - timedelta(days=offset, hours=1), + content_text="Baseline trend content", + ) + membership_rows.append( + ContentClusterMembership( + content=content, + cluster=cluster, + project=project, + similarity=0.9, + ) + ) + for index in range(4): + content = Content.objects.create( + project=project, + entity=source_plugin_context.entity, + url=f"https://example.com/burst-{index}", + title=f"Burst {index}", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date=fixed_now - timedelta(hours=index + 1), + content_text="Burst trend content", + ) + membership_rows.append( + ContentClusterMembership( + content=content, + cluster=cluster, + project=project, + similarity=0.95, + ) + ) + ContentClusterMembership.objects.bulk_create(membership_rows) + + result = recompute_topic_velocity(project.id) + + snapshot = TopicVelocitySnapshot.objects.get(cluster=cluster) + assert result["clusters_evaluated"] == 1 + assert result["snapshots_created"] == 1 + assert snapshot.window_count == 4 + assert snapshot.trailing_mean == pytest.approx(1.0) + assert snapshot.trailing_stddev == pytest.approx(0.0) + assert snapshot.z_score == pytest.approx(3.0) + assert snapshot.velocity_score == pytest.approx(1.0) + + +def test_generate_theme_suggestions_creates_pending_suggestion( + source_plugin_context, settings, mocker +): + settings.OPENROUTER_API_KEY = "test-key" + project = source_plugin_context.project + cluster = TopicCluster.objects.create( + project=project, + first_seen_at=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), + last_seen_at=datetime(2026, 4, 24, 12, 0, tzinfo=timezone.utc), + is_active=True, + member_count=3, + dominant_entity=source_plugin_context.entity, + ) + content = Content.objects.create( + project=project, + entity=source_plugin_context.entity, + url="https://example.com/theme-source", + title="Theme Source", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-24T12:00:00Z", + content_text="Theme source content", + ) + ContentClusterMembership.objects.create( + content=content, + cluster=cluster, + project=project, + similarity=0.95, + ) + TopicVelocitySnapshot.objects.create( + cluster=cluster, + project=project, + window_count=4, + trailing_mean=1.0, + trailing_stddev=0.0, + z_score=3.0, + velocity_score=1.0, + ) + llm_mock = mocker.patch( + "trends.tasks.openrouter_chat_json", + side_effect=[ + SimpleNamespace( + payload={ + "title": "Platform teams are consolidating around one workflow", + "one_sentence_pitch": "A burst of similar coverage suggests a coherent newsletter theme.", + "why_it_matters": "Editors can turn the cluster into a timely section.", + "suggested_angle": "Explain what changed this week.", + }, + model=settings.AI_SUMMARIZATION_MODEL, + latency_ms=123, + ), + SimpleNamespace( + payload={"novelty_score": 0.91, "explanation": "Novel enough."}, + model=settings.AI_RELEVANCE_MODEL, + latency_ms=98, + ), + ], + ) + + result = generate_theme_suggestions(project.id) + + suggestion = ThemeSuggestion.objects.get(project=project, cluster=cluster) + assert result["created"] == 1 + assert suggestion.status == ThemeSuggestionStatus.PENDING + assert suggestion.title == "Platform teams are consolidating around one workflow" + assert suggestion.novelty_score == pytest.approx(0.91) + assert suggestion.velocity_at_creation == pytest.approx(1.0) + assert llm_mock.call_count == 2 + + +def test_generate_theme_suggestions_updates_existing_pending_for_same_cluster( + source_plugin_context, +): + project = source_plugin_context.project + cluster = TopicCluster.objects.create( + project=project, + first_seen_at=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), + last_seen_at=datetime(2026, 4, 24, 12, 0, tzinfo=timezone.utc), + is_active=True, + member_count=3, + dominant_entity=source_plugin_context.entity, + ) + TopicVelocitySnapshot.objects.create( + cluster=cluster, + project=project, + window_count=4, + trailing_mean=1.0, + trailing_stddev=0.0, + z_score=3.0, + velocity_score=0.88, + ) + suggestion = ThemeSuggestion.objects.create( + project=project, + cluster=cluster, + title="Existing pending theme", + pitch="Pitch", + why_it_matters="Why", + suggested_angle="Angle", + velocity_at_creation=0.2, + novelty_score=0.8, + ) + + result = generate_theme_suggestions(project.id) + + suggestion.refresh_from_db() + assert result["created"] == 0 + assert result["updated"] == 1 + assert ThemeSuggestion.objects.filter(project=project, cluster=cluster).count() == 1 + assert suggestion.velocity_at_creation == pytest.approx(0.88) + + +def test_accept_theme_suggestion_marks_cluster_members_for_newsletter_promotion( + source_plugin_context, +): + project = source_plugin_context.project + cluster = TopicCluster.objects.create( + project=project, + first_seen_at=datetime(2026, 4, 20, 12, 0, tzinfo=timezone.utc), + last_seen_at=datetime(2026, 4, 24, 12, 0, tzinfo=timezone.utc), + is_active=True, + member_count=2, + dominant_entity=source_plugin_context.entity, + ) + primary_content = Content.objects.create( + project=project, + entity=source_plugin_context.entity, + url="https://example.com/promote-1", + title="Promoted One", + author="Author", + source_plugin=SourcePluginName.RSS, + published_date="2026-04-24T12:00:00Z", + content_text="Primary theme content", + ) + secondary_content = Content.objects.create( + project=project, + entity=source_plugin_context.entity, + url="https://example.com/promote-2", + title="Promoted Two", + author="Author", + source_plugin=SourcePluginName.REDDIT, + published_date="2026-04-24T13:00:00Z", + content_text="Secondary theme content", + ) + ContentClusterMembership.objects.bulk_create( + [ + ContentClusterMembership( + content=primary_content, + cluster=cluster, + project=project, + similarity=0.95, + ), + ContentClusterMembership( + content=secondary_content, + cluster=cluster, + project=project, + similarity=0.9, + ), + ] + ) + suggestion = ThemeSuggestion.objects.create( + project=project, + cluster=cluster, + title="Accepted Theme", + pitch="Pitch", + why_it_matters="Why", + suggested_angle="Angle", + velocity_at_creation=0.7, + novelty_score=0.8, + ) + + accept_theme_suggestion(suggestion, user_id=source_plugin_context.user.id) + + suggestion.refresh_from_db() + primary_content.refresh_from_db() + secondary_content.refresh_from_db() + assert suggestion.status == ThemeSuggestionStatus.ACCEPTED + assert primary_content.newsletter_promotion_theme == suggestion + assert secondary_content.newsletter_promotion_theme == suggestion + assert primary_content.newsletter_promotion_by == source_plugin_context.user + assert secondary_content.newsletter_promotion_by == source_plugin_context.user + assert primary_content.newsletter_promotion_at is not None + assert secondary_content.newsletter_promotion_at is not None diff --git a/users/tests/test_models.py b/users/tests/test_models.py index 128aab99..6394c5d9 100644 --- a/users/tests/test_models.py +++ b/users/tests/test_models.py @@ -1,7 +1,6 @@ """Tests for the custom AppUser foundation.""" import pytest -from django.contrib.auth.models import Group from projects.models import Project, ProjectMembership, ProjectRole from users.models import AppUser, avatar_upload_path @@ -22,12 +21,9 @@ def test_avatar_upload_path_uses_user_prefix(): def test_app_user_project_membership_drives_project_visibility(): - group = Group.objects.create(name="platform-team") user = AppUser.objects.create_user(username="reader", password="testpass123") - user.groups.add(group) project = Project.objects.create( name="Platform Weekly", - group=group, topic_description="Platform engineering", ) ProjectMembership.objects.create(