---
name: nacl-sa-ui
model: sonnet
effort: medium
description: |
  UI 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.
---

# /nacl-sa-ui --- UI Architecture (Graph)

## 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 by `nacl-sa-validate` and `nacl-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

```cypher
// 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)

```cypher
// 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

```cypher
// 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

```cypher
// 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

```cypher
// 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)

```cypher
// 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)

```cypher
// 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:**

1. Match orphaned field by name/label similarity to existing DomainAttributes.
2. If field name matches an attribute name on the related entity (entity used in the same UC), propose the MAPS_TO edge.
3. If no match, propose creating a new DomainAttribute (suggest running `/nacl-sa-domain MODIFY`).
4. If field is non-data (button, header, divider), it does NOT need MAPS_TO --- skip.

#### 3.1 Query existing domain attributes for matching

```cypher
// 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

```cypher
// 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

```cypher
// 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

```cypher
// 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

```cypher
// 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:**

1. **DataTable** --- if 3+ forms have list/filter patterns (many read-only fields, same entity), propose a DataTable component.
2. **FormLayout** --- if 3+ forms share similar field arrangement (same field types in same order), propose a FormLayout component.
3. **StatusBadge** --- if 3+ forms display a status field (Enum type on a status-like attribute), propose a StatusBadge component.
4. **DetailCard** --- if 3+ forms are read-only detail views of the same entity structure, propose a DetailCard component.
5. **SearchFilter** --- if 3+ forms include filter/search fields, propose a SearchFilter component.
6. **FileUpload** --- if 2+ forms include file-type fields, propose a FileUpload component.
7. **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

```cypher
// 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)

```cypher
// 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

```cypher
// 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:

1. **`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 with
   `reason ∈ { 'no-form', 'no-inbound-action', 'unreachable-component' }`.
