Files
zeekay 1b367e2b62 chore: rebase the tree onto upstream master
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.
2026-08-01 15:44:40 -07:00

453 lines
16 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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