Skip to content

Commit 76f2dcc

Browse files
lbliiiclaude
andauthored
feat: agent-UX — point K-PAR-001 end-tag errors at kida check (#112)
Agents doing bulk template migrations in downstream repos (e.g. IA rewrites across dozens of `{% call %}`/`{% if %}` nests) hit parse errors one rendered route at a time because no error message told them `kida check <dir> --strict --validate-calls` batch-validates the whole directory. The CLI already existed; the agent-UX gap was that nothing surfaced it. Adds `BULK_CHECK_TIP` in `kida.parser.errors`, appended to the five K-PAR-001 end-tag mismatch paths: orphan `{% end %}`, mismatched closing tags, and typed-end mismatches. AGENTS.md gains a "downstream bulk-edit agents" bullet under "Who reads your output" so future error-message work treats them as a first-class reader. No test assertions rely on the exact suggestion strings; 4116 tests pass, ruff/ty clean. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent b12c486 commit 76f2dcc

6 files changed

Lines changed: 36 additions & 7 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Kida is shipped (Bengal and Chirp depend on it) and pre-1.0. Calibrate according
4242

4343
- **Framework builders** — Bengal, Chirp. They read `template_metadata()`, `block_metadata()`, tracebacks with line/col, and your error messages when their users misuse a tag.
4444
- **SSG authors** — read `kida check` / `kida format` output. If your error doesn't include the template path and a fix, it's wrong.
45+
- **Agents doing bulk template edits in downstream repos** — the whack-a-mole case. An agent migrating an IA across dozens of `{% call %}`/`{% if %}` nests finds parse errors one rendered route at a time unless we tell them about `kida check <dir> --strict --validate-calls`. Parser errors on unmatched `{% end %}` should surface that tip; agent-facing docs should lead with it.
4546
- **Migrators from Jinja2** — want to be done in five minutes. The `set` vs `let` trap is the #1 thing they hit. Error messages should *catch them*, not let them debug it themselves.
4647
- **Contributors** — know templating, not our internals. They read parser/compiler files; mixin patterns are surprising.
4748
- **Me (Lawrence)** — read diffs. Put the *what* in code, the *why* in the PR.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- **`kida check --lint-fragile-paths`** — Opt-in lint rule that flags cross-template references whose target lives in the same folder as the caller (e.g. `{% include "pages/card.html" %}` from `pages/about.html`) and suggests the refactor-safe `./card.html` form. The rule covers `{% include %}`, `{% extends %}`, `{% embed %}`, `{% import %}`, and `{% from ... import %}`; references that already use `./`, `../`, or `@alias/` are ignored, and only exact same-folder matches are flagged to keep false-positives low. Default `kida check` behavior is unchanged — the rule only runs when the flag is passed.
1414
- **Refactor-safe templates tutorial** — New `site/content/docs/tutorials/refactor-safe-templates.md` walks through the folder-move problem, when to use `./` / `../` vs `@alias/`, and a step-by-step migration recipe using `ripgrep`. Cross-linked from the tutorials index and `docs/syntax/includes.md`.
1515
- **Namespace aliases for template paths**`Environment(template_aliases={...})` maps short `@name/` prefixes to root-relative subtrees so cross-cutting component libraries stop being path-coupled. With `template_aliases={"components": "ui/components", "layouts": "ui/layouts"}`, templates can write `{% include "@components/card.html" %}` or `{% extends "@layouts/base.html" %}` regardless of where the caller lives — aliases resolve before loader lookup, so the same prefix works in every cross-template statement (`{% include %}`, `{% extends %}`, `{% embed %}`, `{% from ... import ... %}`). Aliases and `./`/`../` relative paths are orthogonal modes; aliases always resolve to an absolute root, so there is no composition of the two. Unknown aliases raise `TemplateNotFoundError` with the list of configured aliases.
16+
- **K-PAR-001 end-tag errors now point at `kida check`** — Orphan `{% end %}`, mismatched closing tags (e.g. `{% endfor %}` against an open `{% if %}`), and typed-end mismatches (e.g. `{% endblock %}` inside a `{% def %}`) now append a tip telling the reader to run `kida check <templates-dir> --strict` to surface every mismatch across the directory at once. Agents and humans doing bulk template migrations were discovering these one rendered route at a time; the CLI already batch-validates, but nothing in the error message said so. New `BULK_CHECK_TIP` constant in `kida.parser.errors`, wired into the five end-tag mismatch paths across `parser/statements.py`, `parser/core.py`, and `parser/blocks/core.py`. `AGENTS.md` gains an "Agents doing bulk template edits in downstream repos" bullet under "Who reads your output" to make the audience explicit for future error-message work.
1617

1718
## [0.8.0] - 2026-04-21
1819

src/kida/parser/blocks/core.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,21 +126,27 @@ def _pop_block(self, expected: str | None = None) -> str:
126126
ParseError: If no blocks are open or if expected doesn't match.
127127
"""
128128
if not self._block_stack:
129+
from kida.parser.errors import BULK_CHECK_TIP
130+
129131
raise self._error(
130132
"Unexpected closing tag - no open block to close",
131-
suggestion="Remove this tag or add a matching opening tag",
133+
suggestion=f"Remove this tag or add a matching opening tag.\n\n{BULK_CHECK_TIP}",
132134
)
133135

134136
popped: tuple[str, int, int] = self._block_stack.pop()
135137
block_type, lineno, _col = popped
136138

137139
# If a specific block type is expected, validate it
138140
if expected and block_type != expected:
141+
from kida.parser.errors import BULK_CHECK_TIP
142+
139143
raise self._error(
140144
f"Mismatched closing tag: expected {{% end{expected} %}}, "
141145
f"but found closing tag for '{block_type}' block opened at line {lineno}",
142-
suggestion=f"Use {{% end %}} to close the innermost block, "
143-
f"or {{% end{block_type} %}} to be explicit",
146+
suggestion=(
147+
f"Use {{% end %}} to close the innermost block, "
148+
f"or {{% end{block_type} %}} to be explicit.\n\n{BULK_CHECK_TIP}"
149+
),
144150
)
145151

146152
return block_type
@@ -205,9 +211,11 @@ def _consume_end_tag(self, block_type: str) -> None:
205211
self._advance() # consume 'endXXX'
206212
self._pop_block(block_type) # Pop with type validation
207213
else:
214+
from kida.parser.errors import BULK_CHECK_TIP
215+
208216
raise self._error(
209217
f"Expected 'end' or 'end{block_type}', got '{keyword}'",
210-
suggestion="Use {% end %} to close the innermost block",
218+
suggestion=f"Use {{% end %}} to close the innermost block.\n\n{BULK_CHECK_TIP}",
211219
)
212220

213221
self._expect(TokenType.BLOCK_END)

src/kida/parser/core.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,11 @@ def parse(self) -> Template:
161161
if self._current.type == TokenType.BLOCK_BEGIN:
162162
next_tok = self._peek(1)
163163
if next_tok.type == TokenType.NAME and next_tok.value in self._end_keywords:
164+
from kida.parser.errors import BULK_CHECK_TIP
165+
164166
raise self._error(
165167
f"Unexpected '{{% {next_tok.value} %}}' - no open block to close",
166-
suggestion="Remove this tag or add a matching opening tag",
168+
suggestion=f"Remove this tag or add a matching opening tag.\n\n{BULK_CHECK_TIP}",
167169
)
168170
# Also check for orphan continuation keywords
169171
if next_tok.type == TokenType.NAME and next_tok.value in self._CONTINUATION_KEYWORDS:

src/kida/parser/errors.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@
2323
}
2424

2525

26+
# Appended to suggestions for bulk end-tag mismatches (extra, mismatched, or
27+
# orphan end tags). Points agents doing multi-file migrations at `kida check`
28+
# so they stop discovering errors one rendered route at a time.
29+
BULK_CHECK_TIP: str = (
30+
"Tip: run `kida check <templates-dir> --strict` to surface every "
31+
"end-tag mismatch across the directory in one pass, instead of "
32+
"discovering them one rendered page at a time."
33+
)
34+
35+
2636
class ParseError(TemplateSyntaxError):
2737
"""Parser error with rich source context.
2838

src/kida/parser/statements.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,11 @@ def _handle_end_keyword(self, keyword: str) -> None:
323323
"""
324324
# End tag without matching opening block
325325
if not self._block_stack:
326+
from kida.parser.errors import BULK_CHECK_TIP
327+
326328
raise self._error(
327329
f"Unexpected '{keyword}' - no open block to close",
328-
suggestion="Remove this tag or add a matching opening tag",
330+
suggestion=f"Remove this tag or add a matching opening tag.\n\n{BULK_CHECK_TIP}",
329331
)
330332

331333
# Check if end tag matches the innermost block
@@ -340,9 +342,14 @@ def _handle_end_keyword(self, keyword: str) -> None:
340342
return None
341343
else:
342344
# Mismatched end tag
345+
from kida.parser.errors import BULK_CHECK_TIP
346+
343347
raise self._error(
344348
f"Mismatched closing tag: expected '{{% {expected_end} %}}' or '{{% end %}}', got '{{% {keyword} %}}'",
345-
suggestion=f"The innermost open block is '{innermost_block}' (opened at line {self._block_stack[-1][1]})",
349+
suggestion=(
350+
f"The innermost open block is '{innermost_block}' "
351+
f"(opened at line {self._block_stack[-1][1]}).\n\n{BULK_CHECK_TIP}"
352+
),
346353
)
347354

348355
def _skip_comment(self) -> None:

0 commit comments

Comments
 (0)