2. **`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-validate` runs `ui_reachability_blockers` as an internal L-rule
  check. Any non-empty result set forces validator status `BLOCKED`.
  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 in `ui_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_SCREEN` screen with `formless = true` — the screen renders no
  Form **by specification** (splash / 404 / landing), so the rule's
  `no-form` blocker is inapplicable: there is no Form to reach. This is
  self-justifying (sa-validate L10.2 already exempts formless screens
  from `RENDERS -> 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

```cypher
// 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

```cypher
// 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)

```cypher
// 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:**

```
System structure for navigation design:

Modules: {N}
| Module | Use Cases | Forms | Roles |
|--------|-----------|-------|-------|
| orders | UC-101, UC-102 | 3 | Manager, Admin |

Roles: {N}
| Role | Use Cases | Entities (CRUD) |
|------|-----------|-----------------|
| Manager | UC-101, UC-102 | Order (CRUD), Product (R) |

Proceed with menu structure proposal?
```

---

### Phase 2: Propose Menu Structure

Based on modules and roles, propose a navigation hierarchy.

**Rules:**
1. Each module becomes a top-level menu section.
2. Each UC with a list form becomes a menu item.
3. Detail/create/edit forms are NOT direct menu items (reached via list navigation).
4. Dashboard is always the first entry point.
5. Settings/profile are separate sections.
6. Menu items are filtered by role.

**Present to user:**

```
Proposed navigation structure:

Main menu (sidebar):
1. Dashboard (all roles)
2. {Module 1}
   2.1. {List screen} (roles: {list})
   2.2. {List screen} (roles: {list})
3. {Module 2}
   3.1. {List screen} (roles: {list})
4. Settings (Admin only)

Route map:
| Path | Screen | Form | UC | Roles |
|------|--------|------|-----|-------|
| / | Dashboard | --- | --- | all |
| /orders | Order List | FORM-OrderList | UC-101 | Manager |
| /orders/:id | Order Detail | FORM-OrderDetail | UC-102 | Manager |
| /orders/new | Create Order | FORM-OrderCreate | UC-101 | Manager |
| /orders/:id/edit | Edit Order | FORM-OrderEdit | UC-103 | Manager |

Questions:
1. Correct menu structure?
2. Default landing page per role?
3. Breadcrumbs needed?
4. Mobile navigation requirements?
```

---

### Phase 3: Define Routes and Role Access

After user confirmation, record navigation in the graph by creating navigation Component nodes.

#### 3.1 Create navigation components

```cypher
// create_nav_component
MERGE (c:Component {id: $componentId})
SET c.name = $name,
    c.component_type = 'navigation',
    c.description = $description,
    c.route = $route,
    c.roles = $roles,
    c.menu_order = $menuOrder,
    c.parent_menu = $parentMenu,
    c.updated = datetime()
RETURN c.id AS id, c.name AS name
```

Parameters:
- `$componentId` --- e.g. `"CMP-NavDashboard"`, `"CMP-NavOrderList"`
- `$name` --- e.g. `"NavDashboard"`, `"NavOrderList"`
- `$route` --- e.g. `"/"`, `"/orders"`, `"/orders/:id"`
- `$roles` --- comma-separated role names, e.g. `"Manager,Admin"`
- `$menuOrder` --- integer for display order
- `$parentMenu` --- parent Component ID or `null` for top-level

#### 3.2 Link navigation to forms via USED_IN

```cypher
// link_nav_to_form
MATCH (c:Component {id: $navComponentId})
MATCH (f:Form {id: $formId})
MERGE (c)-[:USED_IN]->(f)
RETURN c.id AS nav_id, f.id AS form_id
```

This creates the full chain: `Navigation -> Form -> FormField -> DomainAttribute -> DomainEntity`

#### 3.3 Create HAS_INBOUND_ACTION edges (required for actor != SYSTEM)

For every Form whose UseCase has `actor != SYSTEM`, capture each
Component that exposes a user affordance (button, menu item, link, CTA)
to open the Form. One `HAS_INBOUND_ACTION` edge per affordance site.

```cypher
// create_has_inbound_action
MATCH (c:Component {id: $sourceComponentId})
MATCH (f:Form {id: $formId})
MERGE (c)-[r:HAS_INBOUND_ACTION]->(f)
SET r.affordance = $affordance,    // e.g. "primary CTA", "menu item", "row-level link"
    r.label      = $label,          // visible label, e.g. "New upload"
    r.updated    = datetime()
RETURN c.id AS source_component_id, f.id AS form_id, r.label AS label
```

Parameters:
- `$sourceComponentId` --- Component that exposes the affordance (e.g. `"CMP-CatalogPage"`).
- `$formId` --- Form opened by the affordance (e.g. `"FORM-Upload"`).
- `$affordance` --- short kind label (`"primary CTA"`, `"menu item"`, `"row-link"`, `"empty-state CTA"`, etc.).
- `$label` --- exact visible text the user sees on the affordance.

After creating HAS_INBOUND_ACTION edges, immediately verify by running
`ui_reachability_blockers` from `nacl-sa-ui/references/reachability.cypher`.
If the result set is non-empty, present the blockers to the user and
stop the phase — every blocker must be resolved (by adding the missing
affordance to the spec, by re-routing the affordance to a reachable
Component, or by registering a signed exception under W4) before the
navigation phase completes.

---

### Phase 4: Report

```
Navigation structure defined:

Menu items: {N}
Routes: {N}
Role-based access rules: {N}

| Route | Screen | Form | Roles | Menu Item |
|-------|--------|------|-------|-----------|
| / | Dashboard | --- | all | yes |
| /orders | Order List | FORM-OrderList | Manager | yes |
| /orders/:id | Order Detail | FORM-OrderDetail | Manager | no (via list) |

Navigation components created: {N}
USED_IN edges: {N}

Next: `/nacl-sa-ui verify` to re-check form-domain mapping after navigation changes.
```

---

# Command: `state-machine`

## Purpose

Author (or modify) the **deterministic state machine** of one screen: which states the screen can be in, which events move it between states, and which side effects each transition fires. The machine is stored graph-natively — `Screen`, `ScreenState`, `ScreenEvent`, reified `Transition`, `ScreenEffect` (+ minimal `AnalyticsEvent` sinks) — so that a change to the UC, a domain attribute, or an API endpoint **reaches the screen through the graph** (impact closure), and `nacl-sa-validate` L10 can statically check determinism, reachability, and error-escape.

**Why a reified Transition node (not an edge with properties):** (a) the validator needs a stable transition id (`SCRTR-*`) for reports; (b) only a node can be the source of `TRIGGERS -> ScreenEffect`; (c) a reified node falls under the orphan check. This mirrors the BA entity-state pattern (`EntityState-[:TRANSITIONS_TO]->`) one level up.

**Namespace caution:** `HAS_STATE` and `TRIGGERS` edge-type names are shared with the BA layer, and `NAVIGATES_TO` may pre-exist as Form→Form/Component→Form navigation. Always label-qualify (`(:Screen)-[:HAS_STATE]->(:ScreenState)`), never match these types bare.

## Parameters

- `UC-NNN` — the UseCase whose screen is being modeled (one screen per invocation), **or**
- `SCR-Name` — an existing Screen id, to modify its machine.

## Workflow

```
+-----------------+    +-----------------+    +-----------------+    +-----------------+
| Phase 1         |    | Phase 2         |    | Phase 3         |    | Phase 4         |
| Read UC context |--->| Propose machine |--->| Write machine   |--->| Stamp staleness |
| (forms, API,    |    | (states/events/ |    | to graph        |    | + validate L10  |
|  existing SCR)  |    |  transitions)   |    | (MERGE, idem.)  |    | + report        |
+-----------------+    +-----------------+    +-----------------+    +-----------------+
```

**Do not proceed to the next phase without explicit user confirmation.**

---

### Phase 1: Read UC Context

#### 1.1 Query UC, its forms, endpoints, and any existing screen

```cypher
// uc_screen_context
MATCH (uc:UseCase {id: $ucId})
OPTIONAL MATCH (uc)-[:USES_FORM]->(f:Form)
OPTIONAL MATCH (uc)-[:EXPOSES]->(api:APIEndpoint)
OPTIONAL MATCH (uc)-[:HAS_SCREEN]->(scr:Screen)
OPTIONAL MATCH (m:Module)-[:CONTAINS_UC]->(uc)
RETURN uc.id AS uc_id, uc.name AS uc_name, uc.has_ui AS has_ui,
       m.id AS module,
       collect(DISTINCT f.id) AS forms,
       collect(DISTINCT api.id) AS endpoints,
       collect(DISTINCT scr.id) AS existing_screens
```

**Guards:**
- If the UC does not exist → STOP, report.
- If `coalesce(uc.has_ui, true) = false` → STOP: a backend-only UC has no screen to model.
- If the UC has no `USES_FORM` → ask the user whether the screen is genuinely formless (`formless=true`, e.g. splash/404) or whether `/nacl-sa-uc detail` must run first. Do not silently invent a Form.
- If a Screen already exists → load its current machine via the `sa_screen_machine` named query (`graph-infra/queries/sa-queries.cypher`) and present it; the run becomes a MODIFY.

#### 1.2 Read the existing machine (MODIFY mode)

Run `sa_screen_machine($screenId)` and render the result as a transition table + Mermaid `stateDiagram-v2` so the user sees the as-is machine before changing it.

---

### Phase 2: Propose the Machine

**Screen name derivation (deterministic):** the Screen id is `SCR-{PascalName}`.
Derive PascalName from the screen's purpose noun-phrase (form name or UC name):
strip `scr-NNN-` / `FORM-` prefixes, convert kebab/snake to PascalCase —
`scr-031-voice-recorder` → `VoiceRecorder`, `scr-004-result` → `ResultViewer`.
Never embed the UC number in the name.

Propose states, events, transitions, and effects based on the screen archetype.
Two canonical archetypes (templates, not a closed list — derive others from the
UC's ActivitySteps / RuntimeContract when neither fits):

**A. Data-loading screen** (list / detail / result):

| Element | Proposal |
|---------|----------|
| States | `Loading` (state_kind=loading, **is_initial**), `Loaded` (content), `Empty` (empty), `Error` (error) |
| Events | `OnLoaded` (system), `OnLoadFailed` (system), `OnRetry` (user) |
| Transitions | Loading→Loaded on OnLoaded [guard: `items.length > 0`]; Loading→Empty on OnLoaded [guard: `items.length == 0`]; Loading→Error on OnLoadFailed; Error→Loading on OnRetry |
| Effects | one `load` effect on the Error→Loading (OnRetry) transition, CALLS the UC's endpoint |

When the screen starts in `Loading`, the initial fetch is fired by the
implementation on screen entry (the initial state IS the loading state); the
graph-checkable `CALLS` contract hangs on the retry transition — the machine's
only re-entry into `Loading`.

**B. Process screen** (recorder / wizard / pipeline — the screen drives a
user-initiated operation instead of fetching data on open):

| Element | Proposal |
|---------|----------|
| States | `Idle` (state_kind=initial, **is_initial**), one `busy`-kind state per pipeline stage (e.g. `Recording`, `Uploading`, `Transcribing`), `Completed` (content), `Failed` (error) |
| Events | user events to start/stop, `system` events per stage completion/failure, `OnRetry` (user) |
| Transitions | Idle→stage₁ on user start; stageₙ→stageₙ₊₁ on stage success; stageₙ→Failed on failure; Failed→Idle on OnRetry; optionally Completed→stage₁ on re-run |
| Effects | one `mutate` effect per stage transition that hits the backend, each CALLS its endpoint (one provisional endpoint per distinct backend operation) |

Here `Failed→Idle on OnRetry` legitimately carries **no** effect — retry means
"let the user run the operation again", not "re-fetch". Effects are `0..n` per
transition: L10 enforces only that *existing* load/mutate effects CALL an
endpoint; a transition (or a whole machine) without effects is valid. The
canonical effect placements above are authoring guidance, not validator law.

**Authoring rules (the validator will enforce them as L10):**
1. Exactly **one** state has `is_initial=true`.
2. Two transitions may share `(from_state, on_event)` **only if every one of them has a guard** (the guards' disjointness is your responsibility).
3. Every state must be reachable from the initial state.
4. Every `error` state needs an escape transition; name the user-triggered event `OnRetry` by convention (the validator checks `event_kind='user'`, not the name).
5. Every `load`/`mutate` effect CALLS an APIEndpoint; `navigate` effects NAVIGATES_TO a Screen; `analytics` effects EMITS an AnalyticsEvent.
6. The Screen RENDERS the UC's Form — this is the bridge that makes DomainAttribute changes reach the screen. Omit only for genuinely formless screens (`formless=true`).

**Present to user:** transition table + Mermaid `stateDiagram-v2` + effect list with cross-layer targets. Ask for confirmation.

```
Proposed state machine for SCR-{Name} (UC-NNN):

stateDiagram-v2
    [*] --> Loading
    Loading --> Loaded: OnLoaded [items > 0]
    Loading --> Empty: OnLoaded [items == 0]
    Loading --> Error: OnLoadFailed
    Error --> Loading: OnRetry

Effects:
| Transition | Effect | Kind | Target |
|------------|--------|------|--------|
| (entry) Loading | SCREF-{Name}-001 | load | {api-id} |

Confirm or modify?
```

---

### Phase 3: Write the Machine (MERGE, idempotent)

All writes use `MERGE` on stable ids so re-running the command updates rather than duplicates.

#### 3.1 Screen + parent + RENDERS

```cypher
// create_screen
MATCH (uc:UseCase {id: $ucId})
MERGE (scr:Screen {id: $screenId})
SET scr.name = $name,
    scr.description = $description,
    scr.route = $route,
    scr.formless = coalesce($formless, false),
    scr.created_by = 'nacl-sa-ui',
    scr.created_at = coalesce(scr.created_at, datetime()),
    scr.updated = datetime()
MERGE (uc)-[:HAS_SCREEN]->(scr)
RETURN scr.id AS screen_id
```

```cypher
// link_screen_renders_form (skip only when formless=true)
MATCH (scr:Screen {id: $screenId})
MATCH (f:Form {id: $formId})
MERGE (scr)-[:RENDERS]->(f)
RETURN scr.id AS screen_id, f.id AS form_id
```

#### 3.2 States and events

```cypher
// create_screen_state (once per state)
MATCH (scr:Screen {id: $screenId})
MERGE (st:ScreenState {id: $stateId})       // SCRST-{Screen}-{State}
SET st.name = $stateName,
    st.state_kind = $stateKind,              // initial|loading|content|empty|error
    st.is_initial = $isInitial,
    st.terminal = coalesce($terminal, false)
MERGE (scr)-[:HAS_STATE]->(st)
RETURN st.id AS state_id
```

```cypher
// create_screen_event (once per event)
MATCH (scr:Screen {id: $screenId})
MERGE (ev:ScreenEvent {id: $eventId})        // SCREV-{Screen}-{Event}
SET ev.name = $eventName,
    ev.event_kind = $eventKind               // user|system|lifecycle
MERGE (scr)-[:HAS_EVENT]->(ev)
RETURN ev.id AS event_id
```

#### 3.3 Reified transitions

```cypher
// create_transition (once per transition; id from per-screen counter)
MATCH (scr:Screen {id: $screenId})
MATCH (fromSt:ScreenState {id: $fromStateId})
MATCH (toSt:ScreenState {id: $toStateId})
MATCH (ev:ScreenEvent {id: $eventId})
MERGE (tr:Transition {id: $transitionId})    // SCRTR-{Screen}-NNN
SET tr.guard = $guard                        // NULL when unguarded
MERGE (scr)-[:HAS_TRANSITION]->(tr)
MERGE (tr)-[:FROM_STATE]->(fromSt)
MERGE (tr)-[:TO_STATE]->(toSt)
MERGE (tr)-[:ON_EVENT]->(ev)
RETURN tr.id AS transition_id
```

Next available transition/effect number:

```cypher
// next_transition_number
MATCH (scr:Screen {id: $screenId})-[:HAS_TRANSITION]->(tr:Transition)
WITH max(toInteger(split(tr.id, '-')[-1])) AS maxNum
RETURN coalesce(maxNum, 0) + 1 AS next
```

#### 3.4 Effects with kind-required targets

```cypher
// create_load_effect (load | mutate)
MATCH (tr:Transition {id: $transitionId})
MATCH (api:APIEndpoint {id: $apiId})
MERGE (eff:ScreenEffect {id: $effectId})     // SCREF-{Screen}-NNN
SET eff.effect_kind = $effectKind,           // 'load' | 'mutate'
    eff.description = $description
MERGE (tr)-[:TRIGGERS]->(eff)
MERGE (eff)-[:CALLS]->(api)
RETURN eff.id AS effect_id, api.id AS endpoint
```

**If the APIEndpoint does not exist yet** (UC has no `EXPOSES` — common before `nacl-tl-plan` has run): MERGE a **provisional** endpoint and anchor it to the UC so it cannot orphan; `nacl-tl-plan` enriches it later from api-contracts. Report every provisional endpoint created.

```cypher
// create_provisional_endpoint
MATCH (uc:UseCase {id: $ucId})
MERGE (api:APIEndpoint {id: $apiId})         // e.g. "api-result-get"
ON CREATE SET api.path = $path,              // e.g. "GET /api/result/{sessionId}"
              api.provisional = true,
              api.created_by = 'nacl-sa-ui',
              api.created_at = datetime()
MERGE (uc)-[:EXPOSES]->(api)
RETURN api.id AS endpoint, api.provisional AS provisional
```

```cypher
// create_navigate_effect
MATCH (tr:Transition {id: $transitionId})
MATCH (target:Screen {id: $targetScreenId})
MERGE (eff:ScreenEffect {id: $effectId})
SET eff.effect_kind = 'navigate', eff.description = $description
MERGE (tr)-[:TRIGGERS]->(eff)
MERGE (eff)-[:NAVIGATES_TO]->(target)
RETURN eff.id AS effect_id, target.id AS target_screen
```

```cypher
// create_analytics_effect
MATCH (tr:Transition {id: $transitionId})
MERGE (ae:AnalyticsEvent {id: $analyticsId}) // ANEV-{Name}
SET ae.name = $analyticsName
MERGE (eff:ScreenEffect {id: $effectId})
SET eff.effect_kind = 'analytics', eff.description = $description
MERGE (tr)-[:TRIGGERS]->(eff)
MERGE (eff)-[:EMITS]->(ae)
RETURN eff.id AS effect_id, ae.id AS analytics_event
```

#### 3.5 MODIFY mode: removing machine elements

When the user removes a state/transition/effect, `DETACH DELETE` exactly the removed nodes by id — never a label-wide delete. Renumbering existing `SCRTR-*`/`SCREF-*` ids is forbidden (stable ids are what downstream reports reference).

---

### Phase 4: Stamp Staleness + Validate + Report

Authoring or changing a screen state machine **changes the UC's shape**: tasks planned before the machine existed do not reflect it. Therefore (same contract as `nacl-sa-feature` step 3g):

#### 4.1 Bump the UC spec version

```cypher
// bump_spec_version
MATCH (uc:UseCase {id: $ucId})
SET uc.spec_version = coalesce(uc.spec_version, 0) + 1
RETURN uc.id AS uc_id, uc.spec_version AS spec_version
```

#### 4.2 Stamp staleness — DIRECTED and TIGHT (never the broad closure)

The stamp follows the affected-UC list — **the same directed contract as `nacl-sa-feature` step 3g**: the screen's UC's `GENERATES` tasks + tasks of UCs that transitively `DEPENDS_ON` it (`*1..5`), and the directly-changed UC itself. **Never stamp via the undirected `sa_impact_closure` traversal** — it fans out through shared ACTOR/Requirement nodes and marks half the project stale (measured 20× false radius). Two clean statements; report `count(DISTINCT ...)`, never a cartesian row count.

```cypher
// stamp_stale_tasks (1/2) — the re-plan units
MATCH (uc:UseCase {id: $ucId})
OPTIONAL MATCH (dependent:UseCase)-[:DEPENDS_ON*1..5]->(uc)
WITH collect(DISTINCT uc) + [d IN collect(DISTINCT dependent) WHERE d IS NOT NULL] AS affected
UNWIND affected AS a
MATCH (a)-[:GENERATES]->(t:Task)
SET t.review_status = 'stale',
    t.stale_reason = 'screen state machine ' + $changeKind + ' for ' + $ucId + ' (' + $screenId + ')',
    t.stale_since = datetime(),
    t.stale_origin = $screenId
RETURN count(DISTINCT t) AS tasks_stamped
```

```cypher
// stamp_stale_uc (2/2) — the directly-changed UC itself
MATCH (uc:UseCase {id: $ucId})
SET uc.review_status = 'stale',
    uc.stale_reason = 'screen state machine ' + $changeKind + ' for ' + $ucId + ' (' + $screenId + ')',
    uc.stale_since = datetime(),
    uc.stale_origin = $screenId
RETURN count(uc) AS ucs_stamped
```

`$changeKind` ∈ {'created', 'modified'}. The flags are cleared by `nacl-tl-plan` when it re-plans the UC (which also re-bakes the machine into task context).

#### 4.3 Validate the machine (scoped L10)

Run the L10 checks from `nacl-sa-validate` (canonical queries live there) scoped
to this screen. **Scope recipe:** in every screen-anchored query replace the
open anchor `(scr:Screen)` with `(scr:Screen {id: $screenId})`; for the
non-anchored checks L10.0/L10.1, filter by the id family instead:
`WHERE n.id = $screenId OR n.id CONTAINS ('-' + $screenName + '-')` (the
`{Screen}` infix makes every child id of one machine match). Run L10.3 before
L10.5b. Any CRITICAL finding → present it and return to Phase 2; do not leave a
broken machine in the graph.

#### 4.4 Report

```
Screen state machine written: SCR-{Name} (UC-NNN)

Nodes: 1 Screen, {N} states, {N} events, {N} transitions, {N} effects {(+ M provisional APIEndpoint)}
Edges: HAS_SCREEN, RENDERS, {N} HAS_STATE, {N} HAS_EVENT, {N} HAS_TRANSITION,
       {N} FROM_STATE/TO_STATE/ON_EVENT, {N} TRIGGERS, {N} CALLS/NAVIGATES_TO/EMITS

Staleness stamped (directed): {N} tasks + {N} UCs (origin: SCR-{Name})
spec_version: UC-NNN {old} -> {new}

L10 validation: {PASS | N findings}

Next:
  - `/nacl-tl-plan` to re-plan the UC (clears the stale flags)
  - `/nacl-sa-validate internal` for the full gate
```

---

# Command: `full`

## Purpose

Run all phases in sequence: verify form-domain mapping, identify components, define navigation.

## Parameters

- `[module]` (optional) --- Scope to a specific module.

## Workflow

```
+-----------------+    +-----------------+    +-----------------+
| Step 1          |    | Step 2          |    | Step 3          |
| /nacl-sa-ui    |--->| /nacl-sa-ui    |--->| /nacl-sa-ui    |
| verify [module] |    | components      |    | navigation      |
|                 |    | [module]        |    |                 |
+-----------------+    +-----------------+    +-----------------+
```

Execute each step following its own workflow. Between steps, present a transition summary and request confirmation.

**Final report after all three steps:**

```
UI Architecture complete{" for module " + module if specified}:

Form-Domain Mapping:
  Total data fields: {N}
  Mapped: {N} ({pct}%)
  Orphaned: {N}

Components:
  Created: {N} Component nodes
  USED_IN edges: {N}

Navigation:
  Menu items: {N}
  Routes: {N}
  Roles covered: {N}

Full traceability chain:
  Module -> UseCase -> Form -> FormField -> DomainAttribute -> DomainEntity
  Navigation -> Form (via USED_IN)
  Component -> Form (via USED_IN)

Next:
  - `/nacl-sa-validate` to run full cross-layer validation
  - `/nacl-tl-plan` to begin technical planning
```

---

## Error Handling

### Neo4j unavailable

If any `mcp__neo4j__*` call fails with a connection error:

<!-- nacl-graph-halt -->
> Neo4j is not reachable at `bolt://localhost:{$neo4j_bolt_port}`.
> Tell me "start the graph" and I will run `node "$HOME/.claude/skills/nacl-core/scripts/graph-doctor.mjs" --fix` via Bash (works in Claude Code Desktop and CLI).
> Or start it yourself from the project root (main checkout, not a worktree):
> - local mode: `docker compose -f graph-infra/docker-compose.yml up -d` --- if Docker Desktop is not running, open the Docker Desktop app first
> - remote mode: relaunch the sidecar `~/.nacl/sidecar/<project_scope>.sh` (Windows: `%USERPROFILE%\.nacl\sidecar\<project_scope>.cmd`)
> This skill requires Neo4j --- cannot proceed without it.

### No Forms in graph

If `verify` or `components` finds zero Form nodes:

> No Form nodes found in graph. Forms are created during UC detailing. Run `/nacl-sa-uc detail UC-{NNN}` first to create Forms and FormFields, then re-run `/nacl-sa-ui verify`.

### No Modules in graph

If `navigation` finds zero Module nodes:

> No Module nodes found in graph. Modules are created during architecture design. Run `/nacl-sa-architect` first, then re-run `/nacl-sa-ui navigation`.

### Missing domain model

If `verify` finds data fields but no DomainAttributes exist at all:

> WARNING: No DomainAttributes found in graph. Form-domain mapping is impossible without a domain model. Run `/nacl-sa-domain` first to create the domain model, then re-run `/nacl-sa-ui verify`.

---

## Reads / Writes

### Reads

```yaml
# Neo4j (via MCP):
- mcp__neo4j__read-cypher    # Forms, FormFields, Components, DomainAttributes,
                              # Modules, UseCases, SystemRoles, validation queries

# Shared references:
- nacl-core/SKILL.md         # ID rules, Neo4j connection, schema locations
```

### Writes

```yaml
# Neo4j (via MCP):
- mcp__neo4j__write-cypher   # Component nodes, USED_IN edges, MAPS_TO edges (fixes)
```

### Node types created

| Node | Properties |
|------|------------|
| Component | id, name, component_type, description, props, route, roles, menu_order, parent_menu, updated |
| Screen | id, name, description, route, formless, created_by, created_at, updated |
| ScreenState | id, name, state_kind, is_initial, terminal |
| ScreenEvent | id, name, event_kind |
| Transition | id, guard |
| ScreenEffect | id, effect_kind, description |
| AnalyticsEvent | id, name |
| APIEndpoint | id, path, provisional, created_by, created_at — **provisional only**, when a load/mutate effect needs an endpoint that does not exist yet; enriched later by nacl-tl-plan |

### Edge types created

| Edge | From | To | Properties |
|------|------|----|------------|
| USED_IN | Component | Form | --- |
| HAS_INBOUND_ACTION | Component | Form | `affordance`, `label`, `updated` — created during `navigation` Phase 3.3; required for actor-triggered UCs (W7). |
| MAPS_TO | FormField | DomainAttribute | --- (fix only, normally created by nacl-sa-uc) |
| HAS_SCREEN | UseCase | Screen | --- (state-machine command) |
| RENDERS | Screen | Form | --- |
| HAS_STATE | Screen | ScreenState | --- (label-qualify: name shared with BA) |
| HAS_EVENT | Screen | ScreenEvent | --- |
| HAS_TRANSITION | Screen | Transition | --- |
| FROM_STATE / TO_STATE | Transition | ScreenState | exactly one each |
| ON_EVENT | Transition | ScreenEvent | exactly one |
| TRIGGERS | Transition | ScreenEffect | --- (label-qualify: name shared with BA) |
| CALLS | ScreenEffect | APIEndpoint | required for load/mutate effects |
| NAVIGATES_TO | ScreenEffect | Screen | required for navigate effects |
| EMITS | ScreenEffect | AnalyticsEvent | required for analytics effects |
| EXPOSES | UseCase | APIEndpoint | only together with a provisional endpoint |

---

## Validation Queries

Run these after any write operation.

**Cypher --- orphaned FormFields (no MAPS_TO on data fields):**

```cypher
// val_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
```

**Cypher --- components not linked to any form:**

```cypher
// val_orphaned_components
MATCH (c:Component)
WHERE c.component_type <> 'navigation'
  AND NOT (c)-[:USED_IN]->(:Form)
RETURN c.id AS component_id, c.name AS component_name
```

**Cypher --- forms not linked to any component:**

```cypher
// val_forms_without_components
MATCH (f:Form)
WHERE NOT (:Component)-[:USED_IN]->(f)
RETURN f.id AS form_id, f.name AS form_name
```

**Cypher --- UI reachability blockers (actor-triggered UCs with no inbound nav-action from a reachable Component):**

```cypher
// val_ui_reachability_blockers
// Full template: nacl-sa-ui/references/reachability.cypher § 4 (ui_reachability_blockers)
// Returns one row per (UC, Form) blocker with reason ∈ {'no-form', 'no-inbound-action', 'unreachable-component'}.
MATCH (uc:UseCase)-[:ACTOR]->(role:SystemRole)
WHERE coalesce(role.name, '') <> 'SYSTEM'
OPTIONAL MATCH (uc)-[:USES_FORM]->(f:Form)
OPTIONAL MATCH (c:Component)-[:HAS_INBOUND_ACTION]->(f)
WITH uc, role, f, collect(DISTINCT c) AS inbound_components
WITH uc, role, f,
     CASE
       WHEN f IS NULL THEN 'no-form'
       WHEN size(inbound_components) = 0 THEN 'no-inbound-action'
       ELSE NULL
     END AS reason
WHERE reason IS NOT NULL
RETURN uc.id AS uc_id, coalesce(f.id, '<no-form>') AS form_id, reason
ORDER BY uc_id, form_id
```

**Cypher --- entities with no UI surface (no FormField maps to them):**

```cypher
// val_entity_without_ui
MATCH (de:DomainEntity)
WHERE NOT (:FormField)-[:MAPS_TO]->(:DomainAttribute)<-[:HAS_ATTRIBUTE]-(de)
RETURN de.id AS entity_id, de.name AS entity_name, de.module AS module
```

**Cypher --- MAPS_TO coverage stats:**

```cypher
// val_maps_to_coverage
MATCH (f:Form)-[:HAS_FIELD]->(ff:FormField)
WHERE ff.field_type IN ['text', 'textarea', 'number', 'date', 'datetime', 'select', 'multiselect', 'checkbox', 'file']
WITH count(ff) AS total_data_fields
OPTIONAL MATCH (f2:Form)-[:HAS_FIELD]->(ff2:FormField)-[:MAPS_TO]->(:DomainAttribute)
WHERE ff2.field_type IN ['text', 'textarea', 'number', 'date', 'datetime', 'select', 'multiselect', 'checkbox', 'file']
WITH total_data_fields, count(DISTINCT ff2) AS mapped_fields
RETURN total_data_fields, mapped_fields,
       CASE WHEN total_data_fields > 0
            THEN round(100.0 * mapped_fields / total_data_fields)
            ELSE 0 END AS coverage_pct
```

---

## Checklist

### Before completing `verify`
- [ ] All forms loaded from graph
- [ ] Orphaned forms (no UC link) identified
- [ ] Every data-bearing FormField checked for MAPS_TO edge
- [ ] CRITICAL: required fields without MAPS_TO flagged
- [ ] Fixes proposed (match by name or suggest new attribute)
- [ ] User confirmed fixes
- [ ] MAPS_TO edges created for confirmed fixes
- [ ] Coverage stats reported (before/after)

### Before completing `components`
- [ ] All forms analyzed for patterns (field types, entities, structure)
- [ ] Existing components checked (avoid duplicates)
- [ ] Component candidates proposed with rationale
- [ ] User confirmed component list
- [ ] Component nodes created with MERGE
- [ ] USED_IN edges created (Component -> Form)
- [ ] Component inventory presented

### Before completing `navigation`
- [ ] Modules, UCs, roles read from graph
- [ ] Menu hierarchy proposed (modules as sections, list screens as items)
- [ ] Route map defined with role-based access
- [ ] User confirmed navigation structure
- [ ] Navigation Component nodes created
- [ ] USED_IN edges linked to forms
- [ ] **HAS_INBOUND_ACTION edges captured for every Form whose UC actor != SYSTEM (Phase 3.3)**
- [ ] **`val_ui_reachability_blockers` query returns empty result set — or every remaining blocker is covered by a signed exception (W4)**
- [ ] Navigation report presented

### Before completing `state-machine`
- [ ] UC context read (forms, endpoints, existing screens); MODIFY mode detected when a Screen exists
- [ ] Machine proposed as transition table + Mermaid stateDiagram; user confirmed
- [ ] Screen MERGEd with HAS_SCREEN parent and RENDERS → Form (or formless=true confirmed by user)
- [ ] Exactly one is_initial=true state; every transition has same-screen FROM_STATE/TO_STATE/ON_EVENT
- [ ] Shared (from_state, on_event) pairs are all-guarded (determinism)
- [ ] Every error state has an escape transition (user-triggered by convention: OnRetry)
- [ ] load/mutate effects CALL an APIEndpoint (provisional endpoint MERGEd + EXPOSES if missing, and reported)
- [ ] uc.spec_version bumped
- [ ] Staleness stamped DIRECTED, same contract as sa-feature 3g (UC's tasks + transitive DEPENDS_ON-dependents' tasks `*1..5` + the changed UC itself; count(DISTINCT) reported) — never via broad sa_impact_closure
- [ ] Scoped L10 validation run; CRITICAL findings resolved before completing
- [ ] Report presented (nodes/edges written, stamp counts, next steps)

### Before completing `full`
- [ ] `verify` completed
- [ ] `components` completed
- [ ] `navigation` completed
- [ ] Final combined report presented with full traceability chain
