/nacl-migrate-sa — SA Markdown → Neo4j

SkillDocs & knowledge

Migrate an OLD-methodology SA layer from Markdown to Neo4j. Parses docs/10-architecture (or 00-architecture), docs/12-domain (or 02-domain), docs/13-roles (or 03-roles), docs/14-usecases (or 04-usecases), docs/15-interfaces (or 05-interfaces), docs/16-requirements (or 06-requirements), plus docs/99-meta/traceability-matrix.md. Writes SA nodes, SA-internal relationships, and cross-layer handoff edges via deterministic Python scripts, executed through mcp__neo4j__write-cypher. Use when: migrate SA from markdown, import old SA into graph, nacl-migrate-sa. Fails loudly on unknown formats.

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 /nacl-migrate-sa skill

What this skill tells your AI

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

Role

You delegate SA parsing, validation, and Cypher generation to stdlib Python scripts under $NACL_HOME/nacl-migrate-sa/scripts/. You never parse Markdown yourself; the scripts own that. Your job is:

  1. Run the scripts in order.
  2. Read their JSON outputs.
  3. Execute the Cypher plan against Neo4j via mcp__neo4j__write-cypher.
  4. Gather live counts via mcp__neo4j__read-cypher and hand them to audit_sa.py.
  5. Present results and errors to the user in plain language.

If the project has a BA layer, nacl-migrate-ba must have already run — this skill emits cross-layer handoff edges that point at BA nodes.


Invocation

/nacl-migrate-sa [project_path] [--dry-run] [--adapter=<name>] [--no-ba]
ParameterRequiredDescription
project_pathNoProject root (default: cwd).
--dry-runNoParse + validate + Cypher gen; no Neo4j writes, no audit.
--adapterNoForce adapter (inline-table-v1). Default: auto-detect.
--no-baNoSkip cross-layer handoff edges. Use for SA-only projects.

Prelude — locate NaCl home and verify Python

# Resolve $NACL_HOME
if [ -z "$NACL_HOME" ]; then
  for candidate in "$HOME/projects/NaCl" "$HOME/NaCl" "$HOME/code/NaCl" "$HOME/src/NaCl"; do
    if [ -f "$candidate/nacl-migrate-core/nacl_migrate_core/__init__.py" ]; then
      export NACL_HOME="$candidate"
      break
    fi
  done
fi
if [ -z "$NACL_HOME" ] || [ ! -f "$NACL_HOME/nacl-migrate-core/nacl_migrate_core/__init__.py" ]; then
  echo "Unable to locate NaCl repo. Set NACL_HOME=/path/to/NaCl and retry." >&2
  exit 1
fi

python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3,11) else 1)' 2>/dev/null \
  || { echo "Python 3.11+ required. On macOS: brew install python@3.11"; exit 1; }

Soft preflight (direct invocation)

When invoked directly (i.e. not via the /nacl-migrate orchestrator), run an ID-pattern scan and warn — but do not block — if any token in the project's filenames or YAML frontmatter does not match a known adapter pattern. The orchestrator path runs the same scan at Phase A.5 with a hard gate; the direct-invocation path stays non-blocking so a single-layer rerun is ergonomic.

mkdir -p .nacl-migrate
python3 "$NACL_HOME/nacl-migrate-ba/scripts/preflight_ids.py" \
  --project "$PWD" \
  --output .nacl-migrate/preflight.json || true

Exit code 1 from the script means unknown patterns were found. Surface the report's patterns_unknown list to the user:

Preflight surfaced N unknown ID-shaped tokens. These will likely be dropped at parse. Review .nacl-migrate/preflight.json and either widen an adapter (recommended) or pass --force to proceed anyway.

If the user passes --force, continue. Otherwise stop.


Preconditions

  1. config.yaml / .mcp.json present; Neo4j reachable via mcp__neo4j__get-schema.
  2. At least one SA folder exists under docs/ (10-16 or 00-06 variant). If none: "No SA layer detected. Skipping nacl-migrate-sa." Exit cleanly.
  3. Unless --no-ba: BA nodes present in Neo4j (query MATCH (bp:BusinessProcess) RETURN count(bp) > 0). If zero AND the project has a BA docs tree, stop — user must run /nacl-migrate-ba first.

