Graph Core — Shared References

SkillDev tools

Shared references, templates, and utilities for all nacl-* skills. Not directly invocable, provides Neo4j connection conventions, schema references, ID format rules, and Excalidraw standards.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Graph Core skill

What this skill tells your AI

The instructions your AI receives, as published by itsalt/nacl in nacl-core/SKILL.md and read by ahel’s review.

This skill is NOT invocable by users. It provides shared references, templates, and utilities for all nacl-* skills.

Neo4j Connection

All graph skills use Neo4j MCP tools:

  • mcp__neo4j__read-cypher — read-only queries
  • mcp__neo4j__write-cypher — create/update/delete
  • mcp__neo4j__get-schema — introspect schema

Connection is managed by the MCP server (configured in .mcp.json at project root). Skills do NOT pass connection strings — they just call MCP tools.

Liveness probe and start/fix tool: nacl-core/scripts/graph-doctor.mjs (node "$HOME/.claude/skills/nacl-core/scripts/graph-doctor.mjs" [--fix]) — reports NACL_GRAPH_DOCTOR: status=UP|DOWN|NOT_NACL, and with --fix starts the local container / relaunches the remote sidecar. Use it in remediation instead of raw docker commands.

Graph Config Resolution (execute at skill start)

Every graph skill MUST read config.yaml at the start to resolve graph settings.

Steps:

  1. Resolve $NACL_PROJECT_ROOT (see Project Root Resolution below) and read config.yaml from there
  2. Extract the graph section
  3. If graph section missing or config.yaml absent → use all defaults
  4. Store resolved values for use in error messages and board paths:
    • $neo4j_bolt_port = graph.neo4j_bolt_port || 3587
    • $neo4j_http_port = graph.neo4j_http_port || 3574
    • $neo4j_password = graph.neo4j_password || "neo4j_graph_dev"
    • $excalidraw_port = graph.excalidraw_port || 3580
    • $boards_dir = graph.boards_dir || "graph-infra/boards"
    • $container_prefix = graph.container_prefix || project.name || "graph"

Configuration Resolution Table

DataSource priority
Neo4j Bolt portgraph.neo4j_bolt_port > fallback 3587
Neo4j HTTP portgraph.neo4j_http_port > fallback 3574
Neo4j passwordgraph.neo4j_password > fallback "neo4j_graph_dev"
Excalidraw portgraph.excalidraw_port > fallback 3580
Boards directorygraph.boards_dir > fallback "graph-infra/boards"
Container prefixgraph.container_prefix > project.name > fallback "graph"

Important:

  • Do NOT pass connection strings to MCP tools — the MCP server handles this
  • These values are ONLY for: error messages, Docker commands, board file paths, Excalidraw URLs
  • When showing error messages, always use resolved values: bolt://localhost:{$neo4j_bolt_port}

Project Root Resolution (worktree-safe)

Claude Code Desktop parallel sessions run in linked git worktrees (<project>/.claude/worktrees/...). Configuration and infrastructure live at the MAIN checkout root, not the worktree root. Resolve the root before reading config.yaml or composing docker commands:

# NaCl project root (worktree-safe) — for config.yaml, graph-infra/, .mcp.json.
# dirname of the absolute common git dir = MAIN checkout root in every case:
# repo root, subdirectory, linked worktree, subdirectory of a worktree.
COMMON=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)  # git >= 2.31
if [ -n "$COMMON" ]; then NACL_PROJECT_ROOT=$(dirname "$COMMON"); else NACL_PROJECT_ROOT=$(pwd); fi

Rule of thumb: configuration and infra (config.yaml, graph-infra/, .mcp.json, .env) resolve against $NACL_PROJECT_ROOT; versioned content (boards, docs, source code) resolves against the checkout you are working in. The JS authority for this resolution is resolveProjectRoot in nacl-core/scripts/graph-doctor.mjs, pinned by its test.

Graph-Down HALT (canonical)

When a graph skill HALTs because Neo4j is unreachable, it MUST use the canonical message below, preceded by the marker comment <!-- nacl-graph-halt -->. The lint gate scripts/check-graph-halt-snippet.sh enforces byte-identical copies across skills — edit here first, then re-sync the copies.

Full form (blockquote HALT sites):

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)

The line after the block is the per-skill tail — one of:

  • > This skill requires Neo4j --- cannot proceed without it. (default)
  • > Cannot proceed with sync. (nacl-ba-sync)

Compact form (table cells and ERROR: print strings — from-board, render, publish):

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).

Concurrent Sessions (Desktop parallel sessions / worktrees)

