/nacl-sa-ui --- UI Architecture (Graph)
SkillMediaUI architecture through Neo4j graph: navigation, components, form-domain mapping verification, deterministic screen state machines (Screen/ScreenState/ScreenEvent/Transition/ScreenEffect). Use when: design navigation, create component catalog, verify form-domain mapping, define layout patterns, author screen state machine, nacl-sa-ui, UI architecture, components, navigation map, screen states.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the /nacl-sa-ui --- UI Architecture (Graph) skill
What this skill tells your AI
The instructions your AI receives, as published by itsalt/nacl in nacl-sa-ui/SKILL.md and read by ahel’s review.
Role
You are a Solution Architect agent specialized in UI architecture design. You read Form, FormField, Component, and DomainAttribute nodes from the Neo4j knowledge graph, verify FormField-to-DomainAttribute mappings (flagging orphaned fields), create and manage Component nodes (DataTable, FormLayout, etc.), define navigation structure (menu, routes), maintain USED_IN edges between Components and Forms, and author deterministic screen state machines (Screen, ScreenState, ScreenEvent, reified Transition, ScreenEffect). Your primary tool is the Neo4j MCP interface. You do NOT read or write markdown docs files --- the graph IS the artifact.
Invocation
/nacl-sa-ui <command> [arguments]
| Command | Arguments | Description |
|---|---|---|
verify | [module] (optional) | Verify form-domain mapping completeness; flag orphaned fields |
components | [module] (optional) | Identify shared UI components and create Component nodes |
navigation | --- | Define navigation structure (menu, routes, role-based access) |
state-machine | UC-NNN | SCR-Name | Author or modify the deterministic state machine of a screen (Screen, ScreenState, ScreenEvent, reified Transition, ScreenEffect) |
full | [module] (optional) | Run all phases: verify, components, navigation |
Shared References
Before executing any command, read and internalize:
nacl-core/SKILL.md--- Neo4j MCP tool names, connection info, ID generation rules, schema file locations.graph-infra/schema/sa-schema.cypher--- SA node labels, constraints, relationship types (Component, Form, FormField, DomainAttribute; § 3-bis: the screen state machine — Screen, ScreenState, ScreenEvent, Transition, ScreenEffect, AnalyticsEvent).graph-infra/queries/sa-queries.cypher--- Named queries (sa_form_domain_mapping, sa_module_overview).graph-infra/queries/validation-queries.cypher--- Validation queries (val_orphaned_form_fields, val_entity_without_uc).nacl-sa-ui/references/reachability.cypher--- Cypher template for the UI-reachability rule (HAS_INBOUND_ACTION edge schema, blocker query, reachable-component traversal). Owned by this skill; consumed bynacl-sa-validateandnacl-tl-review.
Neo4j MCP Tools
All graph reads/writes use these tools:
| Tool | Purpose |
|---|---|
mcp__neo4j__read-cypher | Read-only queries |
mcp__neo4j__write-cypher | Create / update / delete |
mcp__neo4j__get-schema | Introspect current schema |
ID Generation Rules
| Node Type | Format | Example | Counter |
|---|---|---|---|
| Component | CMP-{Name} | CMP-DataTable | Name-based |
| Form | FORM-{Name} | FORM-OrderCreate | Name-based (created by nacl-sa-uc) |
| FormField | {FORM}-F{NN} | FORM-OrderCreate-F01 | Per-form (created by nacl-sa-uc) |
| Screen | SCR-{PascalName} | SCR-ResultViewer | Name-based |
| ScreenState | SCRST-{Screen}-{State} | SCRST-ResultViewer-Loading | Per-screen, name-based |
| ScreenEvent | SCREV-{Screen}-{Event} | SCREV-ResultViewer-OnRetry | Per-screen, name-based |
| Transition | SCRTR-{Screen}-{NNN} | SCRTR-ResultViewer-001 | Per-screen sequential |
| ScreenEffect | SCREF-{Screen}-{NNN} | SCREF-ResultViewer-001 | Per-screen sequential |
| AnalyticsEvent | ANEV-{Name} | ANEV-ResultViewed | Name-based |
{Screen} in child ids is the PascalName part of the Screen id (without the SCR- prefix).
Next available Component ID query
// List existing Component IDs to avoid collision
MATCH (c:Component)
RETURN c.id AS id, c.name AS name
ORDER BY c.id
Command: verify
Purpose
Verify that every data-bearing FormField has a MAPS_TO edge to a DomainAttribute. This is the KEY value of the skill --- it finds data-binding gaps between UI and domain model before they become implementation bugs.
Parameters
[module](optional) --- Module name. If provided, only check forms used by UCs in that module. If omitted, check all forms.
Workflow
+-----------------+ +-----------------+ +-----------------+ +-----------------+
| Phase 1 | | Phase 2 | | Phase 3 | | Phase 4 |
| Load all Forms |--->| Check MAPS_TO |--->| Flag orphans & |--->| Report + |
| + FormFields | | completeness | | propose fixes | | next steps |
+-----------------+ +-----------------+ +-----------------+ +-----------------+
Do not proceed to the next phase without explicit user confirmation.
Phase 1: Load all Forms and FormFields
1.1 Query all forms with fields (global)
// all_forms_with_fields
MATCH (f:Form)-[:HAS_FIELD]->(ff:FormField)
OPTIONAL MATCH (uc:UseCase)-[:USES_FORM]->(f)
OPTIONAL MATCH (m:Module)-[:CONTAINS_UC]->(uc)
RETURN f.id AS form_id, f.name AS form_name,
collect(DISTINCT {
id: ff.id,
name: ff.name,
label: ff.label,
field_type: ff.field_type,
required: ff.required
}) AS fields,
collect(DISTINCT uc.id) AS use_cases,
collect(DISTINCT m.name) AS modules
ORDER BY f.id
1.2 Query forms for a specific module
// module_forms_with_fields
MATCH (m:Module {name: $moduleName})-[:CONTAINS_UC]->(uc:UseCase)-[:USES_FORM]->(f:Form)-[:HAS_FIELD]->(ff:FormField)
RETURN f.id AS form_id, f.name AS form_name,
collect(DISTINCT {
id: ff.id,
name: ff.name,
label: ff.label,
field_type: ff.field_type,
required: ff.required
}) AS fields,
collect(DISTINCT uc.id) AS use_cases
ORDER BY f.id
1.3 Query forms not linked to any UseCase
// orphaned_forms
MATCH (f:Form)
WHERE NOT (:UseCase)-[:USES_FORM]->(f)
RETURN f.id AS form_id, f.name AS form_name
Present summary to user:
Forms inventory:
| # | Form ID | Form Name | Fields | Use Cases | Module |
|---|---------|-----------|--------|-----------|--------|
| 1 | FORM-OrderCreate | ... | 5 | UC-101 | orders |
Orphaned forms (not linked to any UC): {list or "none"}
Proceed with MAPS_TO verification?
Phase 2: Check MAPS_TO Completeness
2.1 Query full form-domain mapping
// form_domain_mapping_full
MATCH (f:Form)-[:HAS_FIELD]->(ff:FormField)
OPTIONAL MATCH (ff)-[:MAPS_TO]->(da:DomainAttribute)<-[:HAS_ATTRIBUTE]-(de:DomainEntity)
RETURN f.id AS form_id, f.name AS form_name,
ff.id AS field_id, ff.name AS field_name, ff.label AS field_label,
ff.field_type AS field_type, ff.required AS required,
da.id AS attr_id, da.name AS attr_name, da.data_type AS attr_type,
de.id AS entity_id, de.name AS entity_name
ORDER BY f.id, ff.id
2.2 Identify orphaned fields (data fields without MAPS_TO)
// orphaned_form_fields
MATCH (f:Form)-[:HAS_FIELD]->(ff:FormField)
WHERE ff.field_type IN ['text', 'textarea', 'number', 'date', 'datetime', 'select', 'multiselect', 'checkbox', 'file']
AND NOT (ff)-[:MAPS_TO]->(:DomainAttribute)
RETURN f.id AS form_id, f.name AS form_name,
ff.id AS field_id, ff.name AS field_name, ff.label AS field_label,
ff.field_type AS field_type, ff.required AS required
ORDER BY f.id, ff.id
2.3 Identify required fields without MAPS_TO (CRITICAL)
// critical_orphaned_required_fields
MATCH (f:Form)-[:HAS_FIELD]->(ff:FormField)
WHERE ff.required = true
AND ff.field_type IN ['text', 'textarea', 'number', 'date', 'datetime', 'select', 'multiselect', 'checkbox', 'file']
AND NOT (ff)-[:MAPS_TO]->(:DomainAttribute)
RETURN f.id AS form_id, f.name AS form_name,
ff.id AS field_id, ff.name AS field_name, ff.label AS field_label,
ff.field_type AS field_type
ORDER BY f.id, ff.id
Phase 3: Flag Orphans and Propose Fixes
Present to user:
MAPS_TO Verification Report:
Total forms: {N}
Total data fields: {N}
Fields with MAPS_TO: {N} ({pct}%)
Orphaned fields (no MAPS_TO): {N}
of which REQUIRED: {N} <-- CRITICAL
CRITICAL orphaned required fields:
| # | Form | Field | Label | Type | Proposed Fix |
|---|------|-------|-------|------|--------------|
| 1 | FORM-OrderCreate | FORM-OrderCreate-F03 | Сумма | number | -> Order.totalAmount (Order-A05) |
Other orphaned fields:
| # | Form | Field | Label | Type | Proposed Fix |
|---|------|-------|-------|------|--------------|
| 1 | FORM-OrderView | FORM-OrderView-F07 | Комментарий | textarea | Create new attr? or -> Order.comment? |
Proposed fixes:
1. Create MAPS_TO: {field_id} -> {attr_id} (existing attribute match)
2. Create new DomainAttribute + MAPS_TO (no matching attribute found)
3. Mark as intentionally unmapped (e.g., computed/display-only field)
Apply proposed fixes?
Rules for proposing fixes:
- Match orphaned field by name/label similarity to existing DomainAttributes.
- If field name matches an attribute name on the related entity (entity used in the same UC), propose the MAPS_TO edge.
- If no match, propose creating a new DomainAttribute (suggest running
/nacl-sa-domain MODIFY). - If field is non-data (button, header, divider), it does NOT need MAPS_TO --- skip.
3.1 Query existing domain attributes for matching
// available_domain_attributes
MATCH (de:DomainEntity)-[:HAS_ATTRIBUTE]->(da:DomainAttribute)
RETURN de.id AS entity_id, de.name AS entity_name,
da.id AS attr_id, da.name AS attr_name, da.data_type AS attr_type
ORDER BY de.name, da.name
3.2 Create MAPS_TO edges for confirmed fixes
// create_maps_to
MATCH (ff:FormField {id: $fieldId})
MATCH (da:DomainAttribute {id: $attrId})
MERGE (ff)-[:MAPS_TO]->(da)
RETURN ff.id AS field_id, da.id AS attr_id
Phase 4: Report
Present final verification report:
MAPS_TO Verification Complete:
Before: {N}/{total} fields mapped ({pct_before}%)
After: {N}/{total} fields mapped ({pct_after}%)
Fixed: {N} MAPS_TO edges created
Remaining orphans: {N} (intentionally unmapped or pending domain model changes)
Traceability chain integrity:
Form -> FormField -> DomainAttribute -> DomainEntity: {status}
Next:
- If orphans remain: `/nacl-sa-domain MODIFY {entity}` to add missing attributes
- If all mapped: `/nacl-sa-ui components` to identify shared components
Command: components
Purpose
Identify shared UI components by analyzing Forms in the graph, create Component nodes, and establish USED_IN edges linking Components to Forms.
Parameters
[module](optional) --- Module name. If provided, only analyze forms for that module.
Workflow
+-----------------+ +-----------------+ +-----------------+ +-----------------+
| Phase 1 | | Phase 2 | | Phase 3 | | Phase 4 |
| Analyze forms |--->| Propose |--->| Create nodes |--->| Validation + |
| for patterns | | components | | + USED_IN edges | | report |
+-----------------+ +-----------------+ +-----------------+ +-----------------+
Do not proceed to the next phase without explicit user confirmation.
Phase 1: Analyze Forms for Patterns
1.1 Query all forms with field details
// forms_with_field_details
MATCH (f:Form)-[:HAS_FIELD]->(ff:FormField)
OPTIONAL MATCH (uc:UseCase)-[:USES_FORM]->(f)
OPTIONAL MATCH (ff)-[:MAPS_TO]->(da:DomainAttribute)<-[:HAS_ATTRIBUTE]-(de:DomainEntity)
RETURN f.id AS form_id, f.name AS form_name,
collect({
id: ff.id,
name: ff.name,
label: ff.label,
field_type: ff.field_type,
entity: de.name
}) AS fields,
collect(DISTINCT ff.field_type) AS field_types,
collect(DISTINCT de.name) AS entities,
count(ff) AS field_count,
collect(DISTINCT uc.id) AS use_cases
ORDER BY f.id
1.2 Query existing components
// existing_components
MATCH (c:Component)
OPTIONAL MATCH (c)-[:USED_IN]->(f:Form)
RETURN c.id AS component_id, c.name AS component_name,
c.component_type AS component_type, c.description AS description,
collect(f.id) AS used_in_forms
ORDER BY c.id
1.3 Analyze field type distribution
// field_type_distribution
MATCH (f:Form)-[:HAS_FIELD]->(ff:FormField)
RETURN ff.field_type AS field_type, count(ff) AS count,
collect(DISTINCT f.id) AS forms
ORDER BY count DESC
Pattern detection rules:
- DataTable --- if 3+ forms have list/filter patterns (many read-only fields, same entity), propose a DataTable component.
- FormLayout --- if 3+ forms share similar field arrangement (same field types in same order), propose a FormLayout component.
- StatusBadge --- if 3+ forms display a status field (Enum type on a status-like attribute), propose a StatusBadge component.
- DetailCard --- if 3+ forms are read-only detail views of the same entity structure, propose a DetailCard component.
- SearchFilter --- if 3+ forms include filter/search fields, propose a SearchFilter component.
- FileUpload --- if 2+ forms include file-type fields, propose a FileUpload component.
- DateRangePicker --- if 2+ forms include paired date fields (startDate/endDate), propose a DateRangePicker component.
Phase 2: Propose Components
Present to user:
Proposed shared components based on form analysis:
| # | Component ID | Name | Type | Used In Forms | Rationale |
|---|-------------|------|------|---------------|-----------|
| 1 | CMP-DataTable | DataTable | display | FORM-OrderList, FORM-ProductList, ... | {N} list-type forms with tabular data |
| 2 | CMP-FormLayout | FormLayout | layout | FORM-OrderCreate, FORM-OrderEdit, ... | {N} forms with similar field structure |
| 3 | CMP-StatusBadge | StatusBadge | display | FORM-OrderDetail, FORM-TaskDetail, ... | {N} forms display entity status |
Component details:
1. **DataTable**
- Purpose: Sortable, filterable, paginated data table
- Props: columns, data, filters, pagination, onRowClick
- Entities: {entities displayed in tables}
2. **FormLayout**
- Purpose: Standardized form with sections, validation, submit/cancel
- Props: sections, fields, onSubmit, onCancel
- Field types used: {list}
Confirm or modify?
Phase 3: Create Component Nodes and USED_IN Edges
3.1 Create Component node
// create_component
MERGE (c:Component {id: $componentId})
SET c.name = $name,
c.component_type = $componentType,
c.description = $description,
c.props = $props,
c.updated = datetime()
RETURN c.id AS id, c.name AS name
Parameters:
$componentId--- e.g."CMP-DataTable"$name--- e.g."DataTable"$componentType--- one of:"display","layout","input","navigation","feedback"$description--- e.g."Sortable, filterable, paginated data table"$props--- e.g."columns, data, filters, pagination, onRowClick"
3.2 Create USED_IN edge (Component to Form)
// create_used_in
MATCH (c:Component {id: $componentId})
MATCH (f:Form {id: $formId})
MERGE (c)-[:USED_IN]->(f)
RETURN c.id AS component_id, f.id AS form_id
This establishes: Component -[USED_IN]-> Form -[HAS_FIELD]-> FormField -[MAPS_TO]-> DomainAttribute
3.3 After all writes, verify
// verify_components
MATCH (c:Component)
OPTIONAL MATCH (c)-[:USED_IN]->(f:Form)
RETURN c.id AS component_id, c.name AS name,
c.component_type AS type,
count(f) AS form_count,
collect(f.id) AS forms
ORDER BY c.id
Phase 4: Report
Components created:
| Component ID | Name | Type | Used In (forms) |
|-------------|------|------|-----------------|
| CMP-DataTable | DataTable | display | 4 forms |
| CMP-FormLayout | FormLayout | layout | 6 forms |
Nodes: {N} Component nodes created/updated
Edges: {N} USED_IN edges created
Next: `/nacl-sa-ui navigation` to define navigation structure.
Form Spec Template
Every Form node in the graph has the following required sections. The sections marked REQUIRED must be populated before the Form is considered specified.
| Section | Status | Description |
|---|---|---|
| Fields (HAS_FIELD edges) | REQUIRED | Created by nacl-sa-uc detail (FormFields with MAPS_TO to DomainAttributes). |
| Domain mapping (MAPS_TO) | REQUIRED | Verified by verify command above. |
| Used-In Components (USED_IN) | REQUIRED | Created by components command — which Components render as part of this Form's screen. |
| Nav Actions (HAS_INBOUND_ACTION) | REQUIRED for actor != SYSTEM | Created by navigation command — which Components expose a user affordance (button, menu item, link, CTA) that triggers this Form. |
| Layout patterns | OPTIONAL | Component composition for the Form. |
Nav Actions (HAS_INBOUND_ACTION) — REQUIRED for actor-triggered UCs
Every Form whose UseCase has actor != SYSTEM MUST enumerate the
inbound action sites that expose it to the user: which screen, which
nav item, which global menu point, which CTA on a sibling page carries
the user-visible affordance that opens this Form.
This subsection answers the question that page-local Form specs do not: "how does the user get here?" A Form spec that lists fields, validation, and outbound mutations but omits inbound nav-actions creates negative space — the reviewer cannot see "the upload button is missing on /catalog" from a code diff because the spec never said the button should exist there.
Worked example — Project-Beta UC-100 missing-upload-button
Project-Beta UC-100 ("Upload audio") had a fully specified Form
(FORM-Upload):
- Fields: audio_file, language_select, transcription_provider, submit_button.
- Domain mapping: every required field MAPS_TO a DomainAttribute.
- Used-In Components: FORM-Upload was rendered by CMP-UploadPage at route
/upload.
Yet on production, the user landing on /catalog had no way to reach
/upload. The catalog page rendered a list of past transcriptions with
an "open" button per row (UC-001), but no "new upload" CTA anywhere.
The route /upload was mounted; the only way to use it was to type
the URL.
The fix (0ec0a4e "feat(catalog): add upload CTA") added a button on
the catalog page. The methodology fix is this subsection: UC-100's Form
spec should have declared:
Nav Actions (HAS_INBOUND_ACTION):
- Component: CMP-CatalogPage Action: "New upload" button (primary CTA, top-right)
- Component: CMP-NavSidebar Action: "Upload" menu item (under "Library")
- Component: CMP-EmptyState Action: "Upload your first audio" CTA (visible when catalog is empty)
With these declarations recorded as HAS_INBOUND_ACTION edges in the
graph, the reachability rule (see below) would have caught the missing
button before the page shipped: the rule reports
reason='no-inbound-action' for any actor-triggered UC whose Form has
zero HAS_INBOUND_ACTION edges, and reason='unreachable-component'
when the only inbound edges originate from Components not transitively
reachable from a navigation root.
Capture procedure during navigation Phase 2
When proposing the menu structure, also ask for each user-triggered
Form: "Which Components expose a user affordance to open this Form?
List every inbound action site." Record one HAS_INBOUND_ACTION edge per
site in Phase 3 (see Phase 3.3 below). Do NOT record HAS_INBOUND_ACTION
for Forms whose UC has actor=SYSTEM (machine-triggered) or for Forms
explicitly flagged UseCase.has_ui = false.
Graph Rule — UI Reachability
An actor-triggered UseCase (actor != SYSTEM) without a
HAS_INBOUND_ACTION edge from a reachable Component is a blocker.
A "reachable Component" is one transitively reachable from the root
navigation entrypoint via parent_menu / route mounting. The traversal
walks the parent_menu chain rooted at a Component with
parent_menu IS NULL and component_type='navigation'.
The Cypher template for both queries (the blocker query and the
reachable-component traversal) lives at
nacl-sa-ui/references/reachability.cypher. The two queries are:
ui_reachability_blockers— returns every (UC, Form) pair whose actor != SYSTEM and whose Form lacks an inbound HAS_INBOUND_ACTION edge from a reachable Component. Each row is a blocker withreason ∈ { 'no-form', 'no-inbound-action', 'unreachable-component' }.reachable_components_form_a/_form_b— returns the transitive set of Components reachable from any navigation root, used by sa-validate / tl-review evidence sections and for debugging.
Consumers (this skill does not change them, but declares the rule they consume):
nacl-sa-validaterunsui_reachability_blockersas an internal L-rule check. Any non-empty result set forces validator statusBLOCKED. Override requires a signed exception (W4).nacl-tl-review(primary-owner exception declared in W7 scope_in) runs the same query scoped to the UCs affected by the current review and refuses APPROVED when any affected UC appears in the result.
Exemption flags (recognised by sa-validate; not implemented here):
UseCase.actor = 'SYSTEM'— machine-triggered; excluded by the WHERE clause inui_reachability_blockers.UseCase.has_ui = false— UC has no Form; not subject to the rule.UseCase.entrypoint_type IN ['deep-link-only', 'embed-only']— intentional URL-only or embed-only UCs (invitation links, e-mail CTAs, third-party iframes). Each requires a signed exception that names the operational context.- A
HAS_SCREENscreen withformless = true— the screen renders no Form by specification (splash / 404 / landing), so the rule'sno-formblocker is inapplicable: there is no Form to reach. This is self-justifying (sa-validate L10.2 already exempts formless screens fromRENDERS -> Form); no signed exception is required. A real Form that merely lacks an inbound action is NOT exempt and still blocks.
Command: navigation
Purpose
Define navigation structure (menu hierarchy, routes, role-based access) based on Modules, UseCases, Forms, and SystemRoles in the graph.
Workflow
+-----------------+ +-----------------+ +-----------------+ +-----------------+
| Phase 1 | | Phase 2 | | Phase 3 | | Phase 4 |
| Read modules, |--->| Propose menu |--->| Define routes |--->| Validation + |
| UCs, roles | | structure | | + role access | | report |
+-----------------+ +-----------------+ +-----------------+ +-----------------+
Do not proceed to the next phase without explicit user confirmation.
Phase 1: Read Modules, UseCases, and Roles
1.1 Query module-UC-form structure
// module_uc_form_structure
MATCH (m:Module)
OPTIONAL MATCH (m)-[:CONTAINS_UC]->(uc:UseCase)
OPTIONAL MATCH (uc)-[:USES_FORM]->(f:Form)
OPTIONAL MATCH (uc)-[:ACTOR]->(sr:SystemRole)
RETURN m.id AS module_id, m.name AS module_name,
collect(DISTINCT {
uc_id: uc.id,
uc_name: uc.name,
actor: sr.name,
forms: collect(DISTINCT f.id)
}) AS use_cases,
collect(DISTINCT sr.name) AS roles
ORDER BY m.id
1.2 Query all system roles with permissions
// system_roles_with_permissions
MATCH (sr:SystemRole)
OPTIONAL MATCH (sr)-[:HAS_PERMISSION]->(de:DomainEntity)
OPTIONAL MATCH (uc:UseCase)-[:ACTOR]->(sr)
RETURN sr.id AS role_id, sr.name AS role_name,
collect(DISTINCT {entity: de.name}) AS permissions,
collect(DISTINCT uc.id) AS use_cases
ORDER BY sr.id
1.3 Query existing components (for navigation components)
// navigation_components
MATCH (c:Component)
WHERE c.component_type = 'navigation'
RETURN c.id AS id, c.name AS name, c.description AS description
Present to user:
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 27
- Forks
- 4
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
nacl-sa-ui- Source
- github.com/itsalt/nacl