feat(arch-rules): TPL-016 flags large persona templates that skip shared/
Some checks failed
Some checks failed
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:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user