Parallel Claude Code sessions of one project share ONE graph (one local container or one remote scope). The database layer is safe (Neo4j per-write locking), but semantic lost-updates are possible when two sessions run the same writing BA/SA skill over the same nodes. Known limitation:

  • Run at most one WRITING BA/SA skill at a time per project. Read-only skills and validators are safe in parallel.
  • TL task claiming in remote mode is protected by the claim-lock (nacl-core/scripts/claim-task.mjs).
  • Board files under graph-infra/boards/ are versioned per-checkout: two worktrees syncing the same board into one graph will fight — run /nacl-ba-sync from the main checkout only.

Schema Reference

Schema files: graph-infra/schema/

  • ba-schema.cypher — BA layer (13 node types: ProcessGroup, BusinessProcess, WorkflowStep, BusinessEntity, EntityAttribute, EntityState, BusinessRole, BusinessRule, GlossaryTerm, SystemContext, Stakeholder, ExternalEntity, DataFlow)
  • sa-schema.cypher — SA layer (12 node types)
  • tl-schema.cypher — TL layer (3 node types)

Task.verification_evidence — Taxonomy

Task.verification_evidence is a string property on TL Task nodes that records how a task was verified. It is read by nacl-tl-release (Step 2 pre-merge gate and the final report Evidence-level column) and must be written by every skill that advances a Task to a terminal status. Leaving it NULL on a done task causes the release skill to surface a "Verification gap" — that is a data-integrity failure, not normal output.

Values

ValueWhen to writeWritten by
test-GREEN:<artifact_path>PASS + regression test ran RED→GREEN. <artifact_path> is a repo-relative path to the test file (e.g. apps/web/src/__tests__/funnel.spec.ts) or to the task's regression-test record (.tl/tasks/<TASK_ID>/regression-test.md).conductor Phase 3, tl-full Phase 7, tl-fix terminal step, tl-deliver, tl-hotfix
verify-GREEN:<artifact_path>Workflow-B (infrastructure) PASS: the documented verification command re-ran cleanly after the change and all expected resources were confirmed. <artifact_path> is a repo-relative path to the committed verification record (.tl/tasks/<TASK_ID>/verification.md, written by nacl-tl-dev step B.3.5). Derived from the sub-skill report line Regression test: verification: <path>.conductor Phase 3, tl-full Phase 1 (TECH tasks)
test-UNVERIFIEDCode applied but RED→GREEN not confirmed (no regression test, or sub-skill returned UNVERIFIED). Paired with t.status = 'verified-pending'.Same writers as above
no-testLegacy — no current producer. Was: PASS under the explicit user NO-TEST override; the flag was REMOVED in W4-blocking-release, so no skill writes this value anymore. Readers keep parsing it for graphs written before W4.(none since W4)
null (unset)Reserved for t.status ∈ {'failed', 'blocked'} — those tasks are excluded from release scope, so an evidence string would be misleading.n/a

Format rules

  • Single string, no JSON.
  • test-GREEN and verify-GREEN payloads after : are forward-slash repo-relative paths; no quoting, no scheme.
  • For test-GREEN, use the test file path when one exists, otherwise the .tl/tasks/<TASK_ID>/regression-test.md artifact path.
  • For verify-GREEN, the payload is always the .tl/tasks/<TASK_ID>/verification.md record path.
  • test-UNVERIFIED and no-test carry no payload — exactly those literal strings.

Reader contract

nacl-tl-release parses verification_evidence like this:

  • Prefix test-GREEN: → Evidence level = test-GREEN, path extracted for the report column.
  • Prefix verify-GREEN: → Evidence level = verify-GREEN, path extracted for the report column. NOT a verification gap.
  • Literal test-UNVERIFIED → Evidence level = test-UNVERIFIED.
  • Literal no-test → Evidence level = no-test (legacy graphs only).
  • NULL / empty / unrecognised → Evidence level = unknown → "Verification gap" footer.

Sources: nacl-tl-release/SKILL.md § Output → "Per-UC evidence-level values".

Writer obligation

Every skill in this list MUST set verification_evidence in the same Cypher statement that sets t.status to a terminal value:

  • nacl-tl-conductor — Phase 3 graph-write block (PASS / UNVERIFIED writes).
  • nacl-tl-full — Phase 7 final aggregation graph-write.
  • nacl-tl-fix — terminal graph-write after fix verified.
  • nacl-tl-deliver — stamping after delivery validation.
  • nacl-tl-hotfix — hotfix Task node graph-write.

