Automated structural awareness for Drupal projects — parses declarative configs to map services, routes, hooks, plugins, and entities.
Project-Agnostic: Works with any Drupal project.
Why Use This
Fills the gap between semantic docs and raw codebase (Glob/Grep):
Layer 3: Semantic docs (@semantic-architect generated, business "why")
Layer 2: Structural index (script-generated, Drupal-aware "what" + "how connected")
Layer 1: Raw codebase (Glob/Grep/Read)
Layer 3 is generated by the @semantic-architect agent via /drupal-semantic. It reads Layer 2 + source code to produce business index, tech specs, and business schemas.
Without structural index:
- Manually grep for service dependencies
- Miss cross-module hook interactions
- No blast radius awareness for changes
With structural index:
- Instant service dependency graph
- Hook/event chain visibility
- Feature impact analysis via
discover deps:FEATURE
Generate Index
$SKILL_DIR/scripts/generate-all.sh [project-dir]
Generates:
docs/semantic/structural/services.md— Service IDs, classes, injected dependenciesdocs/semantic/structural/routes.md— Route names, paths, controllers, accessdocs/semantic/structural/hooks.md— Hook implementations (procedural + OOP)docs/semantic/structural/plugins.md— Block, FieldType, Action, QueueWorker pluginsdocs/semantic/structural/entities.md— Content and config entity types (with Fields column)docs/semantic/structural/schemas.md— Entity schema summary (fields, references, lists per bundle)docs/semantic/structural/base-fields.md— Base field registry (from PHPbaseFieldDefinitions())docs/semantic/structural/permissions.md— Permission registry (from*.permissions.yml)docs/semantic/structural/methods.md— Method index (public methods in Service/Controller/Form)docs/semantic/schemas/*.json— Per-bundle JSON schemas with field types, labels, cardinality, target types/bundles, allowed valuesdocs/semantic/schemas/*.base-fields.json— Per-entity base field schemas with entity metadata, field types, settingsdocs/semantic/DEPENDENCY_GRAPH.md— Cross-reference dependency analysisdocs/semantic/FEATURE_MAP.md— Feature overview with structural counts
Check Staleness
$SKILL_DIR/scripts/check-staleness.sh [project-dir]
Reports which structural files need regeneration based on file modification times.
Query via Discover
Once generated, query structural data through the discover skill:
discover service:entity_type.manager # Find a service
discover svc:config.factory # Shorthand
discover route:/admin/config # Find routes by path
discover path:/node # Shorthand
discover hook:node_presave # Find hook implementations
discover plugin:Block # Find block plugins
discover entity:node # Find entity types
discover schema:node # Show field schemas for node bundles
discover perm:administer # Find permissions
discover method:Timesheet # Find methods related to timesheets
discover deps:AUTH # Blast radius / dependency analysis
discover impact:my_module # What depends on this module
Output Formats
services.md
| Service ID | Class | Dependencies | Module | Tags |
|------------|-------|--------------|--------|------|
| my_module.service | Drupal\my_module\Service | @entity_type.manager, @config.factory | my_module | event_subscriber |
routes.md
| Route Name | Path | Controller/Form | Access | Module |
|------------|------|-----------------|--------|--------|
| my_module.settings | /admin/config/my-module | MyController::settings | administer site | my_module |
hooks.md
| Hook Name | Implementation | Type | File | Module |
|-----------|---------------|------|------|--------|
| node_presave | my_module_node_presave | procedural | my_module.module | my_module |
| form_alter | MyHooks::formAlter | oop | src/Hook/MyHooks.php | my_module |
plugins.md
| Plugin Type | ID | Class | Module | File |
|-------------|----|-------|--------|------|
| Block | my_custom_block | MyCustomBlock | my_module | src/Plugin/Block/MyCustomBlock.php |
entities.md
| Entity Type | ID | Class | Handlers | Fields | Module | File |
|-------------|----|-------|----------|--------|--------|------|
| ContentEntityType | node | Node | list_builder, form, views_data | 12 (5 ref, 2 list) | node | core/modules/node/src/Entity/Node.php |
schemas.md
| Entity Type | Bundle | Fields | References | Lists | Schema File |
|-------------|--------|--------|------------|-------|-------------|
| node | article | 5 | 2 | 1 | schemas/node.article.json |
base-fields.md
| Entity Type | Kind | Base Fields | References | Lists | Computed | Schema File |
|-------------|------|-------------|------------|-------|----------|-------------|
| timan_assignment | Content | 19 | 3 | 3 | 1 | schemas/timan_assignment.base-fields.json |
permissions.md
| Permission | Title | Module | Restricted | Description |
|------------|-------|--------|------------|-------------|
| administer timan holidays | Administer holidays | timan_holiday | yes | Create, edit, and delete public holidays. |
methods.md (3 sections: Services, Controllers, Forms)
| Service Class | Method | Return Type | Module | File | Line |
|---------------|--------|-------------|--------|------|------|
| HolidayService | getHolidaysInRange() | array | timan_holiday | www/.../HolidayService.php | 45 |
schemas/*.base-fields.json (loaded on demand via discover schema:ENTITY)
{
"entity_type": "timan_assignment",
"entity_kind": "Content",
"base_table": "timan_assignment",
"entity_keys": {"id": "id", "uuid": "uuid", "label": "label", "owner": "uid"},
"admin_permission": "administer timan assignments",
"has_owner": true,
"base_fields": {
"label": {"type": "string", "label": "Label", "required": true, "cardinality": 1, "settings": {"max_length": 255}}
},
"field_count": 19,
"ref_count": 3,
"list_count": 3,
"computed_count": 1
}
schemas/*.json (loaded on demand via discover schema:ENTITY)
{
"entity_type": "node",
"bundle": "article",
"fields": {
"field_tags": {
"type": "entity_reference",
"label": "Tags",
"required": false,
"cardinality": -1,
"target_type": "taxonomy_term",
"target_bundles": ["tags"]
}
},
"field_count": 5,
"ref_count": 2,
"list_count": 1
}
Known limitations (config schemas):
- Only configurable fields from YAML config are extracted. Base fields are in separate
*.base-fields.jsonfiles. - Config directory auto-detected from common locations (
config/sync,config/default,config/staging,../config/sync). Projects with non-standard paths need to symlink or adjust.
Known limitations (base fields):
- Fields from
parent::baseFieldDefinitions()(id,uuid,langcode) are implicit — not enumerated. - Config entities without
baseFieldDefinitions()are skipped. - Dynamic permissions from
permission_callbacksare not detected.
FEATURE_MAP.md
Compact overview (~500 tokens) always loaded during session priming:
| Code | Name | Services | Hooks | Routes | Plugins | Entities | Hotspots | Spec |
DEPENDENCY_GRAPH.md
Sections:
- Service Dependencies (who injects who)
- Event/Hook Chains
- Entity Cross-References
- External Boundaries (HTTP, SMTP, queues)
- Feature Adjacency Matrix
When This Triggers
- "Generate structural index"
- "Rebuild dependency graph"
- "What services does X use"
- "Blast radius of changing Y"
- "Show me the service graph"
- "What hooks does module X implement"
- "Map dependencies for feature X"
Prerequisites
- Drupal project with standard directory structure
docs/semantic/directory (for output location)- No external dependencies required (grep/sed/awk only)
- Optional:
yqfor improved YAML parsing accuracy
Integration
- discover skill: Structural query prefixes route to structural index
- prime.sh: FEATURE_MAP.md loaded as primary context when available
- staleness hook: Advisory warnings when structural sources change
- agents: Builder checks deps before multi-file changes; Reviewer validates impact