| name | nacl-publish |
| model | sonnet |
| effort | low |
| description | Публикация графа в Docmost и генерация Excalidraw-бордов.
Используй когда пользователь просит: опубликовать граф, синхронизировать с Docmost,
сгенерировать борды, nacl-publish, graph publish.
|
/nacl-publish -- Граф -> Docmost + Excalidraw Boards
Назначение
Публикация данных из Neo4j-графа во внешние форматы:
- Docmost -- генерация markdown из графа (через
nacl-render md) и публикация страниц в Docmost
- Excalidraw Boards -- генерация визуальных бордов из графа и привязка к страницам Docmost
Neo4j Graph
|
├── nacl-render md ──> Markdown ──> Docmost Pages (/nacl-publish docmost)
|
├── nacl-render excalidraw ──> .excalidraw files (/nacl-publish boards)
|
└── Excalidraw links ──> Docmost Pages updated (/nacl-publish boards-link)
Dependencies
nacl-core/SKILL.md -- shared Neo4j connection, schema, ID rules
nacl-render/SKILL.md -- md and excalidraw rendering logic
- Neo4j MCP:
mcp__neo4j__read-cypher
- Docmost MCP:
mcp__docmost__create_page, mcp__docmost__update_page, mcp__docmost__list_spaces, mcp__docmost__get_page, mcp__docmost__list_pages, mcp__docmost__search
Config Resolution
| Параметр | Источник (по приоритету) | Fallback |
|---|
| Docmost API URL | config.yaml → docmost.api_url | Использовать env-настройки Docmost MCP |
space_id (graph scope) | config.yaml → docmost.spaces.graph.space_id > docmost.spaces.sa.space_id > manifest space_id | Спросить пользователя через mcp__docmost__list_spaces |
root_page_id (graph scope) | config.yaml → docmost.spaces.graph.root_page_id > docmost.spaces.sa.root_page_id > manifest root_page_id | Создать новую корневую страницу |
| Boards directory | config.yaml → graph.boards_dir | graph-infra/boards |
| Neo4j Bolt port | config.yaml → graph.neo4j_bolt_port | 3587 |
Принцип: config.yaml — первый источник для адресации Docmost. Manifest (.docmost-sync.json) отвечает только за page-level sync state (page IDs, content hashes, last_updated). Если в проекте нет отдельного spaces.graph — fallback на spaces.sa, поскольку graph и SA публикуют один и тот же набор страниц.
Invocation
/nacl-publish <command> [args]
Commands Overview
| Command | Description | Status |
|---|
docmost | Full publish: generate md from graph, create/update all pages | Implemented |
docmost-incremental | Publish only nodes changed since last sync | Implemented |
docmost-preview <type> <id> | Preview one page in terminal (no publish) | Implemented |
boards | Generate all Excalidraw boards from graph | Implemented |
boards-link | Add board links/embeds to Docmost pages | Implemented |
full | Complete pipeline: docmost + boards + boards-link | Implemented |
Manifest: .docmost-sync.json
Located at project root (next to graph-infra/). This file tracks synchronization state between the graph and Docmost.
Structure
{
"project": "project-name",
"space_id": "019cd479-...",
"root_page_id": "019cd6de-...",
"last_sync": "2026-03-20T14:30:00Z",
"pages": {
"DE-Order": {
"page_id": "019cd6f0-...",
"parent_page_id": "019cd6df-33de-...",
"content_hash": "sha256:a1b2c3d4...",
"last_updated": "2026-03-20T14:30:00Z",
"source_type": "entity",
"source_id": "DE-Order"
},
"UC-101": {
"page_id": "019cd6f1-...",
"parent_page_id": "019cd6df-4348-...",
"content_hash": "sha256:e5f6a7b8...",
"last_updated": "2026-03-20T14:30:00Z",
"source_type": "uc",
"source_id": "UC-101"
}
},
"sections": {
"Архитектура": "019cd6df-2475-...",
"Domain Model": "019cd6df-33de-...",
"Use Cases": "019cd6df-4348-...",
"Интерфейсы": "019cd6df-5512-...",
"Трассировка": "019cd6df-6623-...",
"Роли и права": "019cd6df-7734-..."
}
}
Fields
| Field | Description |
|---|
project | Project name (from graph or user input) |
space_id | Cached Docmost space ID. Source of truth: config.yaml → docmost.spaces.graph.space_id (fallback spaces.sa.space_id). Manifest stores it for offline reference; config.yaml wins on conflict. |
root_page_id | Cached root page ID. Source of truth: config.yaml → docmost.spaces.graph.root_page_id (fallback spaces.sa.root_page_id). Manifest stores it for offline reference; config.yaml wins on conflict. |
last_sync | ISO 8601 timestamp of the last full or incremental sync |
pages | Map: logical page key -> {page_id, parent_page_id, content_hash, last_updated, source_type, source_id} |
sections | Map: section name -> Docmost page ID (section pages serve as parents) |
Content Hash
Compute SHA-256 of the generated markdown content (trimmed trailing whitespace per line):
echo "$CONTENT" | sed 's/[[:space:]]*$//' | shasum -a 256 | cut -d' ' -f1
Page Structure in Docmost
The hierarchy mirrors the project's artifact types, organized by modules:
{Project}/
├── Архитектура/
│ ├── Context Map
│ └── Модули
├── Domain Model/
│ ├── {Entity1}
│ ├── {Entity2}
│ └── ...
├── Use Cases/
│ ├── UC Index
│ ├── {UC-101}
│ ├── {UC-102}
│ └── ...
├── Интерфейсы/
│ ├── {Form1}
│ ├── {Form2}
│ └── ...
├── Трассировка/
│ └── BA -> SA Matrix
└── Роли и права/
└── Permission Matrix
Section-to-Graph Mapping
| Section | Graph Source | nacl-render command |
|---|
| Context Map | Module nodes + DEPENDS_ON edges | md domain-model (module overview) |
| Domain Model / {Entity} | DomainEntity + attrs + rels | md entity <id> |
| UC Index | All UseCase nodes | md uc-index |
| Use Cases / {UC} | UseCase + steps + forms | md uc <id> |
| Интерфейсы / {Form} | Form + fields + mapping | md form <id> |
| Трассировка | BA->SA handoff edges | md traceability |
| Роли и права | SystemRole + Permission nodes | Custom query (see below) |
Pre-flight Checks
Before any Docmost command, verify:
-
Docmost MCP available? Call mcp__docmost__list_spaces.
- If fails ->
ERROR: Docmost MCP not available. Check that the MCP server is connected.
-
Neo4j available? Call mcp__neo4j__read-cypher with RETURN 1.
- If fails ->
ERROR: + "Neo4j is not reachable at bolt://localhost:{$neo4j_bolt_port}. Tell me "start the graph" (I will run node "$HOME/.claude/skills/nacl-core/scripts/graph-doctor.mjs" --fix) or start it from the project root: docker compose -f graph-infra/docker-compose.yml up -d (local) / ~/.nacl/sidecar/<project_scope>.sh (remote)."
-
Graph has data? Query:
MATCH (n)
WITH labels(n) AS lbls, count(*) AS cnt
UNWIND lbls AS lbl
RETURN lbl, sum(cnt) AS total ORDER BY lbl
- If empty ->
WARNING: Graph is empty. Run /nacl-ba-from-board or seed data first.
-
Manifest exists? Read .docmost-sync.json.
- If missing and command is not
docmost (full) -> suggest running /nacl-publish docmost first.
Pre-publish reconciliation gate
nacl-publish writes externally-visible artifacts (Docmost pages,
Excalidraw boards). Publishing inconsistent state to Docmost makes
the drift visible to stakeholders and harder to retract than an
internal .tl/ artifact. This gate is mandatory before any
Docmost write (full docmost, docmost-incremental, and
boards-link commands). Preview-only commands (docmost-preview)
are exempt.
The gate compares the live graph (the source publish is about
to derive from) against .tl/changelog.md (the record of what has
been shipped). If these two disagree, the publish is refused.
Live graph reads only — no .cypher export fallback. Exports
are stale by definition the moment the next graph mutation lands,
and a publish run that consumed an export would push stale pages to
Docmost. If the graph container is unreachable, the gate emits
Status: BLOCKED with workflow detail graph_unavailable and the
publish refuses. The fix is to bring the graph container up
(docker compose up -d from graph-infra/), not to swap in an
export. Operators who must publish under an unreachable graph file
a W4 signed exception against gate graph-stale — the exception
does NOT re-enable export fallback; it accepts that the publish
ran against unreconciled state.
Step 1: Reach the live graph
Use the project-resolved Bolt endpoint:
RETURN 1 AS ok
On failure: HALT.
HALT — graph_unavailable (pre-publish reconciliation).
The live Neo4j graph is unreachable. A stale .cypher export is
NOT an acceptable substitute — publishing it to Docmost would
push out-of-date pages to stakeholders.
Resolution: bring the project graph container up and rerun, or
file a signed exception (.tl/exceptions/) against gate
`graph-stale`.
Status: BLOCKED (workflow detail: graph_unavailable)
Step 2: Read graph state and changelog
// Single round-trip read of the comparison surface:
OPTIONAL MATCH (fr:FeatureRequest)
WITH collect({ id: fr.id, release_tag: fr.release_tag }) AS fr_list
OPTIONAL MATCH (uc:UseCase)
WITH fr_list, collect(uc.id) AS uc_list
OPTIONAL MATCH (t:Task)
WITH fr_list, uc_list, collect({
id: t.id, release_tag: t.release_tag,
evidence: coalesce(t.verification_evidence, '')
}) AS task_list
RETURN fr_list, uc_list, task_list
Read .tl/changelog.md and parse the most recent released
section: every FR-NNN / UC-NNN mentioned, the release tag
header, and any release-status.json cross-reference.
Step 3: Cross-checks (publish-scope subset)
Reuses pairs from the W5 reconciliation taxonomy but scoped to the
publish target:
| Pair | Sources | Assertion |
|---|
| P-P1 | .tl/changelog.md released FR list vs graph FeatureRequest | every released FR-NNN in the latest changelog section exists as FeatureRequest {id: 'FR-NNN'} in the live graph. |
| P-P2 | .tl/changelog.md released UC list vs graph UseCase | every released UC-NNN in the latest changelog section exists as UseCase {id: 'UC-NNN'}. |
| P-P3 | .tl/release-status.json.release_tag vs graph release_tag properties | if the JSON release tag is non-null, the live graph carries that tag on ≥1 FeatureRequest or Task node. (Skip if .tl/release-status.json is absent — the gate does not require it; absence is logged.) |
An assertion that fails under no active signed exception is a hard
refusal.
Step 4: Refuse on disagreement
If any of P-P1 … P-P3 fails, refuse the publish:
REFUSED — changelog and live graph disagree.
nacl-publish writes externally-visible Docmost pages and refuses
to publish stale or contradictory state.
P-P1 changelog.md vs live graph FeatureRequest
.tl/changelog.md (section "0.18.0 ...") references FR-007;
live graph has NO FeatureRequest {id: 'FR-007'}.
Active signed exceptions against `graph-stale`: none.
Resolution options:
[1] Run /nacl-tl-conductor (or /nacl-sa-feature FR-007) to
reconcile graph state with the changelog claim.
[2] If the changelog is correct and the graph is genuinely
stale, file a signed exception against `graph-stale` and
rerun.
Status: BLOCKED (workflow detail: publish-drift)
Step 5: Record reconciliation evidence
On PASS (or PASS-under-exception), write the publish-side
reconciliation evidence:
.tl/reconciliation/<ISO-8601-utc>-publish.json
Same schema as the conductor's reconciliation artifact
(see <NaCl-checkout>/.tl/reconciliation/ _template.json) but with sources_checked scoped to the publish
subset and terminal_status recorded against the publish gate
specifically.
Only on terminal_status == VERIFIED does the publish proceed to
the actual Docmost write steps.
Command: /nacl-publish docmost
Full publish -- generate markdown from graph for every artifact and create/update all pages in Docmost.
Workflow
Step 1: Init/Load Step 2: Create Step 3: Generate & Step 4: Save
── manifest ──── -> ── section pages ── -> ── publish pages ── -> ── manifest ──
Step 1: Resolve Docmost target, then Initialize or Load Manifest
Step 1a: Read config.yaml → docmost (first source for space_id / root_page_id):
- Try
docmost.spaces.graph.space_id and docmost.spaces.graph.root_page_id.
- If
spaces.graph is missing, fall back to docmost.spaces.sa.space_id / spaces.sa.root_page_id (graph and SA publish into the same Docmost area in most projects).
- If both are empty -> mark
space_id and root_page_id as "needs prompt".
Step 1b: If .docmost-sync.json does NOT exist (first run):
- If
space_id resolved from config.yaml:
- Validate by calling
mcp__docmost__list_spaces -- confirm the space still exists.
- Use the resolved
space_id directly. Do not prompt the user.
- Otherwise ask the user:
space_id -- which Docmost space to publish into? (list spaces via mcp__docmost__list_spaces)
- After the user chooses, suggest writing the value to
config.yaml → docmost.spaces.graph.space_id so the next run is non-interactive.
- If
root_page_id resolved from config.yaml:
- Use it as-is, do not create a new root page.
- Otherwise create a new root page:
mcp__docmost__create_page(
title: "{project} -- Спецификация",
content: "# {project}\n\nАвтоматически сгенерированная спецификация из графа знаний.",
spaceId: "{space_id}"
)
Save returned id as root_page_id and suggest writing it to config.yaml → docmost.spaces.graph.root_page_id.
- Initialize manifest with empty
pages and sections. Persist space_id / root_page_id into the manifest as cache, but config.yaml remains the source of truth on subsequent runs.
Step 1c: If .docmost-sync.json EXISTS:
- Read manifest.
- Reconcile
space_id / root_page_id:
- If
config.yaml has values -> use them. If they differ from manifest, log a warning and update manifest to match config.yaml (manifest is a cache, not the source of truth).
- If
config.yaml is empty -> fall back to manifest values.
- Validate
space_id by calling mcp__docmost__list_spaces -- confirm the space still exists.
- Continue with existing section pages from
sections map.
Step 2: Create Section Pages
For each section in the hierarchy, create a parent page if not already in sections:
| Section | Title | Parent |
|---|
| Архитектура | Архитектура | root_page_id |
| Domain Model | Domain Model | root_page_id |
| Use Cases | Use Cases | root_page_id |
| Интерфейсы | Интерфейсы | root_page_id |
| Трассировка | Трассировка | root_page_id |
| Роли и права | Роли и права | root_page_id |
For each section not in sections map:
mcp__docmost__create_page(
title: "{section title}",
content: "# {section title}\n\nРаздел спецификации.",
spaceId: "{space_id}",
parentPageId: "{root_page_id}"
)
Save returned id to sections[title].
Order of creation: Архитектура, Domain Model, Use Cases, Интерфейсы, Трассировка, Роли и права. Sequential calls (Docmost does not guarantee ordering with parallel requests).
Step 3: Generate Content and Publish Pages
Process artifacts in this order:
3a: Architecture -- Context Map
-
Use nacl-render md domain-model logic to generate the full domain model overview.
-
Also generate a module list page:
Query modules:
MATCH (m:Module)
OPTIONAL MATCH (m)-[:CONTAINS_ENTITY]->(de:DomainEntity)
OPTIONAL MATCH (m)-[:CONTAINS_UC]->(uc:UseCase)
RETURN m.id AS id, m.name AS name, m.description AS description,
count(DISTINCT de) AS entity_count, count(DISTINCT uc) AS uc_count
ORDER BY m.id
Generate markdown:
# Модули
| ID | Модуль | Описание | Сущностей | Use Cases |
|----|--------|----------|-----------|-----------|
| {id} | {name} | {description} | {entity_count} | {uc_count} |
-
Publish to section Архитектура:
- Page "Context Map" --
create_page or update_page under sections["Архитектура"]
- Page "Модули" -- under
sections["Архитектура"]
3b: Domain Model -- Entities
-
Query all DomainEntities:
MATCH (de:DomainEntity)
RETURN de.id AS id, de.name AS name
ORDER BY de.id
-
For each entity, use nacl-render md entity <id> logic to generate full markdown.
-
Publish each entity page under sections["Domain Model"]:
mcp__docmost__create_page(
title: "{de.name}",
content: "{generated_markdown}",
spaceId: "{space_id}",
parentPageId: "{sections['Domain Model']}"
)
or mcp__docmost__update_page(pageId, content) if pages[de.id] already exists.
-
Save to pages:
"{de.id}": {
"page_id": "{returned_id}",
"parent_page_id": "{sections['Domain Model']}",
"content_hash": "sha256:...",
"last_updated": "{now_iso}",
"source_type": "entity",
"source_id": "{de.id}"
}
3c: Use Cases -- Index + Individual UCs
-
Generate UC Index using nacl-render md uc-index logic.
-
Publish as page "UC Index" under sections["Use Cases"].
-
Query all UseCases:
MATCH (uc:UseCase)
RETURN uc.id AS id, uc.name AS name
ORDER BY uc.id
-
For each UC, use nacl-render md uc <id> logic to generate markdown.
-
Publish each UC page under sections["Use Cases"].
3d: Interfaces -- Forms
-
Query all Forms:
MATCH (f:Form)
RETURN f.id AS id, f.name AS name
ORDER BY f.id
-
For each form, use nacl-render md form <id> logic to generate markdown.
-
Publish each form page under sections["Интерфейсы"].
3e: Traceability
- Use
nacl-render md traceability logic to generate the BA->SA matrix.
- Publish as page "BA -> SA Matrix" under
sections["Трассировка"].
3f: Roles and Permissions
-
Query SystemRoles:
MATCH (sr:SystemRole)
OPTIONAL MATCH (sr)<-[:ACTOR]-(uc:UseCase)
OPTIONAL MATCH (sr)<-[:MAPPED_TO]-(br:BusinessRole)
RETURN sr.id AS id, sr.name AS name, sr.description AS description,
collect(DISTINCT uc.id) AS use_cases,
collect(DISTINCT br.full_name) AS ba_roles
ORDER BY sr.id
-
Generate permission matrix markdown:
# Роли и права
| Роль | Описание | BA-роль | Доступные UC |
|------|----------|---------|--------------|
| {name} | {description} | {ba_roles joined} | {use_cases joined} |
-
Publish under sections["Роли и права"].
Step 4: Save Manifest
- Update
last_sync to current ISO 8601 timestamp.
- Write
.docmost-sync.json.
- Report summary:
Публикация завершена:
- Создано: {created_count} страниц
- Обновлено: {updated_count} страниц
- Пропущено: {skipped_count} (без изменений)
- Секций: {section_count}
- Manifest: .docmost-sync.json обновлён
Create vs Update Decision
For each page:
- Check if
pages[key] exists in manifest.
- If yes -- page already published:
- Generate markdown, compute content_hash.
- If content_hash matches manifest -> SKIP (no changes).
- If content_hash differs -> UPDATE via
mcp__docmost__update_page(page_id, content).
- If no -> CREATE via
mcp__docmost__create_page(title, content, spaceId, parentPageId).
Publishing Pace
Publish pages sequentially with a brief pause between API calls (Docmost does not guarantee ordering with parallel requests). Process order:
- Section pages (parents first)
- Summary/index pages (Context Map, UC Index, Permission Matrix, Traceability)
- Individual artifact pages (entities, UCs, forms) -- alphabetically within each section
Command: /nacl-publish docmost-incremental
Publish only graph nodes that changed since the last sync.
Workflow
Step 1: Load manifest Step 2: Detect Step 3: Regenerate Step 4: Publish
── & timestamps ──── -> ── changed nodes ── -> ── markdown ────── -> ── & save ─────
Step 1: Load Manifest
- Read
.docmost-sync.json.
- If missing ->
ERROR: No manifest found. Run /nacl-publish docmost first for initial sync.
- Extract
last_sync timestamp.
Step 2: Detect Changed Nodes
Query Neo4j for nodes modified after last_sync. All graph nodes should have an updated_at property (ISO 8601 string or epoch).
// Entities changed since last sync
WITH datetime($lastSync) AS since
MATCH (de:DomainEntity)
WHERE datetime(de.updated_at) > since
RETURN 'entity' AS type, de.id AS id, de.name AS name
UNION ALL
// UseCases changed (or their steps changed)
WITH datetime($lastSync) AS since
MATCH (uc:UseCase)
WHERE datetime(uc.updated_at) > since
RETURN 'uc' AS type, uc.id AS id, uc.name AS name
UNION ALL
// Also catch UCs whose ActivitySteps changed
WITH datetime($lastSync) AS since
MATCH (as_step:ActivityStep)
WHERE datetime(as_step.updated_at) > since
WITH as_step
MATCH (uc:UseCase)-[:HAS_STEP]->(as_step)
RETURN DISTINCT 'uc' AS type, uc.id AS id, uc.name AS name
UNION ALL
// Forms changed (or their fields changed)
WITH datetime($lastSync) AS since
MATCH (f:Form)
WHERE datetime(f.updated_at) > since
RETURN 'form' AS type, f.id AS id, f.name AS name
UNION ALL
WITH datetime($lastSync) AS since
MATCH (ff:FormField)
WHERE datetime(ff.updated_at) > since
WITH ff
MATCH (f:Form)-[:HAS_FIELD]->(ff)
RETURN DISTINCT 'form' AS type, f.id AS id, f.name AS name
UNION ALL
// Roles changed
WITH datetime($lastSync) AS since
MATCH (sr:SystemRole)
WHERE datetime(sr.updated_at) > since
RETURN 'role' AS type, sr.id AS id, sr.name AS name
Fallback if nodes lack updated_at: regenerate markdown for all pages in manifest, compare content_hash, and publish only those with changed hashes. Report that updated_at properties are missing and recommend adding them.
Step 3: Regenerate Affected Pages
For each changed node, call the appropriate nacl-render md logic:
| type | Render command logic | Page key |
|---|
entity | nacl-render md entity <id> | {id} (e.g., DE-Order) |
uc | nacl-render md uc <id> | {id} (e.g., UC-101) |
form | nacl-render md form <id> | {id} (e.g., FORM-OrderCreate) |
role | Custom role query (see docmost Step 3f) | Regenerate full roles page |
Also regenerate cross-cutting pages if any entity or UC changed:
- UC Index -- if any UC changed
- Traceability -- if any entity or UC changed
- Context Map -- if any entity changed (module composition may shift)
Step 4: Publish and Save
For each regenerated page:
- Compute new content_hash.
- Compare with
pages[key].content_hash in manifest.
- If different:
mcp__docmost__update_page(page_id, content) using page_id from manifest.
- Update
content_hash and last_updated in manifest.
- If same -> skip (no actual change despite node timestamp update).
Update last_sync and save manifest. Report:
Инкрементальная синхронизация:
- Проверено: {checked_count} узлов
- Обновлено: {updated_count} страниц
- Пропущено: {skipped_count} (content_hash совпадает)
- Новых: {created_count} (узел добавлен после прошлого sync)
- Manifest: .docmost-sync.json обновлён
Command: /nacl-publish docmost-preview <type> <id>
Preview a single page in the terminal without publishing to Docmost.
Supported Types
| Type | Argument | nacl-render logic |
|---|
entity | DomainEntity.id (e.g., DE-Order) | md entity <id> |
uc | UseCase.id (e.g., UC-101) | md uc <id> |
form | Form.id (e.g., FORM-OrderCreate) | md form <id> |
uc-index | (no id needed) | md uc-index |
domain-model | (no id needed) | md domain-model |
traceability | (no id needed) | md traceability |
roles | (no id needed) | Custom roles query |
Workflow
-
Validate <type> is one of the supported types.
- If invalid ->
ERROR: Unknown type "{type}". Supported: entity, uc, form, uc-index, domain-model, traceability, roles.
-
Call the corresponding nacl-render md logic to generate markdown.
- For types requiring
<id>, validate that <id> is provided.
- If node not found in graph ->
ERROR: {type} "{id}" not found in graph.
-
Print the generated markdown to the terminal.
-
Show sync status if manifest exists:
---
Sync status: {status}
Where {status} is one of:
NEW -- not yet published (page key not in manifest)
CHANGED -- content differs from published version (content_hash mismatch)
UP TO DATE -- matches published version (content_hash matches)
UNKNOWN -- no manifest (manifest does not exist)
Examples
/nacl-publish docmost-preview entity DE-Order
/nacl-publish docmost-preview uc UC-101
/nacl-publish docmost-preview traceability
Docmost MCP Usage Reference
create_page
mcp__docmost__create_page(
title: "Page Title",
content: "# Markdown Content\n\nBody text...",
spaceId: "019cd479-...",
parentPageId: "019cd6de-..." // optional, omit for top-level
)
Returns: { id: "019cd6f0-...", title: "Page Title", ... }
update_page
mcp__docmost__update_page(
pageId: "019cd6f0-...",
content: "# Updated Content\n\n..."
)
Optionally include title to update the title as well.
list_spaces
mcp__docmost__list_spaces()
Returns list of spaces with their IDs. Use during init to let user pick a space.
list_pages
mcp__docmost__list_pages(
spaceId: "019cd479-..."
)
Returns all pages in a space. Useful for manifest rebuild.
get_page
mcp__docmost__get_page(
pageId: "019cd6f0-..."
)
Returns full page content. Used to verify published content matches.
search
mcp__docmost__search(
query: "search term"
)
Used for finding existing pages during manifest rebuild.
Error Handling
| Situation | Action |
|---|
| Docmost MCP unavailable | ERROR: Docmost MCP not available. Check MCP server connection. |
| Neo4j unavailable | ERROR: + "Neo4j is not reachable at bolt://localhost:{$neo4j_bolt_port}. Tell me "start the graph" (I will run node "$HOME/.claude/skills/nacl-core/scripts/graph-doctor.mjs" --fix) or start it from the project root: docker compose -f graph-infra/docker-compose.yml up -d (local) / ~/.nacl/sidecar/<project_scope>.sh (remote)." |
| Graph is empty | WARNING: Graph is empty. Run /nacl-ba-from-board or seed first. |
| Manifest missing (incremental) | ERROR: No manifest. Run /nacl-publish docmost first. |
| Manifest missing (full) | Proceed with init flow: read config.yaml → docmost.spaces.graph first; only ask the user if config.yaml is empty. |
| Page create fails | Log error, continue with next page, report failures at end. |
| Page update fails (404) | Page was deleted in Docmost. Remove from manifest, re-create. |
| Unknown preview type | ERROR: Unknown type "{type}". Supported: entity, uc, form, uc-index, domain-model, traceability, roles. |
| Node not found in graph | ERROR: {type} "{id}" not found in graph. |
| Content hash unchanged | Skip update (log as "no changes"). |
Excalidraw Boards
Command: /nacl-publish boards
Generate ALL Excalidraw boards from graph data using nacl-render excalidraw logic for each diagram type.
Output directory: {$boards_dir}/ (where $boards_dir is from config.yaml → graph.boards_dir, default: "graph-infra/boards")
Pre-flight Checks
Same as Docmost commands (see Pre-flight Checks section above):
- Neo4j available
- Graph has data
Additionally:
3. Ensure {$boards_dir}/ directory exists. If not, create it.
Workflow
Step 1: Query graph Step 2: Generate boards Step 3: Track & report
── catalog nodes ──── -> ── render excalidraw ────── -> ── summary ────────
Step 1: Catalog All Diagram Sources
Query Neo4j to discover all artifacts that need boards:
// 1a: All DomainEntities (for domain-model board)
MATCH (de:DomainEntity)
RETURN count(de) AS entity_count
// 1b: All Modules (for context-map board)
MATCH (m:Module)
RETURN count(m) AS module_count
// 1c: All UseCases with ActivitySteps (for activity boards)
MATCH (uc:UseCase)-[:HAS_STEP]->(as_step:ActivityStep)
RETURN DISTINCT uc.id AS id, uc.name AS name
ORDER BY uc.id
// 1d: All BusinessProcesses with WorkflowSteps (for process boards)
MATCH (bp:BusinessProcess)-[:HAS_STEP]->(ws:WorkflowStep)
RETURN DISTINCT bp.id AS id, bp.name AS name
ORDER BY bp.id
Step 2: Generate Boards
Generate each board using the corresponding nacl-render excalidraw logic (see nacl-render/SKILL.md for full rendering algorithms, layout constants, and element factories).
2a: Domain Model Board
Use nacl-render excalidraw domain-model logic:
- Query all DomainEntities + attributes + RELATES_TO + enumerations
- Layout as grid of entity cards with arrows
- Output:
{$boards_dir}/domain-model.excalidraw
2b: Context Map Board
Use nacl-render excalidraw context-map logic:
- Query all Modules + DEPENDS_ON + cross-module entity relationships
- Layout as horizontal module boxes with dependency arrows
- Output:
{$boards_dir}/context-map.excalidraw
2c: Activity Boards (one per UseCase)
For each UseCase discovered in Step 1c, use nacl-render excalidraw activity <UC-ID> logic:
- Query ActivitySteps for the UC
- Layout as top-down flowchart with User/System swimlanes
- Output:
{$boards_dir}/activity-{UC-ID}.excalidraw
If a UseCase has no ActivitySteps, skip it and log: SKIP: {UC-ID} -- no activity steps.
2d: BA Process Boards (one per BusinessProcess)
For each BusinessProcess discovered in Step 1d, use nacl-render excalidraw ba-process <BP-ID> logic:
- Query WorkflowSteps with roles, documents
- Layout as horizontal role-swimlane flowchart
- Output:
{$boards_dir}/process-{BP-ID}.excalidraw
If a BusinessProcess has no WorkflowSteps, skip it and log: SKIP: {BP-ID} -- no workflow steps.
Step 3: Track and Report
After all boards are generated, update .docmost-sync.json with a boards section (create the key if absent):
{
"boards": {
"domain-model": {
"file": "{$boards_dir}/domain-model.excalidraw",
"generated_at": "2026-03-20T14:30:00Z",
"element_count": 42,
"source": "all DomainEntities"
},
"context-map": {
"file": "{$boards_dir}/context-map.excalidraw",
"generated_at": "2026-03-20T14:30:00Z",
"element_count": 18,
"source": "all Modules"
},
"activity-UC-101": {
"file": "{$boards_dir}/activity-UC-101.excalidraw",
"generated_at": "2026-03-20T14:30:00Z",
"element_count": 24,
"source": "UC-101"
},
"process-BP-001": {
"file": "{$boards_dir}/process-BP-001.excalidraw",
"generated_at": "2026-03-20T14:30:00Z",
"element_count": 36,
"source": "BP-001"
}
}
}
Save the manifest file.
Report Format
Генерация Excalidraw-бордов завершена:
- Domain Model: domain-model.excalidraw ({N} элементов)
- Context Map: context-map.excalidraw ({N} элементов)
- Activity Diagrams: {UC_count} бордов ({UC-IDs joined})
- BA Process Diagrams: {BP_count} бордов ({BP-IDs joined})
- Пропущено: {skip_count} (нет шагов)
──────────────────────────────────
Итого: {total_boards} бордов, {total_elements} элементов
Директория: {$boards_dir}/
Manifest: .docmost-sync.json обновлён (секция boards)
Generation Order
Generate boards sequentially in this order:
domain-model (single board)
context-map (single board)
activity-{UC-ID} for each UC (alphabetical by UC-ID)
process-{BP-ID} for each BP (alphabetical by BP-ID)
Error Handling for Boards
| Situation | Action |
|---|
| Neo4j unavailable | ERROR: + "Neo4j is not reachable at bolt://localhost:{$neo4j_bolt_port}. Tell me "start the graph" (I will run node "$HOME/.claude/skills/nacl-core/scripts/graph-doctor.mjs" --fix) or start it from the project root: docker compose -f graph-infra/docker-compose.yml up -d (local) / ~/.nacl/sidecar/<project_scope>.sh (remote)." |
| No DomainEntities | Skip domain-model board, log: SKIP: domain-model -- no entities in graph |
| No Modules | Skip context-map board, log: SKIP: context-map -- no modules in graph |
| No UseCases with steps | Skip all activity boards, log: SKIP: activity boards -- no UCs with steps |
| No BusinessProcesses | Skip all process boards, log: SKIP: process boards -- no BPs in graph |
| Individual board generation fails | Log error, continue with next board, report failures at end |
{$boards_dir}/ dir missing | Create it automatically |
Command: /nacl-publish boards-link
Add links to generated Excalidraw boards inside the corresponding Docmost pages. This command reads the boards manifest and the page manifest, determines which board is relevant for each page, and appends a diagram link section to each Docmost page.
Pre-flight Checks
- Manifest exists? Read
.docmost-sync.json.
- If missing ->
ERROR: No manifest found. Run /nacl-publish docmost first.
- Boards section exists? Check
manifest.boards.
- If missing or empty ->
ERROR: No boards generated. Run /nacl-publish boards first.
- Docmost MCP available? Call
mcp__docmost__list_spaces.
- If fails ->
ERROR: Docmost MCP not available.
Workflow
Step 1: Load manifest Step 2: Map pages Step 3: Update pages Step 4: Report
── boards + pages ──── -> ── to boards ──────── -> ── append link ──────── -> ── summary ──
Step 1: Load Manifest
Read .docmost-sync.json and extract:
pages -- map of page keys to {page_id, source_type, source_id}
boards -- map of board keys to {file}
Step 2: Determine Page-to-Board Mapping
For each page in manifest.pages, determine the relevant board file based on source_type and source_id:
| Page source_type | Board key | Board file | Link label |
|---|
entity | domain-model | boards/domain-model.excalidraw | Domain Model |
uc | activity-{source_id} | boards/activity-{source_id}.excalidraw | Activity Diagram: {source_id} |
uc-index | context-map | boards/context-map.excalidraw | Context Map |
domain-model | domain-model | boards/domain-model.excalidraw | Domain Model |
context-map | context-map | boards/context-map.excalidraw | Context Map |
traceability | context-map | boards/context-map.excalidraw | Context Map |
form | domain-model | boards/domain-model.excalidraw | Domain Model |
roles | context-map | boards/context-map.excalidraw | Context Map |
Additionally, for section pages (Архитектура, Domain Model, Use Cases, etc.):
| Section page | Board key | Board file |
|---|
| Архитектура | context-map | boards/context-map.excalidraw |
| Domain Model | domain-model | boards/domain-model.excalidraw |
| Use Cases | context-map | boards/context-map.excalidraw |
Board existence check: Before mapping, verify the board key exists in manifest.boards. If the board was skipped during generation (e.g., no activity steps for a UC), skip linking for that page and log: SKIP link: {page_key} -- board {board_key} not generated.
Step 3: Update Docmost Pages
For each mapped page:
-
Fetch current page content via mcp__docmost__get_page(pageId: page_id).
-
Check if the page already has a diagram link section (search for <!-- excalidraw-link -->).
- If present, replace the existing link section.
- If absent, append the link section at the end.
-
The link section format:
<!-- excalidraw-link -->
---
📊 **Диаграмма:** `boards/{board_filename}.excalidraw`
<!-- /excalidraw-link -->
Where {board_filename} is the filename without path (e.g., domain-model, activity-UC-101, process-BP-001).
For UC pages that also have a related BA process board, append both links:
<!-- excalidraw-link -->
---
📊 **Диаграмма:** `boards/activity-{UC-ID}.excalidraw`
<!-- /excalidraw-link -->
- Update the page via
mcp__docmost__update_page(pageId: page_id, content: updated_content).
Step 4: Report
Встраивание ссылок на борды завершено:
- Обновлено: {updated_count} страниц
- Пропущено: {skipped_count} (борд не сгенерирован)
- Уже со ссылкой: {replaced_count} (ссылка обновлена)
- Manifest: .docmost-sync.json без изменений
Update Pace
Process pages sequentially. For each page:
- GET current content
- Modify content (append or replace link section)
- PUT updated content
This avoids race conditions with Docmost API.
Error Handling for Boards-Link
| Situation | Action |
|---|
| Manifest missing | ERROR: No manifest. Run /nacl-publish docmost first. |
| Boards section missing | ERROR: No boards. Run /nacl-publish boards first. |
| Docmost MCP unavailable | ERROR: Docmost MCP not available. |
| Page not found in Docmost (404) | Log: WARN: Page {page_key} (id: {page_id}) not found in Docmost. Skipping. |
| Board file not in manifest | Skip linking, log: SKIP link: {page_key} -- board not generated |
| Page update fails | Log error, continue with next page, report failures at end |
Command: /nacl-publish full
Complete publishing pipeline: Docmost pages + Excalidraw boards + board links. Runs all three stages sequentially and produces a combined summary report.
Pre-flight Checks
Run all pre-flight checks once at the start (before any step):
- Docmost MCP available? Call
mcp__docmost__list_spaces.
- Neo4j available? Call
mcp__neo4j__read-cypher with RETURN 1.
- Graph has data? Query node counts by label.
If any check fails, abort the entire pipeline with the corresponding error message. Do not proceed to partial execution.
Workflow
Step 1: Docmost Step 2: Boards Step 3: Links Step 4: Report
── markdown → pages ── -> ── graph → .excalidraw ── -> ── links → pages ──── -> ── summary ──
Step 1: Publish Docmost Pages
Execute the full /nacl-publish docmost logic:
- Initialize or load manifest
- Create section pages
- Generate markdown from graph and publish all pages
- Save manifest
Capture results:
docmost_created -- number of pages created
docmost_updated -- number of pages updated
docmost_skipped -- number of pages skipped (unchanged)
Step 2: Generate Excalidraw Boards
Execute the full /nacl-publish boards logic:
- Catalog all diagram sources from graph
- Generate domain-model, context-map, activity, and process boards
- Track boards in manifest
Capture results:
boards_generated -- number of boards created
boards_skipped -- number skipped (no steps/data)
boards_total_elements -- total Excalidraw elements across all boards
Step 3: Embed Board Links
Execute the full /nacl-publish boards-link logic:
- Map pages to boards
- Append diagram link sections to Docmost pages
- Update pages via Docmost MCP
Capture results:
links_added -- number of pages updated with links
links_skipped -- number of pages skipped (board not available)
links_replaced -- number of existing links updated
Step 4: Combined Summary Report
═══════════════════════════════════════════
/nacl-publish full -- Полная публикация
═══════════════════════════════════════════
📄 Docmost (Step 1):
Создано: {docmost_created} страниц
Обновлено: {docmost_updated} страниц
Пропущено: {docmost_skipped} (без изменений)
📊 Excalidraw Boards (Step 2):
Сгенерировано: {boards_generated} бордов
Пропущено: {boards_skipped} (нет данных)
Элементов: {boards_total_elements}
Директория: {$boards_dir}/
🔗 Board Links (Step 3):
Добавлено: {links_added} ссылок
Обновлено: {links_replaced} ссылок
Пропущено: {links_skipped} (борд отсутствует)
───────────────────────────────────────────
Итого: {docmost_created + docmost_updated} страниц в Docmost,
{boards_generated} бордов в {$boards_dir}/,
{links_added + links_replaced} ссылок встроено.
Manifest: .docmost-sync.json обновлён.
═══════════════════════════════════════════
Step Failure Handling
If a step fails partway through:
| Failed Step | Behavior |
|---|
| Step 1 (docmost) fails | Abort pipeline. Report partial results from Step 1. Do not proceed to Steps 2-3. |
| Step 2 (boards) fails partially | Continue with successfully generated boards. Proceed to Step 3 with available boards. Report partial failures. |
| Step 3 (boards-link) fails partially | Continue linking remaining pages. Report partial failures in summary. |
In all cases, save the manifest with whatever state was achieved before the failure. This ensures the next run of /nacl-publish full or any incremental command can pick up where it left off.
Integration with Other Skills
After /nacl-ba-from-board
After importing data into the graph, suggest:
Граф обновлён. Опубликовать в Docmost?
-> /nacl-publish docmost
After /nacl-render
If a user renders an artifact and the manifest exists, suggest incremental sync:
Страница сгенерирована. Обновить в Docmost?
-> /nacl-publish docmost-incremental
After graph edits (any write-cypher)
When another graph skill modifies nodes, suggest incremental sync:
Граф изменён ({N} узлов). Синхронизировать с Docmost?
-> /nacl-publish docmost-incremental