/nacl-sa-feature -- Incremental Feature Specification (Graph)

SkillDev tools

Incremental feature specification via Neo4j graph. Impact analysis through Cypher traversal, selective SA skill invocation, FeatureRequest artifact.Use when: add feature with graph, new functionality, or the user says "/nacl-sa-feature".

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-sa-feature -- Incremental Feature Specification (Graph) skill

What this skill tells your AI

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

Your Role

You are a system analyst who adds new features to an existing, already-specified project. Unlike /nacl-sa-architect (which builds module decomposition from scratch), you surgically update only the affected parts of the specification by leveraging Neo4j graph traversal for impact analysis.

You produce a FeatureRequest artifact (.tl/feature-requests/FR-NNN.md) that serves as a bridge to TL for selective task planning.

Key advantage over sa-feature: Impact analysis is performed via Cypher queries against the live graph, not by reading markdown files. This makes detection precise -- affected modules, entities, UCs, and roles are found through relationship traversal, not text scanning.

Key Principles

1. Graph-first impact: Cypher traversal finds affected nodes BEFORE writing specs
2. Selective execution: Run only the nacl-sa-* skills that are needed
3. Dependency order: Architecture -> Domain -> Roles -> UCs -> UI
4. FeatureRequest handoff: Explicit artifact for TL consumption
5. Spec-first: Define behavior in graph BEFORE any code exists
6. Minimal blast radius: Only affected subgraph is modified

Shared References

Read nacl-core/SKILL.md for:

  • Neo4j MCP tool names and connection info (mcp__neo4j__read-cypher, mcp__neo4j__write-cypher)
  • ID generation rules
  • Schema files location (graph-infra/schema/sa-schema.cypher)
  • Query library location (graph-infra/queries/sa-queries.cypher)

Graph Queries Used

From graph-infra/queries/sa-queries.cypher:

QueryPurpose
sa_impact_analysisFull-text search across graph nodes to find affected modules/entities/UCs by keywords
sa_feature_scopeLoad full subgraph for affected UCs -- entities, forms, requirements
sa_next_uc_in_moduleFind next available UC number within a module's allocated range

Skills Invoked Selectively

SkillWhen invoked
nacl-sa-architectNew module needed
nacl-sa-domainNew or modified entities/enums
nacl-sa-rolesNew permissions or roles
nacl-sa-ucNew or modified use cases
nacl-sa-uiNew or modified screens
nacl-sa-validateIncremental validation (--scope=intra-uc)

Invocation

The user describes the feature in natural language:

/nacl-sa-feature "Add payment system with YooKassa integration"
/nacl-sa-feature "Add VK and email auth alongside existing Telegram"
/nacl-sa-feature "Admin panel for managing prompts and viewing statistics"

No need to specify UC numbers, modules, or domains. The skill determines impact automatically via graph traversal.

Flags

FlagDescription
--namespace=<DOMAIN>Optional FR sub-namespace allocation
--bounded-only(2.10.1+) Refuse to draft a feature spec that exceeds the bounded execution envelope. Used by /nacl-goal intake for FEATURE_SMALL atoms — see ## --bounded-only mode below.

--bounded-only mode (2.10.1+)

When invoked with --bounded-only, this skill checks the proposed feature against an envelope of constraints BEFORE drafting any new UC/spec. If the feature exceeds the envelope, this skill refuses with a structured output instead of producing a partial spec; if within the envelope, the standard skill flow runs unchanged.

Refuse criteria (any one triggers BOUNDED REFUSE)

The feature exceeds the envelope when ANY of the following holds:

  • Migration required — would require a DB schema migration, a public-API contract change, a message-contract change, or any other non-additive backwards-incompatibility
  • Auth/security/permissions touched — modifies authentication, authorization, the permission matrix, or any security-policy surface
  • Billing/payment touched — adds, modifies, or removes pricing, payment flow, invoicing, or any monetization surface
  • L2/L3 architecture amendment — changes bounded-context boundaries, cross-module contracts, or the system's macro architecture (Context Map level)
  • Destructive data operation — bulk delete, data migration that loses information, backup-incompatible change
  • Unresolved product decision — feature spec requires the human to choose between alternatives (e.g. "should pricing be tier-based or usage-based?") that this skill cannot resolve from graph evidence alone

