--- name: nacl-core description: | Shared references, templates, and utilities for all nacl-* skills. Not directly invocable — provides Neo4j connection conventions, schema references, ID format rules, and Excalidraw standards. --- # Graph Core — Shared References 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-home)/nacl-core/scripts/graph-doctor.mjs` (`node "$(nacl-home)/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 | Data | Source priority | |------|---------------| | Neo4j Bolt port | `graph.neo4j_bolt_port` > fallback `3587` | | Neo4j HTTP port | `graph.neo4j_http_port` > fallback `3574` | | Neo4j password | `graph.neo4j_password` > fallback `"neo4j_graph_dev"` | | Excalidraw port | `graph.excalidraw_port` > fallback `3580` | | Boards directory | `graph.boards_dir` > fallback `"graph-infra/boards"` | | Container prefix | `graph.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 (`/.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: ```bash # 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-home)/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 ``. 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 "$(nacl-home)/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/.sh` (Windows: `%USERPROFILE%\.nacl\sidecar\.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 "$(nacl-home)/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/.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-home)/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 | Value | When to write | Written by | |---|---|---| | `test-GREEN:` | PASS + regression test ran RED→GREEN. `` 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//regression-test.md`). | conductor Phase 3, tl-full Phase 7, tl-fix terminal step, tl-deliver, tl-hotfix | | `verify-GREEN:` | Workflow-B (infrastructure) PASS: the documented verification command re-ran cleanly after the change and all expected resources were confirmed. `` is a repo-relative path to the committed verification record (`.tl/tasks//verification.md`, written by `nacl-tl-dev` step B.3.5). Derived from the sub-skill report line `Regression test: verification: `. | conductor Phase 3, tl-full Phase 1 (TECH tasks) | | `test-UNVERIFIED` | Code 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-test` | **Legacy — 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//regression-test.md` artifact path. - For `verify-GREEN`, the payload is always the `.tl/tasks//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) | Skill | Purpose | |-------|---------| | nacl-ba-context | System boundaries → Neo4j | | nacl-ba-process | Business process map → Neo4j | | nacl-ba-workflow | Activity diagrams → Neo4j | | nacl-ba-entities | Entity catalog → Neo4j | | nacl-ba-roles | Business roles → Neo4j | | nacl-ba-glossary | Glossary → Neo4j | | nacl-ba-rules | Business rules → Neo4j | | nacl-ba-validate | L1-L8 + XL1-XL5 validation | | nacl-ba-handoff | BA→SA traceability | | nacl-ba-full | Full BA orchestrator (10 phases) | | nacl-ba-from-board | Board orchestrator (import/sync/enrich/validate/handoff) | | nacl-ba-import-doc | Document → Excalidraw | | nacl-ba-analyze | Board completeness analysis | | nacl-ba-sync | Board → Neo4j sync | ### SA Layer (9 skills) | Skill | Purpose | |-------|---------| | nacl-sa-architect | Module decomposition | | nacl-sa-domain | Domain model | | nacl-sa-roles | Role model + permissions | | nacl-sa-uc | UC registry + detailing | | nacl-sa-ui | UI architecture | | nacl-sa-validate | L1-L13 + XL6-XL9 validation | | nacl-sa-feature | Incremental feature specification | | nacl-sa-finalize | Statistics, ADR, readiness | | nacl-sa-full | Full SA orchestrator (10 phases) | ### TL Layer (7 skills) | Skill | Purpose | |-------|---------| | nacl-tl-plan | Task planning from graph | | nacl-tl-intake | Graph-aware triage | | nacl-tl-status | Status + SA coverage | | nacl-tl-next | Next task + SA context | | nacl-tl-full | Full lifecycle orchestrator | | nacl-tl-conductor | Batch process manager | | nacl-tl-hotfix | Emergency hotfix to main | ### Output (2 skills) | Skill | Purpose | |-------|---------| | nacl-render | Graph → Markdown + Excalidraw | | nacl-publish | Graph → 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 ```json { "type": "excalidraw", "version": 2, "elements": [...], "appState": {"viewBackgroundColor": "#ffffff"} } ``` ### Element Types Used | BA Concept | Excalidraw Type | backgroundColor | customData.nodeType | |---|---|---|---| | WorkflowStep (бизнес-функция) | rectangle | #e8f5e9 (green) | WorkflowStep | | WorkflowStep (автоматизируется) | rectangle | #e3f2fd (blue) | WorkflowStep | | Decision | diamond | #fff3e0 (orange) | Decision | | BusinessEntity / Document | rectangle | #f3e5f5 (purple) | BusinessEntity | | BusinessRole (swimlane label) | rectangle | #fafafa (grey) | BusinessRole | | Annotation / Note | text | — | Annotation | | Connection | arrow | — | — | ### Color Coding for Confidence | Confidence | strokeColor | Meaning | |---|---|---| | 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: ```json { "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 ```json { "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 (`.meta.json`) Every `.excalidraw` board file may have a companion sidecar file at `{$boards_dir}/.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 ```json { "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 | Field | Type | Writer | Null means | |---|---|---|---| | `lastGeneratedAt` | ISO-8601 string \| null | `nacl-render` | board was never generated from graph | | `lastGeneratedBy` | string \| null | `nacl-render` | board was never generated from graph | | `lastSyncedAt` | ISO-8601 string \| null | `nacl-ba-sync` (success only) | board was never synced to graph | | `lastSyncStatus` | `"ok"` \| `"failed"` \| null | `nacl-ba-sync` | no sync has been attempted | | `lastSyncRunId` | string \| null | `nacl-ba-sync` | invoked directly (not via analyst-tool), or no run ID available | | `contentHashAtLastSync` | `"sha256:"` \| null | `nacl-render` and `nacl-ba-sync` (success only) | no render or sync has completed | ### Who Writes What | Action | Fields 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 save | nothing — 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: ```json { "elements": [ ...normalized elements... ], "appState": { "viewBackgroundColor": , "gridSize": }, "files": } ``` 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 `.meta.json.tmp` (same directory as the board). 2. Rename `.meta.json.tmp` → `.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 | Layer | Format | Example | Counter | |---|---|---|---| | BA Process Group | GPR-NN | GPR-01 | Global sequential | | BA Process | BP-NNN | BP-001 | Global sequential | | BA Workflow Step | {BP}-S{NN} | BP-001-S03 | Per-process | | BA Entity | OBJ-NNN | OBJ-001 | Global sequential | | BA Entity Attribute | {OBJ}-A{NN} | OBJ-001-A01 | Per-entity | | BA Entity State | {OBJ}-ST{NN} | OBJ-001-ST01 | Per-entity | | BA Role | ROL-NN | ROL-01 | Global sequential | | BA Rule | BRQ-NNN | BRQ-001 | Global sequential | | BA Glossary | GLO-NNN | GLO-001 | Global sequential | | System Context | SYS-NNN | SYS-001 | Global sequential | | Stakeholder | STK-NN | STK-01 | Global sequential | | External Entity | EXT-NN | EXT-01 | Global sequential | | Data Flow | DFL-NNN | DFL-001 | Global sequential | | Decision (provenance) | DEC-NNN | DEC-001 | Global sequential | | Screen | SCR-{PascalName} | SCR-ResultViewer | Name-based | | Screen State | SCRST-{Screen}-{State} | SCRST-ResultViewer-Loading | Per-screen, name-based | | Screen Event | SCREV-{Screen}-{Event} | SCREV-ResultViewer-OnRetry | Per-screen, name-based | | Screen Transition | SCRTR-{Screen}-NNN | SCRTR-ResultViewer-001 | Per-screen sequential | | Screen Effect | SCREF-{Screen}-NNN | SCREF-ResultViewer-001 | Per-screen sequential | | Analytics Event | ANEV-{Name} | ANEV-ResultViewed | Name-based | | Behavior Slice | SLC-{NNN}-{PascalName} | SLC-006-HappyPath | Per-UC, name-based (latin) | | Domain Error | ERR-{UPPER_SNAKE_CODE} | ERR-PROMO_NOT_FOUND | Code-based (latin) | | Error Presentation | ERRP-{CODE}-{PascalName} | ERRP-PROMO_NOT_FOUND-Inline | Per-error, kind/context-based | | Cache Policy | CACHE-{PascalName} | CACHE-ResultMediaIndexedDb | Name-based (latin) | | Degradation Rule | DEG-{NNN}-{PascalName} | DEG-006-OfflineRestore | Per-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. > **Behavior Slice (`SLC-{NNN}-{PascalName}`)** is the graph-native acceptance > scenario (label `:Slice`), written by `nacl-sa-uc slices`. `{NNN}` is the > parent UC number (`UC-006` → `006`) — the infix makes repeated scenario names > (every UC has a `HappyPath`) unique and enables scoped filtering by > `STARTS WITH 'SLC-NNN-'`. `{PascalName}` derives from a latin short scenario > name (kebab/snake/space → PascalCase); the display `name` property may stay > in the project language. See `graph-infra/schema/sa-schema.cypher` § 3-ter > and the `sa_uc_slices` named query. > **Domain Error (`ERR-{UPPER_SNAKE_CODE}`)** is the transport-independent > domain-error record (label `:DomainError`), written by `nacl-sa-uc errors` > (and by `nacl-tl-fix` when an L2/L3 fix uncovers a missing error branch). > `{UPPER_SNAKE_CODE}` equals the `code` property — the join key to the API > envelope and the codebase; codes are domain-prefixed (`PROMO_NOT_FOUND`, > never bare `NOT_FOUND`) so module catalogs stay collision-free. The catalog > is module-scoped (`(:Module)-[:HAS_ERROR]->`): the same error is shared by > endpoints of different UCs — MERGE by id, never duplicate per UC. > **Error Presentation (`ERRP-{CODE}-{PascalName}`)** is one user-facing > presentation of that error (label `:ErrorPresentation`); `{PascalName}` > derives from the presentation kind/context by the same latin-PascalName rule > as Screen states. See `graph-infra/schema/sa-schema.cypher` § 3-quater and > the `sa_uc_errors` named query. > **Cache Policy (`CACHE-{PascalName}`)** is the caching policy of one server > data surface (label `:CachePolicy`), written by `nacl-sa-uc resilience` (and > by `nacl-tl-fix` when an L2/L3 fix uncovers a missing cache/invalidation > policy). `{PascalName}` derives from the cached surface + storage by the > latin-PascalName rule (`CACHE-ResultMediaIndexedDb`, `CACHE-QuotaMemory`). > The catalog is module-scoped (`(:Module)-[:HAS_CACHE]->`): real caches are > shared infrastructure of a bounded context — MERGE by id, never duplicate > per UC. **Degradation Rule (`DEG-{NNN}-{PascalName}`)** is one UC's > degradation behavior (label `:DegradationRule`); `{NNN}` is the parent UC > number — the infix enables scoped filtering by `STARTS WITH 'DEG-NNN-'`, > exactly like `SLC`. See `graph-infra/schema/sa-schema.cypher` § 3-quinquies > and the `sa_uc_resilience` named query. > **Decision (`DEC-NNN`)** is the graph-native provenance record (label `:Decision`). > Written by `nacl-sa-feature` (feature changes), `nacl-tl-fix` (L2/L3 fixes), and > `nacl-sa-finalize` (architecture ADRs + backfill). It is NOT a `Requirement` — > keeping it a distinct label avoids polluting the L3 "every UC needs a Requirement" > check and the planner's requirement queries. See `graph-infra/schema/sa-schema.cypher` > § Decision properties and the `sa_decisions_for_node` / `sa_timeline_of_why` queries. **Canonical ID format (single authority).** Padding is `apoc.text.lpad(toString(n), width, '0')` — a true left-pad that never truncates. The JS authority is `$(nacl-home)/nacl-core/scripts/nacl-ids.mjs` (`node $(nacl-home)/nacl-core/scripts/nacl-ids.mjs [parentId]`), pinned by `$(nacl-home)/nacl-core/scripts/nacl-ids.test.mjs`; use it when formatting an id OUTSIDE Cypher. In-Cypher, use `apoc.text.lpad` (identical output). **Never** use `right('0…'+toString(n), width)` — it truncates the high digits at n≥10^width (`right('00100',2)` → `'00'`), the bug that once made `nacl-ba-sync` diverge from `nacl-ba-process/-entities/-roles`. To get next available ID: ```cypher // Example: next BusinessProcess ID — canonical lpad form (== nacl-ids.mjs) MATCH (bp:BusinessProcess) WITH max(toInteger(replace(bp.id, 'BP-', ''))) AS maxNum RETURN 'BP-' + apoc.text.lpad(toString(coalesce(maxNum, 0) + 1), 3, '0') AS nextId ``` ## Skill Modifier Conventions Skills accept **modifiers** — arguments that change behavior. See [Skill Modifiers Reference](docs/skill-modifiers.md) for the full catalog. ### Three Paradigms | Paradigm | When to Use | Syntax | Example | |----------|------------|--------|---------| | **Mode** (positional) | 2-5 mutually exclusive workflow branches | `/skill MODE` | `/nacl-sa-domain IMPORT_BA` | | **Subcommand** | 3+ distinct operations under one namespace | `/skill command [args]` | `/nacl-ba-from-board sync` | | **Flag** | Optional behavioral modifiers | `/skill [target] --flag` | `/nacl-tl-ship --deploy` | ### Standard Flag Families Reuse these before inventing new flags: | Family | Flags | Purpose | |--------|-------|---------| | Task type filter | `--be`, `--fe`, `--tech` | Filter by development type | | Workflow filter | `--review`, `--sync`, `--qa` | Filter by workflow phase | | Scope | `--wave N`, `--feature FR-NNN`, `--task UC###`, `--all` | Limit scope | | Skip | `--skip-{phase}` | Skip a named workflow phase | | User gates | `--yes` (skip), `--confirm` (add) | Control confirmation prompts | | Safety | `--dry-run`, `--force`, `--force-push` | Risk-level control | | Auto-chain | `--deploy`, `--auto-ship` | Chain into next skill | | Version | `--major`, `--minor`, `--patch` | SemVer bump type | | Output | `--compact`, `--list`, `--final`, `--output ` | Control output format | ### Naming Rules for New Modifiers - **Flags:** kebab-case with double dashes: `--skip-verify`, `--dry-run` - **Mode values:** UPPER_CASE for CRUD branches (`FULL`, `CREATE`, `MODIFY`), lowercase for utility modes (`full`, `module`) - **Subcommands:** lowercase: `sync`, `verify`, `stories` - **Identifiers:** match layer format: `UC028`, `FR-001`, `TECH003`