Phase 0 — Detect adapter + numbering

The SA parser infers both at once. For now there is a single SA adapter (inline-table-v1), which auto-detects 10-16 vs 00-06 numbering and handles an optional frontmatter fallback for UC files.

Skip this phase when --adapter was passed explicitly.


Phase 1 — Parse SA markdown → SaIR + HandoffIR

python3 "$NACL_HOME/nacl-migrate-sa/scripts/parse_sa.py" \
  --project "$PWD" \
  --adapter inline-table-v1 \
  --output .nacl-migrate/sa-ir.json \
  --handoff-output .nacl-migrate/handoff-ir.json

Expected stdout: counts table for SaIR and HandoffIR. Surface every warning code to the user.

On error exit, print message + remediation verbatim.


Phase 2 — Validate IR

python3 "$NACL_HOME/nacl-migrate-sa/scripts/validate_sa_ir.py" \
  --input .nacl-migrate/sa-ir.json \
  --handoff .nacl-migrate/handoff-ir.json \
  --output .nacl-migrate/sa-validation.json

13 referential checks (SV1–SV8 + HV1–HV5). Exit 0 → all pass. Exit 1 → stop and print the failure list.

The script also prints a Coverage section (SC1–SC7) and writes a coverage block to sa-validation.json. This is the completeness dimension — it measures how much of each node type the adapter actually populated (UC activity steps, UC module, UC↔form links, entity attributes, enum values, form fields). It exists because the referential checks and the Phase 7 audit are both blind to under-extraction: an IR with 1 ActivityStep total is internally consistent and writes cleanly, yet leaves nearly every UseCase an empty shell.

Coverage is advisory by default (it does not change the exit code — some emptiness is legitimate, e.g. a pure list-view UC has no steps). Surface the Coverage lines to the user verbatim; do not bury them. A severely under-extracted migration now shows e.g. SC1 UC activity steps: 1/50 (2.0%) instead of a silent "clean".

To gate in CI, add --strict (fail any metric below 100%) or --min-coverage PCT (fail below PCT). These flip the exit code to non-zero; leave them off for the normal interactive migration.


Phase 3 — Generate Cypher plan

python3 "$NACL_HOME/nacl-migrate-sa/scripts/generate_sa_cypher.py" \
  --input .nacl-migrate/sa-ir.json \
  --handoff .nacl-migrate/handoff-ir.json \
  --output .nacl-migrate/sa-cypher.json

Three batch kinds: node, edge (SA-internal), handoff (BA↔SA). If --no-ba was set, skip all handoff batches when executing.

If --dry-run: stop here and report the plan summary.


Phase 4 — Execute Cypher against Neo4j

Preflight: mcp__neo4j__get-schema() must succeed.

Read .nacl-migrate/sa-cypher.json. For each batch:

mcp__neo4j__write-cypher(query = batch.cypher, params = batch.params)

Report progress per batch: [i/N] {label} rows=... ok.

If --no-ba, skip batches where kind == "handoff" and log a single line: Skipping N handoff batches (--no-ba).

On any batch failure: log the batch index + error; stop. The partial graph is MERGE-idempotent — rerunning after a fix is safe.


Phase 5 — SUGGESTS edges (optional, best-effort)

Emit ProcessGroup -[:SUGGESTS]-> Module edges from in-graph data. This is the only SA edge type that can't be computed by the scripts because it requires joining SA's Module.related_process_ids with BA's ProcessGroup -[:CONTAINS]-> BusinessProcess. Run via MCP:

MATCH (gpr:ProcessGroup)-[:CONTAINS]->(bp:BusinessProcess)
MATCH (m:Module)
WHERE bp.id IN m.related_process_ids
MERGE (gpr)-[:SUGGESTS]->(m)
RETURN count(*) AS n

Skip entirely if --no-ba.


Phase 6 — Gather live counts

Query Neo4j via mcp__neo4j__read-cypher for every node label and edge type listed in audit_sa.py. Important: scope HAS_STEP, HAS_ATTRIBUTE, and NEXT_STEP counts to the SA layer, because those relationship types also exist on the BA layer. Use label-scoped Cypher:

MATCH (:UseCase)-[r:HAS_STEP]->(:ActivityStep) RETURN count(r) AS n
MATCH (:DomainEntity)-[r:HAS_ATTRIBUTE]->(:DomainAttribute) RETURN count(r) AS n
MATCH (:ActivityStep)-[r:NEXT_STEP]->(:ActivityStep) RETURN count(r) AS n

All other edge types have SA-exclusive labels and can use the unscoped form.

Write the counts to .nacl-migrate/sa-live-counts.json.


Phase 7 — Audit

python3 "$NACL_HOME/nacl-migrate-sa/scripts/audit_sa.py" \
  --ir .nacl-migrate/sa-ir.json \
  --handoff .nacl-migrate/handoff-ir.json \
  --counts .nacl-migrate/sa-live-counts.json \
  --output .nacl-migrate/sa-audit.json

Exit 0 → "All SA counts match." Exit 1 → surface mismatches to the user.

The audit prints a pointer line — count parity ✓ — see validation coverage (SC1–SC7 …) — because count parity only proves IR→graph fidelity, never extraction completeness. When you report the audit result to the user, quote the Phase-2 Coverage numbers next to it; never present "All SA counts match" as if it meant the migration is complete.


Phase 7b — Backfill validation exemption flags

Migrated graphs are populated from markdown documents that predate the L4–L7 validators. Their nodes lack the has_ui / system_only / shared / internal / field_category exemption properties, which causes nacl-sa-validate to emit dozens of false-positive findings on the very first run.

To prevent this, the migration ends with an automatic backfill via nacl-sa-flags:

/nacl-sa-flags backfill-all --detect-internal

Behavior:

  • :UseCase.has_ui is derived deterministically: true if (uc)-[:USES_FORM]->(:Form), else false.
  • :DomainAttribute.internal is auto-flagged for system patterns (id, *_id, *_at, *_token, *_hash).
  • :SystemRole.system_only, :DomainEntity.shared, :FormField.field_category get safe defaults (false, false, 'input').

The user is prompted at the end of Phase 7 to confirm before the backfill runs. If declined, the migration completes without flags and nacl-sa-validate full will surface ~50–150 NULL-property findings as recommendations rather than CRITICAL/WARN; the user can then run nacl-sa-flags backfill-all manually at any later time.

Why --detect-internal by default: post-migration graphs have many surrogate keys and timestamps. The heuristic flags them automatically and is safe (it only sets internal=true, never internal=false, so manually-flagged user-facing attributes are preserved). Real telemetry / provider-internal columns may need additional manual marking after the first validate run.


Phase 8 — Report

Write MIGRATION-REPORT-SA.md at project root:

# SA Migration Report

**Project:** {project_path}
**Adapter:** {adapter}
**Numbering:** {numbering}  (10-16 or 00-06)
**Ran at:** {ISO timestamp}

## SA IR vs live Neo4j
(Copy from sa-audit.json — node + edge tables.)

**Audit result: N/N CLEAN** — but count parity only proves IR→graph fidelity.
See Completeness / Coverage below for whether the IR itself is complete.

## Completeness / Coverage
(Copy the `coverage` block from sa-validation.json. One line per metric, kept
adjacent to the audit headline so "CLEAN" is never read as "complete".)

| Metric | Covered | % | Sample missing |
|--------|---------|---|----------------|
| SC1 UC activity steps | {covered}/{total} | {pct}% | {sample_missing} |
| SC2 UC module | {covered}/{total} | {pct}% | … |
| SC3 UC→form link | {covered}/{total} | {pct}% | … |
| SC3f Form→UC link | {covered}/{total} | {pct}% | … |
| SC4 DomainEntity module | {covered}/{total} | {pct}% | … |
| SC5 DomainEntity attributes | {covered}/{total} | {pct}% | … |
| SC6 Enumeration values | {covered}/{total} | {pct}% | … |
| SC7 Form fields | {covered}/{total} | {pct}% | … |

Coverage is advisory: low percentages flag adapter under-extraction (or
legitimately empty nodes), not a write failure. Investigate any metric well
below 100% before declaring the migration done.

## Cross-layer handoff
(Counts of AUTOMATES_AS, REALIZED_AS, TYPED_AS, MAPPED_TO, IMPLEMENTED_BY, SUGGESTS.)