Refuse output

When refused, this skill writes TWO artifacts to .tl/goal-runs/<NACL_GOAL_RUN_ID>/planning/ (if NACL_GOAL_RUN_ID is set; otherwise to .tl/feature-plans/<sanitized-feature-slug>/) and exits with headline FEATURE BOUNDED REFUSE:

  1. feature-plan.md — what this skill understood from the feature description:

    • Candidate UCs that would need to be created or modified
    • Suggested module placement
    • Identified affected entities and existing UCs
    • Suggested NFRs from existing patterns
    • Missing inputs the human needs to provide
  2. open-decisions.md — explicit decision points requiring human input:

    • Each decision as a bullet with: alternatives, trade-offs, suggested-but-not-chosen default
    • Migration-impact notes if any
    • Security/billing/permissions impact notes if any

The human reviews these artifacts and either:

  • Resolves the decisions and re-runs /nacl-sa-feature interactively (without --bounded-only) to draft the full spec, OR
  • Narrows the feature to a bounded subset and re-runs /nacl-goal intake "<narrower goal>" for autonomous execution

Accept path

When the feature is within the envelope, --bounded-only runs the standard skill flow without modification — the same FR allocation, UC drafting, graph persistence, and handoff that an interactive invocation produces. The only difference: this skill records bounded_only: true in the FR artifact metadata so downstream skills (/nacl-tl-dev*) know the feature was constrained.

Invariant

When --bounded-only is NOT passed, this skill behaves exactly as today (drafts whatever the feature description implies, prompts the user for clarifications). Interactive /nacl-sa-feature "..." is unaffected. The bounded mode is opt-in by the orchestrator, not the default.

Goal-context env vars (2.10.1+)

When this skill is invoked under /nacl-goal intake, the wrapper exports NACL_GOAL_RUN_ID, NACL_GOAL_BRANCH, NACL_SHIP_MODE=append, NACL_GOAL_BUDGET_FILE. The bounded-mode refuse output writes to .tl/goal-runs/<NACL_GOAL_RUN_ID>/planning/ (see above). On accept, the wrapper subsequently invokes /nacl-tl-dev --auto-ship for implementation, inheriting the env vars and triggering append-mode ship.


Language Rules

  • This SKILL.md: English (instructions for Claude)
  • Generated graph node properties (names, descriptions): Project's documentation language (detect from existing graph data -- usually Russian)
  • FeatureRequest artifact (.tl/): English (consumed by TL agents)
  • User-facing output (console): User's language (detect from conversation)

Workflow: 6 Phases

+---------------+    +------------------+    +------------------+    +------------------+    +------------------+    +-----------+
| Phase 1       |    | Phase 2          |    | Phase 3          |    | Phase 4          |    | Phase 5          |    | Phase 6   |
| Understand    |--->| Impact Analysis  |--->| Spec Updates     |--->| Incremental      |--->| Update           |--->| Handoff   |
| Request       |    | (Cypher          |    | (selective       |    | Validation       |    | Traceability     |    | (FR file) |
|               |    |  traversal)      |    |  nacl-sa-*)     |    | (nacl-sa-       |    | (graph indexes)  |    |           |
+---------------+    +------------------+    +------------------+    |  validate)       |    +------------------+    +-----------+
                                                                     +------------------+

Phase 1: UNDERSTAND THE REQUEST

Goal: Parse the feature, load current system state from graph, determine approach.

Step 1.1: Read the feature description

Parse the user's natural language description. Identify keywords for graph search.

Step 1.2: Load current system state from Neo4j

Run these queries to understand the existing specification:

Modules and their UC ranges:

// mcp__neo4j__read-cypher
MATCH (m:Module)
OPTIONAL MATCH (m)-[:CONTAINS_UC]->(uc:UseCase)
OPTIONAL MATCH (m)-[:CONTAINS_ENTITY]->(de:DomainEntity)
RETURN m.id AS id, m.name AS name, m.description AS description,
       m.uc_range_start AS uc_start, m.uc_range_end AS uc_end,
       count(DISTINCT uc) AS uc_count,
       count(DISTINCT de) AS entity_count
