feat(arch-rules): TPL-016 flags large persona templates that skip shared/
Some checks failed
CI / ruff (push) Successful in 17s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running

Architecture rule that warns on any template under
app/modules/<m>/templates/<m>/{admin,merchant,store}/*.html that
exceeds 75 LOC AND does not {% include %} a `*/shared/*` partial.

Catches new persona-specific templates that inline body content rather
than sharing it with sibling personas (the project-wide pain point that
prompted the persona-template-consolidation work).

- Rule definition in .architecture-rules/frontend.yaml at warning
  severity. Suppressible per-file with `{# noqa: TPL-016 #}`.
- Check function `_check_persona_template_shared_include` in
  scripts/validate/validate_architecture.py, wired at both template
  validation sites (full scan + per-file -f mode).
- Loyalty was migrated under this rule and reports clean (5 legit
  exceptions carry noqa with reason).
- First run surfaces ~110 warnings across other modules — the
  migration backlog. Severity stays at warning until at least one
  non-loyalty module is migrated, then escalate to error.

See docs/architecture/persona-template-consolidation.md for the
pattern this rule guards.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 23:11:21 +02:00
parent f82dce30ca
commit f9a15deed7
2 changed files with 88 additions and 0 deletions

View File

@@ -644,6 +644,37 @@ template_rules:
exceptions:
- "shared/macros/headers.html"
- id: "TPL-016"
name: "Persona templates >75 LOC must include a shared/ partial"
severity: "warning"
description: |
Any persona template (under app/modules/<m>/templates/<m>/{admin,merchant,store}/*.html)
that exceeds 75 LOC AND does not {% include %} a `*/shared/*` partial is likely
duplicating body content that should live in a shared partial used by all three
personas. See docs/architecture/persona-template-consolidation.md for the pattern.
RIGHT (thin wrapper + shared body):
{% extends "store/base.html" %}
...page-header + loading/error...
{% set cards_api_prefix = '/store/loyalty' %}
{% set cards_base_url = '/store/' ~ store_code ~ '/loyalty/cards' %}
{% include 'loyalty/shared/cards-list.html' %}
WRONG (inlined table + filters that already exist in shared/):
{% extends "store/base.html" %}
...200 lines of inline <table>/filters/pagination identical to merchant's...
Suppress for legit exceptions (admin multi-merchant aggregator, store-only
hardware UI, persona-unique tabbed dashboard) with `{# noqa: TPL-016 #}`
anywhere in the file.
pattern:
file_pattern: "app/modules/*/templates/*/{admin,merchant,store}/*.html"
threshold_loc: 75
required_pattern: "{% include .*/shared/.*"
exceptions:
- "base.html"
- "partials/"
- id: "TPL-014"
name: "Use new modal_simple macro API with call block"
severity: "error"

View File

@@ -871,6 +871,10 @@ class ArchitectureValidator:
if not is_macro:
self._check_static_v_usage(file_path, content, lines)
# TPL-016: Persona templates >75 LOC should include a shared/ partial
if not is_macro:
self._check_persona_template_shared_include(file_path, content, lines)
# TPL-004: Check x-text usage for dynamic content
self._check_xtext_usage(file_path, content, lines)
@@ -1832,6 +1836,55 @@ class ArchitectureValidator:
suggestion="Replace url_for(...) with static_v(request, ...) so the URL carries ?v=<commit>",
)
_TPL_016_THRESHOLD = 75
_TPL_016_PERSONA_RE = re.compile(
r"app/modules/[^/]+/templates/[^/]+/(?:admin|merchant|store)/[^/]+\.html$"
)
_TPL_016_SHARED_INCLUDE_RE = re.compile(
r"""\{%\s*include\s+['"][^'"]*/shared/[^'"]+['"]"""
)
def _check_persona_template_shared_include(
self, file_path: Path, content: str, lines: list[str]
):
"""TPL-016: Persona templates >75 LOC should include a shared/ partial.
Catches new persona-specific templates that inline body content already
living (or worth living) in `<module>/templates/<module>/shared/*.html`.
See docs/architecture/persona-template-consolidation.md.
Skip on file-level `noqa: TPL-016` comment.
"""
if not self._TPL_016_PERSONA_RE.search(str(file_path).replace("\\", "/")):
return
if len(lines) <= self._TPL_016_THRESHOLD:
return
if any(
"noqa: tpl-016" in line.lower() or "noqa: tpl016" in line.lower()
for line in lines
):
return
if self._TPL_016_SHARED_INCLUDE_RE.search(content):
return
self._add_violation(
rule_id="TPL-016",
rule_name="Persona templates >75 LOC must include a shared/ partial",
severity=Severity.WARNING,
file_path=file_path,
line_number=1,
message=(
f"Persona template is {len(lines)} LOC and doesn't include a shared/ partial — "
"likely duplicates body content already in a sibling persona"
),
context=file_path.name,
suggestion=(
"Extract the body into app/modules/<m>/templates/<m>/shared/<feature>-(list|form|view).html "
"and {% include %} it from this wrapper. Or add {# noqa: TPL-016 #} if intentionally standalone "
"(see docs/architecture/persona-template-consolidation.md for the heuristic)."
),
)
def _validate_api_endpoints(self, target_path: Path):
"""Validate API endpoint rules (API-001, API-002, API-003, API-004)"""
print("📡 Validating API endpoints...")
@@ -3551,6 +3604,10 @@ class ArchitectureValidator:
if not is_base_or_partial and not is_macro:
self._check_static_v_usage(file_path, content, lines)
# TPL-016: Persona templates >75 LOC should include a shared/ partial
if not is_base_or_partial and not is_macro:
self._check_persona_template_shared_include(file_path, content, lines)
# TPL-008: Check for call table_header() pattern
self._check_table_header_call_pattern(file_path, content, lines)