Skip to content

Detect props a component's own twig reads that its <name>.yaml does not declare #26

Description

@parisek

Sibling to #14, not a duplicate: that issue covers props a calling template passes that the child does not declare. This is the mirror — props a component's own twig body reads that its <name>.yaml does not declare.

The shape

graph-budget.twig:137-139 (mairateam) honours item.hidden — an item that feeds the chart's curve but draws no dot, label or year — and the component's own styleguide fixture uses it. graph-budget.yaml declares only year, label, value, highlight.

The consequence is specific to the MCP path: an agent reading get-block-catalog learns the definition, the definition is silent, and a write carrying the undeclared prop is refused by validation. The designed behaviour is unreachable — not degraded, unreachable.

How common, measured

Surveyed ~200 components across five real projects (mairateam 49, eprukaz2025 41, keypers 40, proficio(-de) 34, proficiohub 10, plus fellows' apartment family).

Three genuine instances:

project component prop evidence
mairateam graph-budget item.hidden graph-budget.twig:137-139, no sidecar — portadesign/mairateam#56
mairateam certificate-list content.spacing certificate-list.twig:16-18, no .php sidecar, absent from yaml
fellows apartment-list-type item.status apartment-list-type.twig:132, no sidecar

So this is rare, not systemic. Rare enough that a human catches it only by accident — #56 surfaced while writing MCP guidance annotations, which forces a side-by-side read of twig and yaml.

Why a naive linter would be worse than none

The survey's false positives are the design constraint. Five distinct legitimate reasons a prop is read but undeclared:

  1. Framework injectionwrapper_id / wrapper_classes, set by BlockRenderer.php:308-309 on every component. Present in essentially every twig, absent from every yaml, correctly.
  2. .php sidecar assemblyarticle-featured.php:37-51, career-list.php:1-27, reference-list.php:1-25 build their items from a WP_Query.
  3. Global optionscontact-details.php:1-11 pulls socials via Helpers::formatFields('option').
  4. Render flags from the caller — eprukaz's card-list / divider read content.inner, set by {% include … with {inner: true} %}. divider.yaml deliberately declares fields: {}.
  5. ACF return shapescards-list.item.attributes is part of the link type's own {url, title, attributes} structure, not a separate subfield.

A check that flags all of these is noise, and a noisy check gets disabled — the same reasoning #14 already applies to its wrapper_id example.

What would make it viable

The suppression is the feature, not the check:

  1. A Twig-aware extractor collecting content.X and item.X reads per component. This is the hard part and the reason to be honest about cost — definition-kit is pure PHP with no Twig parser today (src/Lint/DriftLinter.php only diffs a definition against its own generated projections).
  2. Suppression driven by the role: taxonomy from Implement role: + source: — provenance and ACF projection as two axes, not one #13query, computed, global fields are declared-as-not-authored and must never be flagged.
  3. A seeded list for framework injections (wrapper_id, wrapper_classes) plus anything the component's own .php sidecar assigns onto $content.
  4. Report read-but-undeclared only. The inverse (declared-but-never-read) needs body analysis across loops, conditionals and {% embed %} — the same half source: parent — declare the props a component takes from its call sites #14 defers, for the same reason.

Recommendation

Not urgent. Three instances in two hundred components does not justify building a Twig parser on its own; it justifies filing this next to #14 so that if a Twig-aware extractor is ever built for that issue, this direction rides along — the extractor is the expensive part and both checks consume it.

Until then this stays a human-review item. Worth noting the one thing that reliably surfaces it: writing MCP guidance for a component forces reading its twig and its definition side by side, which is exactly how #56 was found.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions