Install
$ agentstack add skill-itsalt-nacl-nacl-render ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
nacl-render -- Graph to Markdown Renderer
Skill converts Neo4j graph data into human-readable Markdown documents with auto-generated Mermaid diagrams. Excalidraw board rendering has moved to the analyst-tool backend (see "Excalidraw Rendering — moved to analyst-tool" below).
Invocation
/nacl-render md [args] [--output ]
Dependencies
nacl-core/SKILL.md-- shared Neo4j connection and schema references- Neo4j MCP tools:
mcp__neo4j__read-cypher - Graph-infra queries:
graph-infra/queries/sa-queries.cypher,graph-infra/queries/handoff-queries.cypher,graph-infra/queries/ba-queries.cypher
Shared Conventions
Output Modes
Every md command supports two output modes:
- Terminal (default) -- print the rendered markdown directly to the terminal so the user can review and copy.
- File (
--output) -- write the rendered markdown to the specified file path and confirm with a short message.
When --output is provided, always use the absolute path. If the target directory does not exist, create it.
Neo4j Access
All queries use mcp__neo4j__read-cypher. Never use write-cypher from this skill -- rendering is strictly read-only.
Mermaid Generation Principle
Mermaid diagrams are AUTO-GENERATED from graph structure, never hand-written. The mapping rules are:
| Graph Pattern | Mermaid Syntax | Diagram Type | |---|---|---| | (de:DomainEntity) + HAS_ATTRIBUTE + RELATES_TO | class blocks + association arrows | classDiagram | | (as:ActivityStep) + step_number ordering | flowchart TD nodes + arrows with swimlanes | flowchart | | (es:EntityState) + TRANSITIONS_TO | stateDiagram-v2 states + transitions | stateDiagram | | (ff:FormField) + MAPS_TO + DomainAttribute | flowchart LR field -> attribute -> entity | flowchart |
Mermaid ID Sanitization
Neo4j IDs may contain hyphens (e.g. UC-101). Mermaid node IDs must be alphanumeric. Rule:
mermaidId = graphId.replace(/-/g, '_')
Example: UC-101 becomes UC_101, OBJ-001-A01 becomes OBJ_001_A01.
Commands: Markdown Rendering
/nacl-render md entity
Render a single DomainEntity as a full markdown document with class diagram.
Step 1: Fetch Data
// Query: render_entity_full
// Params: $entityId -- DomainEntity.id (e.g. "DE-Order")
MATCH (de:DomainEntity {id: $entityId})
OPTIONAL MATCH (de)-[:HAS_ATTRIBUTE]->(da:DomainAttribute)
OPTIONAL MATCH (de)-[rel:RELATES_TO]->(de2:DomainEntity)
OPTIONAL MATCH (de)-[:HAS_ENUM]->(en:Enumeration)-[:HAS_VALUE]->(ev:EnumValue)
OPTIONAL MATCH (de)(de)
RETURN de,
collect(DISTINCT da) AS attributes,
collect(DISTINCT {target_id: de2.id, target_name: de2.name, rel_type: rel.rel_type, cardinality: rel.cardinality}) AS relationships,
collect(DISTINCT {enum_name: en.name, enum_id: en.id, values: collect(DISTINCT ev.value)}) AS enumerations,
be.id AS ba_source_id, be.name AS ba_source_name,
m.id AS module_id, m.name AS module_name;
> Note: The enumerations subquery above may need to be split into two queries if Neo4j raises a nested collect error. In that case, run a separate query: > ``cypher > MATCH (de:DomainEntity {id: $entityId})-[:HAS_ENUM]->(en:Enumeration) > OPTIONAL MATCH (en)-[:HAS_VALUE]->(ev:EnumValue) > RETURN en.id, en.name, collect(ev.value) AS values; > ``
Step 2: Generate Mermaid classDiagram
Map graph data to Mermaid syntax:
classDiagram
class {de.name} {
// For each da in attributes:
+{da.data_type} {da.name}
}
// For each relationship:
{de.name} "{rel.cardinality left}" --> "{rel.cardinality right}" {target_name} : {rel.rel_type}
// For each enumeration:
class {en.name} {
>
// For each value:
{ev.value}
}
{de.name} --> {en.name}
Cardinality mapping (rel.cardinality string to Mermaid):
| Graph cardinality | Left side | Right side | Example | |---|---|---|---| | 1:N | "1" | "*" | Order "1" --> "*" OrderItem | | N:1 | "*" | "1" | OrderItem "*" --> "1" Order | | N:M | "*" | "*" | User "*" --> "*" Role | | 1:1 | "1" | "1" | User "1" --> "1" Profile |
Step 3: Fill Template
---
title: "{de.name}"
type: entity
module: {module_name}
generated_from: graph
date: {YYYY-MM-DD}
---
# {de.name}
## Описание
{de.description}
## BA-источник
| BA-сущность | ID |
|---|---|
| {ba_source_name} | {ba_source_id} |
> Omit this section if ba_source_id is null.
## Диаграмма классов
```mermaid
classDiagram
class {de.name} {
+{da1.data_type} {da1.name}
+{da2.data_type} {da2.name}
...
}
{de.name} "1" --> "*" {target_name} : {rel_type}
...
`` `
## Атрибуты
| Атрибут | Тип | Обязательный | Описание |
|---------|-----|--------------|----------|
| {da.name} | {da.data_type} | {da.required} | {da.description} |
## Связи
| Связь | Целевая сущность | Кардинальность | Тип |
|-------|-------------------|----------------|-----|
| {rel_type} | {target_name} | {cardinality} | {rel_type} |
## Справочники
| Справочник | Значения |
|------------|----------|
| {en.name} | {values joined with ", "} |
> Omit this section if no enumerations found.
/nacl-render md uc
Render a single UseCase as a full markdown document with activity flowchart.
Step 1: Fetch Data
// Query: render_uc_full (reuses sa_uc_full_context pattern)
// Params: $ucId -- UseCase.id (e.g. "UC-101")
MATCH (uc:UseCase {id: $ucId})
OPTIONAL MATCH (uc)-[:HAS_STEP]->(as_step:ActivityStep)
OPTIONAL MATCH (uc)-[:USES_FORM]->(f:Form)-[:HAS_FIELD]->(ff:FormField)
OPTIONAL MATCH (ff)-[:MAPS_TO]->(da:DomainAttribute)(rq:Requirement)
OPTIONAL MATCH (uc)-[:ACTOR]->(sr:SystemRole)
OPTIONAL MATCH (m:Module)-[:CONTAINS_UC]->(uc)
OPTIONAL MATCH (uc)-[:DEPENDS_ON]->(dep:UseCase)
RETURN uc,
collect(DISTINCT as_step) AS activity_steps,
collect(DISTINCT f) AS forms,
collect(DISTINCT {field: ff, attr: da, entity: de}) AS field_mappings,
collect(DISTINCT rq) AS requirements,
collect(DISTINCT sr) AS roles,
m.id AS module_id, m.name AS module_name,
collect(DISTINCT dep) AS dependencies;
Step 2: Generate Mermaid Flowchart from ActivitySteps
Sort activity_steps by step_number. Map each step to a flowchart node, using actor property to assign swimlanes.
Mapping rules:
| ActivityStep property | Mermaid element | |---|---| | as.actor = "User" | Node in subgraph User | | as.actor = "System" | Node in subgraph System | | as.step_type = "action" | Rectangle: A1[description] | | as.step_type = "decision" | Diamond: D1{description} | | as.step_type = "start" | Stadium: Start([description]) | | as.step_type = "end" | Stadium: End([description]) | | Sequential steps | Arrow: A1 --> A2 |
Generation algorithm:
flowchart TD
// For each step sorted by step_number:
// nodeId = sanitize(step.id)
// If step_type == "decision":
// {nodeId}{"{"}description{"}"}
// Else if step_type in ["start","end"]:
// {nodeId}(["description"])
// Else:
// {nodeId}["{step.actor}: {step.description}"]
//
// Connect sequential steps:
// {prev_nodeId} --> {curr_nodeId}
//
// For decisions, use labels from step.branch_yes / step.branch_no if available
If steps have an actor property, group them into swimlanes:
flowchart TD
subgraph User["User"]
A1["Заполняет форму"]
A3["Подтверждает"]
end
subgraph System["System"]
A2["Валидирует данные"]
A4["Сохраняет в БД"]
end
A1 --> A2
A2 --> A3
A3 --> A4
Step 3: Fill Template
---
title: "{uc.id}. {uc.name}"
type: usecase
module: {module_name}
priority: {uc.priority}
generated_from: graph
date: {YYYY-MM-DD}
---
# {uc.id}. {uc.name}
## User Story
Как **{role.name}**, я хочу **{uc.goal}**, чтобы **{uc.benefit}**.
> Build the user story from uc.goal / uc.benefit properties. If those are absent, use uc.description.
## Актор
{role.name} ({role.id})
## Модуль
{module_name} ({module_id})
## Activity Diagram
```mermaid
flowchart TD
...auto-generated from activity_steps...
`` `
## Шаги сценария
| # | Актор | Описание | Тип |
|---|-------|----------|-----|
| {step.step_number} | {step.actor} | {step.description} | {step.step_type} |
## Формы
| Форма | Поля | Связанная сущность |
|-------|------|--------------------|
| {f.name} | {list of ff.name} | {de.name} |
## Требования
| ID | Описание | Тип | Приоритет |
|----|----------|-----|-----------|
| {rq.id} | {rq.description} | {rq.type} | {rq.priority} |
## Зависимости
| UC | Название |
|----|----------|
| {dep.id} | {dep.name} |
> Omit sections that have no data (empty collections).
/nacl-render md form
Render a form with field-to-attribute mapping diagram.
Step 1: Fetch Data
// Query: render_form_mapping
// Params: $formId -- Form.id (e.g. "FORM-OrderCreate")
MATCH (f:Form {id: $formId})-[:HAS_FIELD]->(ff:FormField)
OPTIONAL MATCH (ff)-[:MAPS_TO]->(da:DomainAttribute)(f)
RETURN f,
collect(DISTINCT {
field_name: ff.name,
field_id: ff.id,
field_type: ff.field_type,
field_label: ff.label,
attr_name: da.name,
attr_id: da.id,
attr_type: da.data_type,
entity_name: de.name,
entity_id: de.id
}) AS field_mappings,
collect(DISTINCT uc) AS use_cases;
Step 2: Generate Mermaid Mapping Diagram
Build a flowchart LR that shows field -> attribute -> entity chains:
flowchart LR
subgraph Form["{f.name}"]
// For each field:
{ff_mermaidId}["{ff.label}{ff.field_type}"]
end
subgraph Domain["Domain Model"]
// For each unique entity:
subgraph {de_mermaidId}["{de.name}"]
// For each attribute mapped to from this form:
{da_mermaidId}["{da.name} : {da.data_type}"]
end
end
// For each mapping:
{ff_mermaidId} --> {da_mermaidId}
Fields with no MAPS_TO get a dashed arrow to a "unmapped" node:
{ff_mermaidId} -.-> Unmapped["unmapped"]
Step 3: Fill Template
---
title: "Форма: {f.name}"
type: form-mapping
generated_from: graph
date: {YYYY-MM-DD}
---
# Форма: {f.name}
## Связанные UC
| UC | Название |
|----|----------|
| {uc.id} | {uc.name} |
## Диаграмма маппинга
```mermaid
flowchart LR
...auto-generated field->attribute->entity mapping...
`` `
## Таблица полей
| Поле | Label | Тип поля | Атрибут | Тип атрибута | Сущность |
|------|-------|----------|---------|--------------|----------|
| {ff.name} | {ff.label} | {ff.field_type} | {da.name} | {da.data_type} | {de.name} |
## Покрытие
- Полей: {total_fields}
- Замаплено: {mapped_count} ({mapped_pct}%)
- Незамаплено: {unmapped_count}
/nacl-render md domain-model
Render the full domain model: all entities, relationships, and attributes as a single class diagram.
Step 1: Fetch Data
// Query: render_domain_model_full (reuses sa_domain_model pattern)
MATCH (de:DomainEntity)
OPTIONAL MATCH (de)-[:HAS_ATTRIBUTE]->(da:DomainAttribute)
OPTIONAL MATCH (de)-[rel:RELATES_TO]->(de2:DomainEntity)
OPTIONAL MATCH (de)-[:HAS_ENUM]->(en:Enumeration)-[:HAS_VALUE]->(ev:EnumValue)
OPTIONAL MATCH (m:Module)-[:CONTAINS_ENTITY]->(de)
RETURN de,
collect(DISTINCT da) AS attributes,
collect(DISTINCT {target_id: de2.id, target_name: de2.name, rel_type: rel.rel_type, cardinality: rel.cardinality}) AS relationships,
collect(DISTINCT {enum_id: en.id, enum_name: en.name, values: collect(DISTINCT ev.value)}) AS enumerations,
m.name AS module_name;
> Same note as entity: if nested collect fails, run enumeration query separately.
Step 2: Generate Full Mermaid classDiagram
Build the class diagram from ALL entities at once:
classDiagram
%% ===== ENTITIES =====
// For each DomainEntity de:
class {de.name} {
// For each attribute da:
+{da.data_type} {da.name}
}
%% ===== ENUMERATIONS =====
// For each unique Enumeration en:
class {en.name} {
>
// For each value:
{ev.value}
}
%% ===== RELATIONSHIPS =====
// For each RELATES_TO edge (deduplicated):
{source.name} "{left_card}" --> "{right_card}" {target.name} : {rel_type}
// For each HAS_ENUM edge:
{de.name} --> {en.name}
Deduplication rule: If both A -> B and B -> A exist for the same reltype, keep only one (the one where `A.name (uc) OPTIONAL MATCH (uc)-[:ACTOR]->(sr:SystemRole) OPTIONAL MATCH (uc)-[:DEPENDSON]->(dep:UseCase) RETURN uc.id AS id, uc.name AS name, uc.priority AS priority, uc.status AS status, m.name AS modulename, collect(DISTINCT sr.name) AS actors, collect(DISTINCT dep.id) AS dependson ORDER BY uc.id;
#### Step 2: Fill Template
No Mermaid diagram needed for the index -- it is a pure table.
```markdown
---
title: "UC Index"
type: uc-index
generated_from: graph
date: {YYYY-MM-DD}
---
# Реестр Use Cases
| ID | Название | Модуль | Приоритет | Статус | Актор(ы) | Зависимости |
|----|----------|--------|-----------|--------|----------|-------------|
| {id} | {name} | {module_name} | {priority} | {status} | {actors joined} | {depends_on joined} |
## Статистика
- Всего UC: {total}
- Primary: {count where priority = "primary"}
- Secondary: {count where priority = "secondary"}
- По модулям: {module_name}: {count}, ...
/nacl-render md traceability
Render the BA to SA traceability matrix.
Step 1: Fetch Data
Run the handoff_traceability_matrix query:
// Query: handoff_traceability_matrix
MATCH (ws:WorkflowStep)-[:AUTOMATES_AS]->(uc:UseCase)
RETURN 'Step→UC' AS category, ws.id AS ba_id, ws.function_name AS ba_name, uc.id AS sa_id, uc.name AS sa_name
UNION ALL
MATCH (be:BusinessEntity)-[:REALIZED_AS]->(de:DomainEntity)
RETURN 'Entity→Domain' AS category, be.id AS ba_id, be.name AS ba_name, de.id AS sa_id, de.name AS sa_name
UNION ALL
MATCH (br:BusinessRole)-[:MAPPED_TO]->(sr:SystemRole)
RETURN 'Role→SysRole' AS category, br.id AS ba_id, br.full_name AS ba_name, sr.id AS sa_id, sr.name AS sa_name
UNION ALL
MATCH (brq:BusinessRule)-[:IMPLEMENTED_BY]->(rq:Requirement)
RETURN 'Rule→Req' AS category, brq.id AS ba_id, brq.name AS ba_name, rq.id AS sa_id, rq.description AS sa_name;
Also fetch coverage stats:
// Query: handoff_coverage_stats (from handoff-queries.cypher)
// ... full query as in graph-infra/queries/handoff-queries.cypher
Step 2: Fill Template
Group results by category and render four sections:
---
title: "BA→SA Traceability Matrix"
type: traceability
generated_from: graph
date: {YYYY-MM-DD}
---
# Трассировочная матрица BA → SA
## Покрытие
| Категория | Покрыто | Всего | % |
|-----------|---------|-------|---|
| Шаги → UC | {covered} | {total} | {pct}% |
| Сущности → Domain | {covered} | {total} | {pct}% |
| Роли → SystemRole | {covered} | {total} | {pct}% |
| Правила → Requirements | {covered} | {total} | {pct}% |
## 1. Бизнес-шаги → Use Cases
| BA ID | BA Функция | SA ID | SA Use Case |
|-------|------------|-------|-------------|
| {ba_id} | {ba_name} | {sa_id} | {sa_name} |
## 2. Бизнес-сущности → Domain Entities
| BA ID | BA Сущность | SA ID | SA Domain Entity |
|-------|-------------|-------|------------------|
| {ba_id} | {ba_name} | {sa_id} | {sa_name} |
## 3. Бизнес-роли → System Roles
| BA ID | BA Роль | SA ID | SA System Role |
|-------|---------|-------|----------------|
| {ba_id} | {ba_name} | {sa_id} | {sa_name} |
## 4. Бизнес-правила → Requirements
| BA ID | BA Правило | SA ID | SA Requirement |
|-------|------------|-------|----------------|
| {ba_id} | {ba_name} | {sa_id} | {sa_name} |
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [ITSalt](https://github.com/ITSalt)
- **Source:** [ITSalt/NaCl](https://github.com/ITSalt/NaCl)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.