Skills that only read terminal status (nacl-tl-release, nacl-tl-deploy, nacl-tl-reconcile) do NOT write this field. They may, however, gate on it: if a writer leaves it NULL, an orchestrator gate (e.g. conductor Phase 4) MUST HALT and surface the contract violation.

Graph Skills Registry

BA Layer (14 skills)

SkillPurpose
nacl-ba-contextSystem boundaries → Neo4j
nacl-ba-processBusiness process map → Neo4j
nacl-ba-workflowActivity diagrams → Neo4j
nacl-ba-entitiesEntity catalog → Neo4j
nacl-ba-rolesBusiness roles → Neo4j
nacl-ba-glossaryGlossary → Neo4j
nacl-ba-rulesBusiness rules → Neo4j
nacl-ba-validateL1-L8 + XL1-XL5 validation
nacl-ba-handoffBA→SA traceability
nacl-ba-fullFull BA orchestrator (10 phases)
nacl-ba-from-boardBoard orchestrator (import/sync/enrich/validate/handoff)
nacl-ba-import-docDocument → Excalidraw
nacl-ba-analyzeBoard completeness analysis
nacl-ba-syncBoard → Neo4j sync

SA Layer (9 skills)

SkillPurpose
nacl-sa-architectModule decomposition
nacl-sa-domainDomain model
nacl-sa-rolesRole model + permissions
nacl-sa-ucUC registry + detailing
nacl-sa-uiUI architecture
nacl-sa-validateL1-L13 + XL6-XL9 validation
nacl-sa-featureIncremental feature specification
nacl-sa-finalizeStatistics, ADR, readiness
nacl-sa-fullFull SA orchestrator (10 phases)

TL Layer (7 skills)

SkillPurpose
nacl-tl-planTask planning from graph
nacl-tl-intakeGraph-aware triage
nacl-tl-statusStatus + SA coverage
nacl-tl-nextNext task + SA context
nacl-tl-fullFull lifecycle orchestrator
nacl-tl-conductorBatch process manager
nacl-tl-hotfixEmergency hotfix to main

Output (2 skills)

SkillPurpose
nacl-renderGraph → Markdown + Excalidraw
nacl-publishGraph → Docmost

Query library: graph-infra/queries/

  • ba-queries.cypher, sa-queries.cypher, handoff-queries.cypher, validation-queries.cypher, tl-queries.cypher

Excalidraw File-Based Workflow