ORDER BY m.uc_range_start

Existing roles:

// mcp__neo4j__read-cypher
MATCH (sr:SystemRole)
OPTIONAL MATCH (sr)-[:HAS_PERMISSION]->(p:Permission)
RETURN sr.id AS id, sr.name AS name,
       count(p) AS permission_count

Existing UC registry:

// mcp__neo4j__read-cypher
MATCH (m:Module)-[:CONTAINS_UC]->(uc:UseCase)
RETURN m.name AS module, uc.id AS uc_id, uc.name AS uc_name,
       uc.priority AS priority, uc.status AS status
ORDER BY uc.id
Step 1.3: Determine approach
  • Requirements-First: Behavior is clear, technical approach unclear -- start with User Stories, derive UCs
  • Design-First: Architecture constraints exist (e.g., must use specific API, existing DB schema) -- start with technical design, derive behavior

Output: Feature brief in user's language -- what, why, for whom, approach.


Phase 2: IMPACT ANALYSIS (Cypher traversal)

Goal: Use graph queries to determine exactly what the feature touches. Present to user for confirmation.

This is the key advantage of nacl-sa-feature over sa-feature: impact is detected by traversing the live graph, not by scanning markdown files.

Step 2.1: Run sa_impact_analysis query

Extract keywords from the feature description, then query:

// mcp__neo4j__read-cypher
// Query: sa_impact_analysis
CALL db.index.fulltext.queryNodes('fulltext_ba_search', $keywords) YIELD node, score
WHERE score > 0.5
RETURN labels(node)[0] AS node_type, node.id AS id,
       coalesce(node.name, node.term, node.function_name, node.description) AS name,
       score
ORDER BY score DESC
LIMIT 20

Parameters:

  • $keywords -- space-separated keywords extracted from the feature description
Step 2.2: Trace affected modules from impact results

For each node returned by sa_impact_analysis, trace upward to its Module:

// mcp__neo4j__read-cypher
MATCH (node) WHERE node.id IN $affected_ids
OPTIONAL MATCH (m:Module)-[:CONTAINS_UC|CONTAINS_ENTITY*1..2]->(node)
RETURN DISTINCT m.id AS module_id, m.name AS module_name,
       collect(DISTINCT {id: node.id, type: labels(node)[0], name: node.name}) AS affected_nodes
Step 2.3: Load full scope for affected UCs

If existing UCs are in the impact set, load their full subgraph:

// mcp__neo4j__read-cypher
// Query: sa_feature_scope
MATCH (uc:UseCase) WHERE uc.id IN $ucIds
OPTIONAL MATCH (uc)-[:HAS_STEP]->(as_step:ActivityStep)
OPTIONAL MATCH (uc)-[:USES_FORM]->(f:Form)-[:HAS_FIELD]->(ff:FormField)
OPTIONAL MATCH (ff)-[:MAPS_TO]->(da:DomainAttribute)<-[:HAS_ATTRIBUTE]-(de:DomainEntity)
OPTIONAL MATCH (uc)-[:HAS_REQUIREMENT]->(rq:Requirement)
OPTIONAL MATCH (uc)-[:ACTOR]->(sr:SystemRole)
RETURN uc,
       collect(DISTINCT as_step) AS steps,
       collect(DISTINCT f) AS forms,
       collect(DISTINCT de) AS entities,
       collect(DISTINCT rq) AS requirements,
       collect(DISTINCT sr) AS roles
Step 2.4: Classify impact

Analyze the feature against graph results and classify:

AreaQuestionIf YES -> flag
ArchitectureDoes this need a new module/bounded context?nacl-sa-architect module
Domain: newDoes this introduce new entities or enums?nacl-sa-domain CREATE
Domain: modifyDoes this change existing entities?nacl-sa-domain MODIFY
UCs: newDoes this create new user interaction flows?nacl-sa-uc (create)
UCs: modifyDoes this change existing UC behavior?nacl-sa-uc (update)
RolesDoes this add new permissions or roles?nacl-sa-roles
UI: newDoes this need new forms or UI components?nacl-sa-uc (creates Form/FormField) + nacl-sa-ui (creates Component, including component_type='navigation')
UI: modifyDoes this change existing forms or components?nacl-sa-ui (update)