## Warnings
{list from sa-ir.json warnings[] and handoff-ir.json, or "none"}

## Non-extracted relationships
The following are currently 0 because the adapter does not extract them:
  - FormField / HAS_FIELD / MAPS_TO (needs _form-domain-mapping.md parser)
  - ActivityStep.NEXT_STEP (needs UC flowchart Mermaid parser)
  - UseCase -[:ACTOR]-> SystemRole
  - SystemRole -[:HAS_PERMISSION]-> DomainEntity
  - BusinessRule -[:IMPLEMENTED_BY]-> Requirement (matrix uses category names, not REQ ids)

Adding any of these is a per-adapter enhancement. Non-blocker for migration.

## Next steps
- Run `/nacl-sa-validate` (L1-L13 + XL6-XL9 cross-validation; L10-L13 pass vacuously until the project adopts the 2.15+ extension layers — see `docs/runbooks/upgrade-graph-extensions.md`)
- Run `/nacl-tl-diagnose` for code-docs drift analysis

Idempotency

All Neo4j writes are MERGE-keyed on canonical IDs. Safe to rerun. If an adapter change produces different canonical IDs, old nodes become orphans — audit_sa.py flags this as a count mismatch.


Known non-blockers

The current adapter does not extract:

  • FormField / HAS_FIELD / MAPS_TO — requires parsing _form-domain-mapping.md.
  • ActivityStep.NEXT_STEP — UC activity flows are text tables without explicit step-number links.
  • UseCase -[:ACTOR]-> SystemRole — UC.actor is text ("ACT-01 Пользователь"); resolving it to a SYSROL requires the SystemRole table to be loaded first.
  • SystemRole -[:HAS_PERMISSION]-> DomainEntity — requires parsing the permissions matrix.
  • BusinessRule -[:IMPLEMENTED_BY]-> Requirement — traceability matrix maps BRQ to category strings, not REQ IDs. Warning: HANDOFF_REQ_CATEGORY is logged per row.

These can be added as adapter enhancements later. None is blocking for migration correctness.


Error handling

SituationResponse
Prelude can't find $NACL_HOMEStop. Ask to export NACL_HOME.
Python < 3.11Stop. Ask user to upgrade.
BA docs present but BA nodes missingStop. Ask user to run /nacl-migrate-ba first.
Ambiguous numbering (both 10-16 and 00-06 dirs present)Stop. Ask user to remove one.
Adapter unknownStop. Present supported adapters.
Validation failureStop. Print failed checks.
Batch failureStop after the failing batch. Rerun after fix.
Audit mismatchReport, do not claim success.

Reads / Writes

Reads

  • {project}/docs/{10-16|00-06}-*/**/*.md
  • {project}/docs/99-meta/traceability-matrix.md
  • mcp__neo4j__read-cypher (live counts in Phase 6)
  • mcp__neo4j__get-schema (preflight)

Writes

  • .nacl-migrate/sa-ir.json
  • .nacl-migrate/handoff-ir.json
  • .nacl-migrate/sa-validation.json
  • .nacl-migrate/sa-cypher.json
  • .nacl-migrate/sa-live-counts.json
  • .nacl-migrate/sa-audit.json
  • MIGRATION-REPORT-SA.md
  • mcp__neo4j__write-cypher (Phases 4, 5)

Checklist

  • Prelude: $NACL_HOME resolved, Python 3.11+ present
  • Phase 0: adapter + numbering detected (or user supplied)
  • Phase 1: SA IR + Handoff IR emitted
  • Phase 2: validation 13/13 pass (or blocker list surfaced) + Coverage (SC1–SC7) reported
  • Phase 3: Cypher plan generated
  • Phase 4: every batch executed via mcp__neo4j__write-cypher
  • Phase 5: SUGGESTS edges emitted (or skipped with --no-ba)
  • Phase 6: live counts gathered (layer-scoped for shared rel types)
  • Phase 7: audit clean
  • Phase 8: MIGRATION-REPORT-SA.md written

Signals

GitHub stars
27
Forks
4
Last commit
Sep 2026

ahel review

  • K1binfo
    installs-packages

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

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