Board files stored in: {$boards_dir}/*.excalidraw (where $boards_dir is resolved from config.yaml → graph.boards_dir, default: graph-infra/boards)

AI reads/writes .excalidraw JSON directly (no MCP server needed).

Excalidraw JSON Format

{
  "type": "excalidraw",
  "version": 2,
  "elements": [...],
  "appState": {"viewBackgroundColor": "#ffffff"}
}

Element Types Used

BA ConceptExcalidraw TypebackgroundColorcustomData.nodeType
WorkflowStep (бизнес-функция)rectangle#e8f5e9 (green)WorkflowStep
WorkflowStep (автоматизируется)rectangle#e3f2fd (blue)WorkflowStep
Decisiondiamond#fff3e0 (orange)Decision
BusinessEntity / Documentrectangle#f3e5f5 (purple)BusinessEntity
BusinessRole (swimlane label)rectangle#fafafa (grey)BusinessRole
Annotation / Notetext—Annotation
Connectionarrow——

Color Coding for Confidence

ConfidencestrokeColorMeaning
High (confirmed)#2e7d32 (green)Clearly identified in source
Medium (assumption)#f57f17 (amber)AI inferred from context
Low (needs info)#c62828 (red)Missing information

customData Structure

Every shape element MUST have customData:

{
  "customData": {
    "nodeId": "BP-001-S03",       // Neo4j node id (null if not synced)
    "nodeType": "WorkflowStep",   // Graph node label
    "confidence": "high",         // high | medium | low
    "sourceDoc": "process.docx",  // source document (if from import)
    "sourcePage": 3,              // page number in source
    "synced": false               // true after /nacl-ba-sync
  }
}

Excalidraw Element Template (rectangle with text)

To create a labeled rectangle, you need TWO elements:

  1. The shape (rectangle/diamond) with boundElements pointing to text
  2. The text element with containerId pointing back to shape
{
  "id": "rect-001",
  "type": "rectangle",
  "x": 100, "y": 200,
  "width": 200, "height": 60,
  "strokeColor": "#2e7d32",
  "backgroundColor": "#e3f2fd",
  "fillStyle": "solid",
  "strokeWidth": 2,
  "strokeStyle": "solid",
  "roughness": 1,
  "opacity": 100,
  "angle": 0,
  "seed": 12345,
  "version": 1,
  "versionNonce": 1,
  "isDeleted": false,
  "groupIds": [],
  "frameId": null,
  "boundElements": [{"id": "text-001", "type": "text"}],
  "updated": 1,
  "link": null,
  "locked": false,
  "customData": {
    "nodeId": "BP-001-S03",
    "nodeType": "WorkflowStep",
    "confidence": "high",
    "synced": false
  }
}

Layout Guidelines

  • Swimlanes: horizontal bands, 200px height each
  • Steps: left-to-right flow, 220px spacing
  • Documents: right column, 50px margin from steps
  • Decisions: centered between alternative paths
  • Arrows: use startBinding/endBinding for connected arrows

Board Meta Sidecar (<board>.meta.json)

Every .excalidraw board file may have a companion sidecar file at {$boards_dir}/<board>.meta.json. This file is the source of truth for "when was this board last generated from the graph" and "when was it last synced back". The NaCl Analyst Tool (analyst-tool/) reads the sidecar to display sync status in its sidebar tree — without the sidecar the tool cannot determine freshness. Skills are the canonical writers of this file; the tool only reads it (and may write optimistically as a fallback when a skill run fails partway through).

Schema

{
  "lastGeneratedAt":        "2026-05-03T18:42:00.000Z",
  "lastGeneratedBy":        "nacl-render",
  "lastSyncedAt":           "2026-05-03T19:00:00.000Z",
  "lastSyncStatus":         "ok",
  "lastSyncRunId":          "r-95f71b58",
  "contentHashAtLastSync":  "sha256:<64-hex-chars>"
}

All six fields are nullable (null when not yet set). Dates are ISO-8601 UTC strings.

Field Semantics

FieldTypeWriterNull means
lastGeneratedAtISO-8601 string | nullnacl-renderboard was never generated from graph
lastGeneratedBystring | nullnacl-renderboard was never generated from graph
lastSyncedAtISO-8601 string | nullnacl-ba-sync (success only)board was never synced to graph
lastSyncStatus"ok" | "failed" | nullnacl-ba-syncno sync has been attempted
lastSyncRunIdstring | nullnacl-ba-syncinvoked directly (not via analyst-tool), or no run ID available
contentHashAtLastSync"sha256:<hex>" | nullnacl-render and nacl-ba-sync (success only)no render or sync has completed

Who Writes What

ActionFields written
Render from graph (success)lastGeneratedAt, lastGeneratedBy, contentHashAtLastSync
Sync to graph (success)lastSyncedAt, lastSyncStatus = "ok", lastSyncRunId, contentHashAtLastSync
Sync to graph (failure)lastSyncStatus = "failed", lastSyncRunId
Analyst-tool savenothing — the tool does not write meta; hasUnsyncedEdits is derived at read time from mtime + recomputed hash

Content Hash Algorithm

contentHashAtLastSync is a SHA-256 hash of a normalized representation of the board scene. The algorithm (canonical TypeScript implementation: analyst-tool/server/src/services/meta.ts, function computeBoardHash):

  1. Extract three top-level fields from the .excalidraw JSON: elements (array), appState (object), files (object).
  2. Normalize appState: keep only viewBackgroundColor and gridSize; all other keys are dropped. Missing keys default to null.
  3. Normalize each element in elements:
    • Strip the following volatile per-element keys: version, versionNonce, seed, updated.
    • Sort the remaining keys alphabetically (ascending).
    • Produce a new object with only the sorted, non-volatile keys.
  4. Assemble the normalized scene object:
    {
      "elements": [ ...normalized elements... ],
      "appState": { "viewBackgroundColor": <value or null>, "gridSize": <value or null> },
      "files": <files object or {}>
    }
    
  5. Serialize with JSON.stringify (no extra whitespace, no key sorting at the top level — the top-level key order is elements, appState, files as shown above).
  6. Hash the UTF-8 bytes of the JSON string with SHA-256.
  7. Prefix the lowercase hex digest with sha256:.

Result format: "sha256:<64 lowercase hex characters>".

Any re-implementation (Python, shell, etc.) must produce the same byte sequence as the TypeScript reference for identical input. The key sort in step 3 is per-element, not recursive — nested objects inside element properties are not sorted.

Atomic Write Requirement

To prevent the analyst-tool fs-watcher from reading a half-written file, the sidecar must be written atomically:

  1. Write the new JSON content to <board>.meta.json.tmp (same directory as the board).
  2. Rename <board>.meta.json.tmp → <board>.meta.json.

On POSIX filesystems rename(2) is atomic. On Windows the tool is expected to run on macOS/Linux so this guarantee holds.

Merge Rule

A skill never blindly overwrites the entire sidecar. Before writing, it reads the existing sidecar (or uses all-null defaults if missing), merges only the fields it owns (per the "Who Writes What" table above), and writes the merged result. This ensures nacl-render does not destroy lastSyncedAt set by a previous nacl-ba-sync run, and vice versa.

Analyst Tool Integration

The meta sidecar is also read by the local NaCl Analyst Tool (analyst-tool/), which uses it to display sync status in the sidebar tree. The tool writes meta optimistically as a fallback in case a skill run fails partway, but the skills themselves are the canonical writers per the table above.


Project Initialization

The /nacl-init skill creates or updates CLAUDE.md, config.yaml, and graph infrastructure for a project. Re-running /nacl-init on an existing project is idempotent and triggers automatic migration of legacy infrastructure (removing stale excalidraw containers, creating missing graph-infra/boards/, injecting missing project.id / project.name fields) — see nacl-init/SKILL.md § Auto-migrate Legacy Artefacts for details.


Project Registry (~/.nacl/projects.json)

The NaCl Analyst Tool discovers projects through a per-user registry at ~/.nacl/projects.json (override via NACL_HOME env var — see docs/configuration.md). The registry is written only by nacl-init (Step 2d); the analyst-tool and all other skills only read it. The canonical TypeScript types (ProjectRecord, ProjectRegistry) and the atomic-write implementation live in analyst-tool/server/src/services/project-registry.ts. Every entry has the fields id, name, root, createdAt, and lastUsed; the top-level object carries version: 1 and activeProjectId. Skills must never write to this file directly — invoke /nacl-init to register or refresh a project.


ID Generation Rules

LayerFormatExampleCounter
BA Process GroupGPR-NNGPR-01Global sequential
BA ProcessBP-NNNBP-001Global sequential
BA Workflow Step{BP}-S{NN}BP-001-S03Per-process
BA EntityOBJ-NNNOBJ-001Global sequential
BA Entity Attribute{OBJ}-A{NN}OBJ-001-A01Per-entity
BA Entity State{OBJ}-ST{NN}OBJ-001-ST01Per-entity
BA RoleROL-NNROL-01Global sequential
BA RuleBRQ-NNNBRQ-001Global sequential
BA GlossaryGLO-NNNGLO-001Global sequential
System ContextSYS-NNNSYS-001Global sequential
StakeholderSTK-NNSTK-01Global sequential
External EntityEXT-NNEXT-01Global sequential
Data FlowDFL-NNNDFL-001Global sequential
Decision (provenance)DEC-NNNDEC-001Global sequential
ScreenSCR-{PascalName}SCR-ResultViewerName-based
Screen StateSCRST-{Screen}-{State}SCRST-ResultViewer-LoadingPer-screen, name-based
Screen EventSCREV-{Screen}-{Event}SCREV-ResultViewer-OnRetryPer-screen, name-based
Screen TransitionSCRTR-{Screen}-NNNSCRTR-ResultViewer-001Per-screen sequential
Screen EffectSCREF-{Screen}-NNNSCREF-ResultViewer-001Per-screen sequential
Analytics EventANEV-{Name}ANEV-ResultViewedName-based
Behavior SliceSLC-{NNN}-{PascalName}SLC-006-HappyPathPer-UC, name-based (latin)
Domain ErrorERR-{UPPER_SNAKE_CODE}ERR-PROMO_NOT_FOUNDCode-based (latin)
Error PresentationERRP-{CODE}-{PascalName}ERRP-PROMO_NOT_FOUND-InlinePer-error, kind/context-based
Cache PolicyCACHE-{PascalName}CACHE-ResultMediaIndexedDbName-based (latin)
Degradation RuleDEG-{NNN}-{PascalName}DEG-006-OfflineRestorePer-UC, name-based (latin)

Screen state machine ids (SCR-* family) belong to the SA screen state machine written by nacl-sa-ui state-machine (labels :Screen, :ScreenState, :ScreenEvent, :Transition (reified), :ScreenEffect, :AnalyticsEvent). {Screen} in child ids is the PascalName part of the Screen id without the SCR- prefix. See graph-infra/schema/sa-schema.cypher § 3-bis and the sa_screen_machine named query.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
27
Forks
4
Last commit
Sep 2026

ahel review

  • K6info
    bundled executables the agent is told to run

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Item type
skill
Key
nacl-core
Source
github.com/itsalt/nacl