Note on UI terminology. The SA schema (graph-infra/schema/sa-schema.cypher) does not define Screen or NavigationRoute labels. UI is modeled as Form + FormField + Component. Navigation is a Component with component_type='navigation' and route/roles/menu_order/parent_menu properties, linked to Form via USED_IN. Trace path: UseCase -[USES_FORM]-> Form -[HAS_FIELD]-> FormField -[MAPS_TO]-> DomainAttribute.

Step 2.5: Determine UC allocation

For new UCs, find the next available number for the target module:

// mcp__neo4j__read-cypher
// Query: sa_next_uc_in_module
// Candidate = module-local max + 1 (empty module: m.uc_range_start).
// MANDATORY collision check against ALL UseCase ids: range-partitioned
// projects keep their module-local number (no collision -> candidate stands);
// projects with global UC numbering fall back to global max + 1, which is
// collision-free by construction. Never hand-compute UC ids.
MATCH (m:Module {id: $moduleId})
OPTIONAL MATCH (m)-[:CONTAINS_UC]->(muc:UseCase)
WITH m, max(toInteger(replace(muc.id, 'UC-', ''))) AS localMax
OPTIONAL MATCH (any:UseCase) WHERE any.id =~ 'UC-[0-9]+'
WITH coalesce(localMax + 1, m.uc_range_start, 1) AS candidate,
     collect(toInteger(replace(any.id, 'UC-', ''))) AS allNums
WITH CASE WHEN candidate IN allNums
     THEN reduce(mx = 0, n IN allNums | CASE WHEN n > mx THEN n ELSE mx END) + 1
     ELSE candidate END AS nextNum
RETURN 'UC-' + apoc.text.lpad(toString(nextNum), 3, '0') AS nextUcId

The query itself handles the empty module (m.uc_range_start fallback) and the collision check. On a project with global UC numbering (UC ids increase across modules while uc_range_start sits unused), the module-local candidate collides with a sibling module's UC and the query falls back to global max + 1; on a range-partitioned project the module-local number stands.

Step 2.6: Present impact matrix

Present to user (in their language):

+-----------------------------------------+
| FEATURE IMPACT ANALYSIS (Graph)         |
+-----------------------------------------+
| Feature: [name]                         |
|                                         |
| Architecture:  [NEW MODULE / no change] |
| Domain:        [+N entities, +M enums]  |
| Use Cases:     [+N new, ~M modified]    |
| Roles:         [+N permissions]         |
| UI: Forms      [+N new, ~M modified]    |
| UI: Components [+N new, ~M modified]    |
|                                         |
| Affected modules: [list from graph]     |
| Affected UCs:     [list from graph]     |
| Affected entities:[list from graph]     |
|                                         |
| Skills to run: [list]                   |
| Estimated steps: [N]                    |
+-----------------------------------------+

USER GATE: User confirms scope before proceeding. User may adjust (e.g., "skip admin panel for now, just do payment").


Phase 3: SPEC UPDATES (selective, dependency order)

Goal: Run only the flagged nacl-sa-* skills, in dependency order.

Execute ONLY the steps that were flagged in Phase 2. Skip everything else.

3a. Architecture (if new module flagged)

Invoke /nacl-sa-architect module [module_name] via Skill tool:

  • Creates Module node in graph with allocated UC range
  • Creates DEPENDS_ON edges to existing modules
  • Creates SUGGESTS edge from ProcessGroup (if applicable)

If Skill tool unavailable, create Module node manually following nacl-sa-architect conventions.

3b. Domain Model (if new/modified entities flagged)

For each new entity:

  • Invoke /nacl-sa-domain CREATE [entity_name] via Skill tool or manually:
    • Create DomainEntity node with attributes
    • Create CONTAINS_ENTITY edge from Module
    • Create REALIZED_AS edge from BusinessEntity (if BA source exists)

