mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
453 lines
16 KiB
YAML
453 lines
16 KiB
YAML
# Canonical product structure definition
|
||
# Used by: insightscli product:bootstrap, insightscli product:lint
|
||
#
|
||
# Each file can have:
|
||
# required: bool - whether lint should warn if missing (in strict mode)
|
||
# required_lenient: bool - whether lint should warn if missing (in lenient mode)
|
||
# can_be_folder: bool - whether file.py can also be file/ directory
|
||
# template: str - content template with {product} and {Product} placeholders
|
||
#
|
||
# Modes:
|
||
# strict: product has backend/facade/contracts.py (isolated architecture)
|
||
# lenient: legacy product without contracts.py
|
||
|
||
# Root-level product files (not in backend/ or frontend/)
|
||
root_files:
|
||
__init__.py:
|
||
required: true
|
||
required_lenient: true
|
||
template: ''
|
||
|
||
manifest.tsx:
|
||
required: true
|
||
required_lenient: false
|
||
template: |
|
||
/**
|
||
* Product manifest for {product}.
|
||
*
|
||
* Defines scenes, routes, URLs, and navigation for this product.
|
||
*/
|
||
import {{ ProductManifest }} from '../../frontend/src/types'
|
||
|
||
export const manifest: ProductManifest = {{
|
||
name: '{Product}',
|
||
scenes: {{
|
||
// Define scenes here
|
||
}},
|
||
routes: {{
|
||
// Define routes here
|
||
}},
|
||
redirects: {{}},
|
||
urls: {{
|
||
// Define URL helpers here
|
||
}},
|
||
fileSystemTypes: {{}},
|
||
treeItemsNew: [],
|
||
treeItemsProducts: [],
|
||
}}
|
||
|
||
package.json:
|
||
required: true
|
||
required_lenient: true
|
||
template: |
|
||
{{
|
||
"name": "@hanzo/products-{product_kebab}",
|
||
"scripts": {{
|
||
"backend:test": "pytest -c ../../pytest.ini --rootdir ../.. backend/tests -v --tb=short",
|
||
"backend:contract-check": "echo 'Contract files unchanged'"
|
||
}}
|
||
}}
|
||
|
||
turbo.json:
|
||
required: false
|
||
required_lenient: false
|
||
template: |
|
||
{{
|
||
"extends": ["//"],
|
||
"tasks": {{
|
||
"backend:contract-check": {{
|
||
"inputs": ["backend/facade/**", "backend/presentation/**", "backend/tasks/**"],
|
||
"outputs": [],
|
||
"cache": true
|
||
}}
|
||
}}
|
||
}}
|
||
|
||
tsconfig.json:
|
||
required: true
|
||
required_lenient: false
|
||
template: |
|
||
{{
|
||
"extends": "../../tsconfig.json",
|
||
"compilerOptions": {{
|
||
"composite": true
|
||
}}
|
||
}}
|
||
|
||
# Backend files (in backend/)
|
||
#
|
||
# We deliberately do not scaffold `__init__.py` files for `backend/`,
|
||
# `backend/facade/`, `backend/presentation/`, or `backend/tasks/`. Python 3.3+
|
||
# implicit namespace packages handle imports without them, and the convention
|
||
# under products/ is to keep package surfaces intentional — either populate
|
||
# `__init__.py` with explicit exports (rare) or omit it.
|
||
# `tests/__init__.py` is the one exception: pytest's default `prepend` import
|
||
# mode would collide on duplicate test filenames across products (11×
|
||
# `test_models.py`, 11× `test_api.py`, etc.) without it.
|
||
backend_files:
|
||
apps.py:
|
||
required: true
|
||
template: |
|
||
"""Django app configuration for {product}."""
|
||
from django.apps import AppConfig
|
||
|
||
|
||
class {Product}Config(AppConfig):
|
||
name = "products.{product}.backend"
|
||
label = "{product}"
|
||
|
||
models.py:
|
||
required: true
|
||
can_be_folder: true
|
||
template: |
|
||
"""
|
||
Django models for {product}.
|
||
|
||
Keep models thin — business logic belongs in logic/.
|
||
Use types from facade/enums.py where applicable.
|
||
Avoid ForeignKeys to models outside this app; if needed,
|
||
disallow reverse relations with related_name='+'.
|
||
"""
|
||
|
||
import uuid
|
||
|
||
from django.db import models
|
||
|
||
{team_base_import}
|
||
|
||
from .facade.enums import SplineStatus
|
||
|
||
|
||
# Inherits {team_base_mixin} so models opt into fail-closed team scoping —
|
||
# queries without team context raise TeamScopeError instead of silently
|
||
# returning every team's rows. See insights/models/scoping/README.md.
|
||
# Main-DB products (TeamScopedRootMixin): add `team = models.ForeignKey(
|
||
# "insights.Team", on_delete=models.CASCADE)` to the subclass.
|
||
class SplineReticulator({team_base_mixin}):
|
||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||
name = models.CharField(max_length=255)
|
||
status = models.CharField(
|
||
max_length=32,
|
||
choices=[(s.value, s.value) for s in SplineStatus],
|
||
default=SplineStatus.PENDING,
|
||
)
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
|
||
def __str__(self) -> str:
|
||
return self.name
|
||
|
||
logic/:
|
||
# Scaffolded as a package because business logic tends to grow fast and
|
||
# splitting a single logic.py later is more churn than starting with a
|
||
# package. Legacy products with a flat logic.py are still fine — it's
|
||
# not a required file. FileFolderConflictsCheck only complains if
|
||
# logic.py and logic/ both exist in the same product.
|
||
__init__.py:
|
||
required: false
|
||
template: |
|
||
"""Business logic for {product}."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from ..facade.enums import SplineStatus
|
||
from ..models import SplineReticulator
|
||
|
||
|
||
def create_spline_reticulator(*, team_id: int, name: str) -> SplineReticulator:
|
||
return SplineReticulator.objects.create(team_id=team_id, name=name, status=SplineStatus.PENDING)
|
||
|
||
|
||
def list_spline_reticulators() -> list[SplineReticulator]:
|
||
return list(SplineReticulator.objects.all())
|
||
|
||
tasks/:
|
||
tasks.py:
|
||
required: false
|
||
template: |
|
||
"""
|
||
Celery tasks for {product}.
|
||
|
||
Async entrypoints that call the facade (facade/api.py).
|
||
Keep task functions thin - only call facade methods.
|
||
"""
|
||
# from celery import shared_task
|
||
# from ..facade import api
|
||
|
||
schedules.py:
|
||
required: false
|
||
template: |
|
||
"""Celery beat schedules for {product}."""
|
||
# Define periodic task schedules here
|
||
|
||
facade/:
|
||
api.py:
|
||
required: true
|
||
template: |
|
||
"""
|
||
Facade for {product}.
|
||
|
||
The ONLY module other products are allowed to import.
|
||
Accept frozen dataclasses, call logic/, return frozen
|
||
dataclasses. Never return ORM instances or import DRF.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from .. import logic
|
||
from ..models import SplineReticulator
|
||
from . import contracts
|
||
from .enums import SplineStatus
|
||
|
||
|
||
def _to_dto(obj: SplineReticulator) -> contracts.SplineReticulatorDTO:
|
||
return contracts.SplineReticulatorDTO(
|
||
id=obj.id,
|
||
name=obj.name,
|
||
status=SplineStatus(obj.status),
|
||
created_at=obj.created_at,
|
||
)
|
||
|
||
|
||
def create(input: contracts.CreateSplineReticulatorInput) -> contracts.SplineReticulatorDTO:
|
||
obj = logic.create_spline_reticulator(team_id=input.team_id, name=input.name)
|
||
return _to_dto(obj)
|
||
|
||
|
||
def list_all() -> list[contracts.SplineReticulatorDTO]:
|
||
return [_to_dto(obj) for obj in logic.list_spline_reticulators()]
|
||
|
||
contracts.py:
|
||
required: true
|
||
template: |
|
||
"""
|
||
Contract types for {product}.
|
||
|
||
Frozen dataclasses that define what this product exposes.
|
||
No Django imports. Used by facade as inputs/outputs.
|
||
|
||
Uses ``pydantic.dataclasses.dataclass`` rather than the stdlib
|
||
variant — same syntax, same ``is_dataclass()`` compatibility (so
|
||
``DataclassSerializer`` keeps working), but with runtime type
|
||
validation on construction.
|
||
"""
|
||
|
||
from datetime import datetime
|
||
from uuid import UUID
|
||
|
||
from pydantic.dataclasses import dataclass
|
||
|
||
from .enums import SplineStatus
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class SplineReticulatorDTO:
|
||
id: UUID
|
||
name: str
|
||
status: SplineStatus
|
||
created_at: datetime
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class CreateSplineReticulatorInput:
|
||
team_id: int
|
||
name: str
|
||
|
||
enums.py:
|
||
required: false
|
||
template: |
|
||
"""
|
||
Exported enums for {product}.
|
||
|
||
If an enum appears in a contract dataclass field, it belongs here.
|
||
Internal-only constants (DB magic values, feature flags) stay in
|
||
the implementation (logic.py, models.py).
|
||
"""
|
||
|
||
from enum import StrEnum
|
||
|
||
|
||
class SplineStatus(StrEnum):
|
||
PENDING = "pending"
|
||
RETICULATED = "reticulated"
|
||
|
||
presentation/:
|
||
serializers.py:
|
||
required: true
|
||
template: |
|
||
"""DRF serializers for {product}."""
|
||
|
||
from rest_framework import serializers
|
||
from rest_framework_dataclasses.serializers import DataclassSerializer
|
||
|
||
from ..facade.contracts import SplineReticulatorDTO
|
||
|
||
|
||
class SplineReticulatorSerializer(DataclassSerializer):
|
||
class Meta:
|
||
dataclass = SplineReticulatorDTO
|
||
|
||
|
||
class CreateSplineReticulatorSerializer(serializers.Serializer):
|
||
name = serializers.CharField(max_length=255, help_text="Name of the spline to reticulate.")
|
||
|
||
views.py:
|
||
required: true
|
||
template: |
|
||
"""
|
||
DRF views for {product}.
|
||
|
||
Validate JSON via serializers, call facade methods,
|
||
return serialized responses. No business logic here.
|
||
"""
|
||
|
||
from drf_spectacular.utils import extend_schema
|
||
from rest_framework import status, viewsets
|
||
from rest_framework.request import Request
|
||
from rest_framework.response import Response
|
||
|
||
from insights.api.routing import TeamAndOrgViewSetMixin
|
||
|
||
from ..facade import api, contracts
|
||
from .serializers import CreateSplineReticulatorSerializer, SplineReticulatorSerializer
|
||
|
||
|
||
class SplineReticulatorViewSet(TeamAndOrgViewSetMixin, viewsets.GenericViewSet):
|
||
scope_object = "INTERNAL"
|
||
|
||
@extend_schema(responses={{200: SplineReticulatorSerializer(many=True)}})
|
||
def list(self, request: Request, **kwargs) -> Response:
|
||
items = api.list_all()
|
||
return Response(SplineReticulatorSerializer(items, many=True).data)
|
||
|
||
@extend_schema(request=CreateSplineReticulatorSerializer, responses={{201: SplineReticulatorSerializer}})
|
||
def create(self, request: Request, **kwargs) -> Response:
|
||
serializer = CreateSplineReticulatorSerializer(data=request.data)
|
||
serializer.is_valid(raise_exception=True)
|
||
dto = api.create(contracts.CreateSplineReticulatorInput(
|
||
team_id=self.team_id,
|
||
**serializer.validated_data,
|
||
))
|
||
return Response(SplineReticulatorSerializer(dto).data, status=status.HTTP_201_CREATED)
|
||
|
||
urls.py:
|
||
required: true
|
||
template: |
|
||
"""URL routes for {product}."""
|
||
|
||
from rest_framework.routers import DefaultRouter
|
||
|
||
from .views import SplineReticulatorViewSet
|
||
|
||
router = DefaultRouter()
|
||
router.register(r"spline_reticulators", SplineReticulatorViewSet, basename="spline_reticulators")
|
||
urlpatterns = router.urls
|
||
|
||
tests/:
|
||
__init__.py:
|
||
required: true
|
||
template: ''
|
||
|
||
conftest.py:
|
||
required: false
|
||
template: |
|
||
import pytest
|
||
|
||
from insights.models.scoping import team_scope
|
||
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _set_team_scope(request):
|
||
if request.node.get_closest_marker("django_db") is None:
|
||
yield
|
||
return
|
||
team = request.getfixturevalue("team")
|
||
with team_scope(team.id):
|
||
yield
|
||
|
||
test_models.py:
|
||
required: false
|
||
template: ''
|
||
|
||
test_logic.py:
|
||
required: false
|
||
template: ''
|
||
|
||
test_api.py:
|
||
required: false
|
||
template: |
|
||
from uuid import UUID
|
||
|
||
import pytest
|
||
|
||
from products.{product}.backend.facade import api
|
||
from products.{product}.backend.facade.contracts import CreateSplineReticulatorInput
|
||
from products.{product}.backend.facade.enums import SplineStatus
|
||
|
||
|
||
@pytest.mark.django_db
|
||
class TestSplineReticulatorAPI:
|
||
def test_create_and_list(self, team):
|
||
dto = api.create(CreateSplineReticulatorInput(team_id=team.id, name="test-spline"))
|
||
|
||
assert isinstance(dto.id, UUID)
|
||
assert dto.name == "test-spline"
|
||
assert dto.status == SplineStatus.PENDING
|
||
|
||
all_items = api.list_all()
|
||
assert len(all_items) == 1
|
||
assert all_items[0].id == dto.id
|
||
|
||
test_presentation.py:
|
||
required: false
|
||
template: ''
|
||
|
||
test_tasks.py:
|
||
required: false
|
||
template: ''
|
||
|
||
# Frontend files (in frontend/)
|
||
frontend_files:
|
||
components/:
|
||
__init__: false # just create the folder
|
||
|
||
scenes/:
|
||
__init__: false # just create the folder
|
||
|
||
hooks/:
|
||
__init__: false # just create the folder
|
||
|
||
logics/:
|
||
__init__: false # just create the folder
|
||
|
||
generated/:
|
||
__init__: false # for OpenAPI-generated TypeScript types
|
||
|
||
tests/:
|
||
__init__: false
|
||
|
||
# Files that are known but should live in specific locations
|
||
# Used by lint to detect misplaced files in backend/
|
||
# Only checked for isolated (strict) products — legacy products have flat backend/ by design
|
||
backend_known_files:
|
||
api.py: presentation/api.py
|
||
contracts.py: facade/contracts.py
|
||
serializers.py: presentation/serializers.py
|
||
views.py: presentation/views.py
|
||
urls.py: presentation/urls.py
|
||
tasks.py: tasks/tasks.py
|
||
schedules.py: tasks/schedules.py
|
||
test_logic.py: tests/test_logic.py
|
||
test_api.py: tests/test_api.py
|
||
test_presentation.py: tests/test_presentation.py
|
||
test_tasks.py: tests/test_tasks.py
|