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:
|
exceptions:
|
||||||
- "shared/macros/headers.html"
|
- "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"
|
- id: "TPL-014"
|
||||||
name: "Use new modal_simple macro API with call block"
|
name: "Use new modal_simple macro API with call block"
|
||||||
severity: "error"
|
severity: "error"
|
||||||
|
|||||||
@@ -871,6 +871,10 @@ class ArchitectureValidator:
|
|||||||
if not is_macro:
|
if not is_macro:
|
||||||
self._check_static_v_usage(file_path, content, lines)
|
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
|
# TPL-004: Check x-text usage for dynamic content
|
||||||
self._check_xtext_usage(file_path, content, lines)
|
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>",
|
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):
|
def _validate_api_endpoints(self, target_path: Path):
|
||||||
"""Validate API endpoint rules (API-001, API-002, API-003, API-004)"""
|
"""Validate API endpoint rules (API-001, API-002, API-003, API-004)"""
|
||||||
print("📡 Validating API endpoints...")
|
print("📡 Validating API endpoints...")
|
||||||
@@ -3551,6 +3604,10 @@ class ArchitectureValidator:
|
|||||||
if not is_base_or_partial and not is_macro:
|
if not is_base_or_partial and not is_macro:
|
||||||
self._check_static_v_usage(file_path, content, lines)
|
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
|
# TPL-008: Check for call table_header() pattern
|
||||||
self._check_table_header_call_pattern(file_path, content, lines)
|
self._check_table_header_call_pattern(file_path, content, lines)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user