For each modified entity:

  • Invoke /nacl-sa-domain MODIFY [entity_name] via Skill tool or manually:
    • Add/change DomainAttribute nodes
    • Update relationships
    • Run downstream impact check on dependent UCs:
// mcp__neo4j__read-cypher
MATCH (de:DomainEntity {id: $entityId})<-[:HAS_ATTRIBUTE]-(:DomainAttribute)<-[:MAPS_TO]-(:FormField)<-[:HAS_FIELD]-(:Form)<-[:USES_FORM]-(uc:UseCase)
RETURN DISTINCT uc.id AS uc_id, uc.name AS uc_name

For new enums:

  • Create DomainEnum node with values
  • Link to owning DomainEntity
3c. Roles (if new permissions flagged)

Invoke /nacl-sa-roles via Skill tool or manually:

  • Create SystemRole / Permission nodes
  • Create HAS_PERMISSION edges
  • Update ACTOR edges on affected UCs
3d. UC Registration + Detail (if new UCs flagged)

For each new UC:

  1. Get next UC number — same collision-safe query as Step 2.5 (sa_next_uc_in_module); do NOT use a module-only max here:
// mcp__neo4j__read-cypher
// Query: sa_next_uc_in_module (see Step 2.5 for the collision rationale)
MATCH (m:Module {id: $moduleId})
OPTIONAL MATCH (m)-[:CONTAINS_UC]->(muc:UseCase)
WITH m, max(toInteger(replace(muc.id, 'UC-', ''))) AS localMax
OPTIONAL MATCH (any:UseCase) WHERE any.id =~ 'UC-[0-9]+'
WITH coalesce(localMax + 1, m.uc_range_start, 1) AS candidate,
     collect(toInteger(replace(any.id, 'UC-', ''))) AS allNums
WITH CASE WHEN candidate IN allNums
     THEN reduce(mx = 0, n IN allNums | CASE WHEN n > mx THEN n ELSE mx END) + 1
     ELSE candidate END AS nextNum
RETURN 'UC-' + apoc.text.lpad(toString(nextNum), 3, '0') AS nextUcId
  1. Invoke /nacl-sa-uc [UC_number] via Skill tool or create manually:
    • UseCase node with name, description, priority, user_story
    • CONTAINS_UC edge from Module
    • ActivityStep nodes with HAS_STEP edges
    • Form + FormField nodes with USES_FORM / HAS_FIELD edges
    • FormField MAPS_TO DomainAttribute edges
    • Requirement nodes with HAS_REQUIREMENT edges
    • ACTOR edge to SystemRole
3e. UC Update (if existing UCs modified)

For each modified UC:

  • Invoke /nacl-sa-uc [UC_number] --mode=update via Skill tool or edit manually:
    • Add new ActivityStep nodes
    • Update preconditions/postconditions on UseCase node
    • Add new Requirement nodes
    • Update Form/FormField nodes if UI changed
3f. Interface (if new/modified UI flagged)

Invoke /nacl-sa-ui via Skill tool or manually. The schema for UI is Form/FormField/Component — there are no Screen or NavigationRoute labels:

  • Run nacl-sa-ui verify to confirm FormField -[MAPS_TO]-> DomainAttribute traceability for affected UCs
  • Run nacl-sa-ui components to create/update Component nodes (display, layout, input, feedback) and [:USED_IN]->Form edges
  • Run nacl-sa-ui navigation to create/update Component {component_type:'navigation', route, roles, menu_order, parent_menu} for UCs with UI

After each sub-step: Report progress to user. User can stop at any point.

3g. Mark change provenance (spec_version + staleness)

After all spec updates land, record the change in the graph so downstream planning and closure skills can detect what must be revisited. This is the mechanism behind "pull one thread → see everything": the change stamps its dependents, and they stay stamped until re-synced.

  1. Bump spec_version on every created/modified UC so nacl-tl-plan can detect tasks that were planned from an older version:
// mcp__neo4j__write-cypher
// Params: $affectedUcIds — new + modified UC ids from Phase 2/3
MATCH (uc:UseCase) WHERE uc.id IN $affectedUcIds
SET uc.spec_version = coalesce(uc.spec_version, 0) + 1,
    uc.updated_at   = datetime()
  1. Stamp staleness on the true downstream of the change: the affected UCs' generated Tasks (the snapshot-bearers that re-plan regenerates), the Tasks of UCs that transitively depend on them, and the affected UCs themselves.

    Use the affected-UC list ($affectedUcIds) — NOT the broad undirected closure. sa_impact_closure is deliberately broad for exploration/display ("what's potentially related"); the stamp gates release closure, so it must be precise. An undirected blob fans out through shared ACTOR (one role → every UC with that role) or a shared Requirement/Form, marking half the project stale and blocking releases on false staleness. The directed, UC-keyed stamp below is identical to nacl-tl-fix's and stays bounded to real dependents.

Run as two statements (keep them separate — a single statement that stamps tasks and then re-matches the UCs produces a cartesian whose row count is NOT the stale-task count, which misleads on a plain channel). Params for both: $affectedUcIds — UCs created/modified in this feature (Phase 2/3); $reason — e.g. "feature FR-007 changed UC-014"; $origin — the change anchor (FR id).

// mcp__neo4j__write-cypher  (1/2) — stamp the dependent Tasks (the re-plan units)
MATCH (uc:UseCase) WHERE uc.id IN $affectedUcIds
OPTIONAL MATCH (dependent:UseCase)-[:DEPENDS_ON*1..5]->(uc)   // UCs that depend ON the changed UC
WITH collect(DISTINCT uc) + [d IN collect(DISTINCT dependent) WHERE d IS NOT NULL] AS affected
UNWIND affected AS a
MATCH (a)-[:GENERATES]->(t:Task)
SET t.review_status='stale', t.stale_reason=$reason, t.stale_since=datetime(), t.stale_origin=$origin
RETURN count(DISTINCT t) AS tasks_stamped
// mcp__neo4j__write-cypher  (2/2) — stamp the directly-changed UCs themselves
MATCH (uc:UseCase) WHERE uc.id IN $affectedUcIds
SET uc.review_status='stale', uc.stale_reason=$reason, uc.stale_since=datetime(), uc.stale_origin=$origin
RETURN count(uc) AS ucs_stamped

The dependent Tasks are now stale. This is expected — it is the signal that /nacl-tl-plan --feature FR-NNN must regenerate them. The closure gate (nacl-tl-release / nacl-tl-conductor) refuses until they clear. sa-feature does NOT clear them; planning does. Surface the count in the FeatureRequest's "Modified UCs to Re-plan" section and the completion summary.


Phase 4: INCREMENTAL VALIDATION

Goal: Validate only the affected artifacts, not the entire spec.

Run /nacl-sa-validate with scope limited to affected nodes. The advantage of graph validation is that scoping is precise -- only the affected subgraph is checked.

Step 4.1: Determine validation scope
What changedValidation levels to run
Domain modelL1 (data consistency), L2 (model connectivity)
New UCsL4 (form-domain traceability), L5 (UC-form validation)
Modified UCsL4, L5 for affected UCs only
New moduleL6 (cross-module consistency)
RolesL1 (role consistency)
Step 4.2: Run scoped validation

Invoke /nacl-sa-validate --scope=intra-uc via Skill tool if available.

Otherwise, run targeted Cypher checks manually:

Form-Domain traceability (L4) for affected UCs:

// mcp__neo4j__read-cypher
MATCH (uc:UseCase)-[:USES_FORM]->(f:Form)-[:HAS_FIELD]->(ff:FormField)
WHERE uc.id IN $affectedUcIds
  AND NOT (ff)-[:MAPS_TO]->(:DomainAttribute)
RETURN uc.id AS uc_id, f.id AS form_id, ff.id AS field_id, ff.name AS field_name,
       'FormField has no MAPS_TO -> DomainAttribute' AS problem

UC-Form validation (L5) for affected UCs:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
27
Forks
4
Last commit
Sep 2026
Advanced
Item type
skill
Key
nacl-sa-feature
Source
github.com/itsalt/nacl