nacl-sa-validate

SkillAI & models

Validate specification consistency through Neo4j Cypher queries. Internal validation (L1-L13): data consistency, model connectivity, requirement completeness, form-domain traceability, UC-form validation, cross-module consistency, FeatureRequest consistency, staleness closure, decision provenance, screen state machines, behavior slices, domain error taxonomy, cache & degradation policies (SA-extension connectivity). Cross-validation BA->SA (XL6-XL9): UC coverage, entity coverage, role coverage, rule coverage. Use when: validate specification, check consistency, find errors, run checks, quality gate.

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-validate skill

What this skill tells your AI

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

Use with /goal

Wrap with: /nacl-goal validate:module:<MOD-ID> (tier S)

This skill is a good fit for autonomous /goal loops because all checks are read-only Cypher queries whose results are deterministic: the check script queries the same Neo4j graph and counts zero-row (PASS) vs non-zero-row (FAIL) outcomes for each L1–L13 and XL6–XL9 check. The wrapper composes a completion condition that all enabled checks return zero findings.

Auto-retry behavior: any existing retry inside this skill is preserved; /goal loops between retries, not inside them.

Check script: nacl-goal/checks/validate.sh Refusals: see nacl-goal/refusal-catalog.md for the gates this wrapper guards. Background: docs/guides/goal-command.md


/nacl-sa-validate -- Specification Validation (Graph)

Purpose

Quality gate for the entire specification. Runs Cypher queries against Neo4j to detect problems in data consistency, model connectivity, requirement completeness, form-domain traceability, and BA-to-SA cross-layer coverage. All checks are read-only -- validation never modifies data.

Shared references: nacl-core/SKILL.md


Neo4j Tools

ToolUsage
mcp__neo4j__read-cypherALL validation queries (read-only)
mcp__neo4j__get-schemaIntrospect current graph schema before running checks

IMPORTANT: This skill uses ONLY read-cypher. Validation must NEVER write to the graph.


Invocation

/nacl-sa-validate [level] [--scope=<scope>]

Parameters

ParameterValuesDescription
levelinternalL1-L13: SA-internal consistency checks (incl. L8 staleness, L9 decision provenance, L10 screen state machines, L11 behavior slices, L12 domain error taxonomy, L13 cache & degradation policies)
ba-crossXL6-XL9: BA-to-SA cross-layer coverage
full (default)All levels: L1-L13 + XL6-XL9
--scopeintra-uc UC-NNN[,UC-NNN]Limit validation to specific UCs and their subgraph (forms, fields, requirements, entities). Used by nacl-sa-feature for incremental validation.
intra-module mod-xxxLimit validation to a specific module's nodes.

When --scope is provided, all Cypher queries are augmented with a WHERE clause filtering to the specified UC or module subgraph. Checks outside the scope are skipped.


Workflow Overview

                         LAUNCH VALIDATION
                               |
                    +----------+-----------+
                    |                      |
              [level = internal]     [level = ba-cross]
              [level = full]        [level = full]
                    |                      |
          +---------+---------+    +-------+-------+
          |  L1: Data         |    | XL6: UC       |
          |  Consistency      |    | Coverage      |
          +---------+---------+    +-------+-------+
                    |                      |
          +---------+---------+    +-------+-------+
          |  L2: Model        |    | XL7: Entity   |
          |  Connectivity     |    | Coverage      |
          +---------+---------+    +-------+-------+
                    |                      |
          +---------+---------+    +-------+-------+
          |  L3: Requirement  |    | XL8: Role     |
          |  Completeness     |    | Coverage      |
          +---------+---------+    +-------+-------+
                    |                      |
          +---------+---------+    +-------+-------+
          |  L4: Form-Domain  |    | XL9: Rule     |
          |  Traceability     |    | Coverage      |
          +---------+---------+    +-------+-------+
                    |                      |
          +---------+---------+            |
          |  L5: UC-Form      |            |
          |  Validation       |            |
          +---------+---------+            |
                    |                      |
          +---------+---------+            |
          |  L6: Cross-Module |            |
          |  Consistency      |            |
          +---------+---------+            |
                    |                      |
          +---------+---------+            |
          |  L7: FeatureReq   |            |
          |  Consistency      |            |
          +---------+---------+            |
                    |                      |
          +---------+---------+            |
          |  L8: Staleness    |            |
          |  Closure          |            |
          +---------+---------+            |
                    |                      |
          +---------+---------+            |
          |  L9: Decision     |            |
          |  Provenance       |            |
          +---------+---------+            |
                    |                      |
          +---------+---------+            |
          |  L10: Screen      |            |
          |  State Machines   |            |
          +---------+---------+            |
                    |                      |
          +---------+---------+            |
          |  L11: Behavior    |            |
          |  Slices           |            |
          +---------+---------+            |
                    |                      |
          +---------+---------+            |
          |  L12: Domain      |            |
          |  Error Taxonomy   |            |
          +---------+---------+            |
                    |                      |
          +---------+---------+            |
          |  L13: Cache &     |            |
          |  Degradation      |            |
          +---------+---------+            |
                    |                      |
                    +----------+-----------+
                               |
                    +----------+-----------+
                    |  GENERATE REPORT     |
                    +----------------------+

Schema Reference

This skill assumes the canonical SA schema as defined in graph-infra/schema/sa-schema.cypher and produced by:

  • /nacl-sa-architect -- writes :Module, :Component, edge (:Module)-[:CONTAINS_UC]->(:UseCase), (:Module)-[:CONTAINS_ENTITY]->(:DomainEntity)
  • /nacl-sa-domain -- writes :DomainEntity, :DomainAttribute, :Enumeration, :EnumValue (with .value property)
  • /nacl-sa-uc -- writes :UseCase, :Requirement, :Form, :FormField, :ActivityStep; slices command writes :Slice with edges HAS_SLICE, COVERS, CALLS, VERIFIED_BY (see graph-infra/schema/sa-schema.cypher § 3-ter; note the CALLS name is shared with ScreenEffect→APIEndpoint — every L11 query label-qualifies the source as (sl:Slice)); errors command writes :DomainError, :ErrorPresentation with edges HAS_ERROR, MAY_RAISE, HANDLES, PRESENTED_AS, SHOWS (see § 3-quater; all five names are unshared — no label-qualification hazard); resilience command writes :CachePolicy, :DegradationRule with edges HAS_CACHE, CACHES, HAS_DEGRADATION, ON_ERROR, DEGRADES_TO (see § 3-quinquies; all five names again unshared)
  • /nacl-sa-roles -- writes :SystemRole
  • /nacl-sa-ui -- writes :Component, :FormField; state-machine command writes :Screen, :ScreenState, :ScreenEvent, :Transition, :ScreenEffect, :AnalyticsEvent with edges HAS_SCREEN, RENDERS, HAS_STATE, HAS_EVENT, HAS_TRANSITION, FROM_STATE, TO_STATE, ON_EVENT, TRIGGERS, CALLS, NAVIGATES_TO, EMITS (see graph-infra/schema/sa-schema.cypher § 3-bis; note HAS_STATE/TRIGGERS names are shared with the BA layer — every L10 query is label-qualified)
  • BA->SA handoff edges (canonical names): AUTOMATES_AS, REALIZED_AS, IMPLEMENTED_BY, MAPPED_TO, TYPED_AS, SUGGESTS
  • Provenance (canonical names, written by /nacl-sa-feature, /nacl-tl-fix, /nacl-sa-finalize): :Decision node, edges (:Decision)-[:JUSTIFIES]->(...), (:Decision)-[:SUPERSEDES]->(:Decision), (:FeatureRequest)-[:IMPLEMENTS]->(:Decision)
  • Staleness properties (set by /nacl-sa-feature, /nacl-tl-fix): review_status, stale_reason, stale_since, stale_origin — read with coalesce(n.review_status,'current').
  • Stereotype on automated steps: WorkflowStep.stereotype = 'Автоматизируется' (Russian) or 'Automated' (English) -- both accepted.

Non-canonical aliases are NOT supported. If the graph uses any of these, validation will HALT in pre-flight (Step 0a) instead of producing false-positive criticals:

Non-canonicalCanonical
:SAModule:Module
:SAEntity:DomainEntity
:SARequirement:Requirement
:SAActor:SystemRole
:SAComponent:Component
edge TRACES_TOuse AUTOMATES_AS / REALIZED_AS / IMPLEMENTED_BY / MAPPED_TO per source-target semantics

If your graph uses non-canonical labels, see the Migration Cypher Appendix at the bottom of this skill, or re-import the SA layer using canonical skills.


Pre-flight Checks

Step 0: Verify graph has data

Before running any validation, confirm that the graph contains SA-layer nodes under canonical labels.

// Pre-flight: count canonical SA-layer nodes
MATCH (n)
WHERE n:Module OR n:UseCase OR n:DomainEntity OR n:Form OR n:Requirement OR n:SystemRole
RETURN labels(n)[0] AS label, count(n) AS count
ORDER BY label

If the result is empty or all counts are 0:

  1. STOP -- validation is impossible without data in the graph.
  2. Suggest the user runs /nacl-sa-architect or /nacl-sa-domain first.
  3. Explain that /nacl-sa-validate works only with a populated Neo4j graph.

Step 0a: Detect schema drift (CRITICAL gate)

If canonical SA labels are absent but non-canonical aliases exist (e.g. :SAModule, :SAEntity, :SARequirement), the validator's queries silently return zero rows and produce false-positive CRITICAL findings for every L2-L9 / XL6-XL9 check. This step catches that scenario explicitly.

// Step 0a: schema-drift detection
CALL db.labels() YIELD label
WITH collect(label) AS allLabels
RETURN
  [l IN allLabels WHERE l IN
     ['Module','DomainEntity','Requirement','SystemRole','Component']] AS canonical_present,
  [l IN allLabels WHERE l IN
     ['SAModule','SAEntity','SARequirement','SAActor','SAComponent']] AS dialect_present,
  [l IN allLabels WHERE l STARTS WITH 'SA' AND l <> 'SystemRole'] AS sa_prefixed_labels,
  allLabels AS all_labels_in_graph

Also probe for non-canonical edge types that signal drift in BA->SA handoff:

// Step 0a (cont.): probe non-canonical edge types
CALL db.relationshipTypes() YIELD relationshipType
WITH collect(relationshipType) AS allEdges
RETURN
  [r IN allEdges WHERE r IN
     ['AUTOMATES_AS','REALIZED_AS','IMPLEMENTED_BY','MAPPED_TO','TYPED_AS','SUGGESTS']]
       AS canonical_handoff_edges,
  [r IN allEdges WHERE r IN ['TRACES_TO','REALIZES','MAPS_FROM']] AS dialect_handoff_edges

Decision rule:

ConditionAction
canonical_present non-empty AND dialect_present emptyOK -- continue to Step 0b
canonical_present empty AND dialect_present non-emptyHALT -- emit drift report below; do NOT run any L*/XL* checks
Both non-emptyHALT -- mixed schema is worse than pure dialect; emit drift report; do NOT run checks
dialect_handoff_edges non-empty AND no canonical handoff edgesHALT -- BA->SA layer uses non-canonical edges; emit drift report

Drift report template (emit verbatim, fill placeholders from query results):

==============================================================================
SCHEMA DRIFT DETECTED -- VALIDATION HALTED
==============================================================================

The graph uses non-canonical labels/edges that this skill does not support.

  Canonical labels found    : {canonical_present}
  Non-canonical labels found: {dialect_present}
  All SA-prefixed labels    : {sa_prefixed_labels}
  Canonical handoff edges   : {canonical_handoff_edges}
  Non-canonical handoff edges: {dialect_handoff_edges}

The skill expects the canonical SA schema (see "Schema Reference" section above).
Running validation queries against this graph would silently miss every node and
produce false-positive CRITICAL findings.

To proceed, choose ONE:

  1. Migrate the graph to canonical labels & edges.
     See the "Migration Cypher Appendix" at the bottom of this skill --
     copy the block, run it via mcp__neo4j__write-cypher, then re-run
     /nacl-sa-validate. The migration is idempotent.

  2. Re-import the SA layer using canonical skills:
       /nacl-sa-architect   (creates :Module + structural skeleton)
       /nacl-sa-domain      (creates :DomainEntity, :Enumeration)
       /nacl-sa-uc          (creates :UseCase, :Requirement, :Form, :FormField)
       /nacl-sa-roles       (creates :SystemRole)
       /nacl-ba-handoff     (creates BA->SA handoff edges)

DO NOT proceed with validation. Halt here, surface this report to the user,
and wait for instruction.
==============================================================================

Step 0b: Pre-flight node-count report (two sections)

Once Step 0a has confirmed the graph is canonical, render a two-section node-count report. The first section is canonical labels; the second surfaces any unexpected labels that didn't trigger HALT but are still worth flagging (e.g. typos, custom labels).

// Step 0b: canonical SA-layer node counts
// (keep in sync with the Schema Reference above: every label a producer skill
//  writes and every L-level anchors on belongs here — L10 added Screen*, L11
//  Slice, L12 DomainError/ErrorPresentation, L13 CachePolicy/DegradationRule,
//  L8/L9 Decision. A future L14+ MUST extend this list in the same commit.)
UNWIND ['Module','UseCase','DomainEntity','DomainAttribute','Enumeration','EnumValue',
        'Form','FormField','Requirement','SystemRole','Component','ActivityStep',
        'FeatureRequest','Screen','ScreenState','ScreenEvent','Transition',
        'ScreenEffect','AnalyticsEvent','Slice','Decision','APIEndpoint',
        'DomainError','ErrorPresentation','CachePolicy','DegradationRule'] AS labelName
CALL {
  WITH labelName
  MATCH (n) WHERE labelName IN labels(n)
  RETURN count(n) AS cnt
}
RETURN labelName AS label, cnt AS count
ORDER BY labelName
// Step 0b (cont.): non-canonical labels still present in graph
// The NOT IN list = canonical SA labels + known neighbor-layer labels that are
// legitimate on a shared graph and must NOT be reported as drift:
//   BA family  — BusinessProcess..DataFlow, EntityState, GlossaryTerm, SystemContext
//   TL family  — Task, Wave, IntakeItem (written by tl-plan / tl-intake)
//   legacy SA  — RuntimeContract (flat-format runtime contracts)
// `cnt > 0` filters constraint-registered label tokens with zero nodes — those
// are schema residue, not findings.
CALL db.labels() YIELD label
WITH label
WHERE NOT label IN
   ['Module','UseCase','DomainEntity','DomainAttribute','Enumeration','EnumValue',
    'Form','FormField','Requirement','SystemRole','Component','ActivityStep',
    'FeatureRequest','Screen','ScreenState','ScreenEvent','Transition','ScreenEffect',
    'AnalyticsEvent','Slice','Decision','APIEndpoint','DomainError','ErrorPresentation',
    'CachePolicy','DegradationRule','BusinessProcess','WorkflowStep','BusinessEntity','BusinessRole',
    'BusinessRule','EntityAttribute','ProcessGroup','Term','Glossary','Stakeholder',
    'ExternalEntity','Document','DataFlow','EntityState','GlossaryTerm','SystemContext',
    'Task','Wave','IntakeItem','RuntimeContract']
CALL {
  WITH label
  MATCH (n) WHERE label IN labels(n)
  RETURN count(n) AS cnt
}
WITH label, cnt
WHERE cnt > 0
RETURN label, cnt AS count
ORDER BY label

Render in the report header as:

Pre-flight node counts:

Canonical SA labels:
  Module          : 4
  DomainEntity    : 15
  Requirement     : 29
  UseCase         : 24
  Form            : 14
  ...

Non-canonical labels detected (informational):
  (none, or list with "<-- review" hint)

Step 0c: Verify BA layer exists (for ba-cross / full)

// Pre-flight: count BA-layer nodes
MATCH (n)
WHERE n:BusinessProcess OR n:WorkflowStep OR n:BusinessEntity OR n:BusinessRole OR n:BusinessRule
RETURN labels(n)[0] AS label, count(n) AS count
ORDER BY label

If the result is empty:

  • level=ba-cross --> STOP, report that BA layer is not populated. User must run /nacl-ba-import-doc or /nacl-ba-from-board first.
  • level=full --> Run only L1-L13 (internal), skip XL6-XL9 with a WARNING in the report.

Step 0d: Verify exemption properties are populated

Before running checks, confirm that exemption properties exist in the graph. Include this table in the report header so the user can see which properties are set and which are missing.

// Pre-flight: exemption property coverage
MATCH (ff:FormField)
WITH count(ff) AS total,
     sum(CASE WHEN ff.field_category IS NOT NULL THEN 1 ELSE 0 END) AS has_prop
RETURN 'FormField.field_category' AS property, total, has_prop, total - has_prop AS missing
UNION ALL
MATCH (uc:UseCase)
WITH count(uc) AS total,
     sum(CASE WHEN uc.has_ui IS NOT NULL THEN 1 ELSE 0 END) AS has_prop
RETURN 'UseCase.has_ui' AS property, total, has_prop, total - has_prop AS missing
UNION ALL
MATCH (de:DomainEntity)
WITH count(de) AS total,
     sum(CASE WHEN de.shared IS NOT NULL THEN 1 ELSE 0 END) AS has_prop
RETURN 'DomainEntity.shared' AS property, total, has_prop, total - has_prop AS missing
UNION ALL
MATCH (sr:SystemRole)
WITH count(sr) AS total,
     sum(CASE WHEN sr.system_only IS NOT NULL THEN 1 ELSE 0 END) AS has_prop
RETURN 'SystemRole.system_only' AS property, total, has_prop, total - has_prop AS missing

If missing is high for any property, the exemption filters in L4-L6/XL8 will treat those nodes as non-exempt (defaulting to the strict check). This is correct behavior -- it means the SA skills haven't classified those nodes yet.

Escape-valve debt (INFO). The opt-in valves (anchor_exempt for L3.7, coverage_exempt for L3.8, overlap_accepted for L13.9) default to absent, so the table above cannot show them. Report how many nodes have pulled each valve and how many did so without the required reason, so the debt stays visible even when the gate is green:

// Pre-flight: escape-valve debt (INFO)
// Aggregate WITHOUT a grouping key so a valve with zero uses still returns its 0 row.
OPTIONAL MATCH (rq:Requirement) WHERE coalesce(rq.anchor_exempt, false) = true
WITH count(rq) AS exempted,
     sum(CASE WHEN rq IS NOT NULL AND trim(coalesce(toString(rq.anchor_exempt_reason), '')) = '' THEN 1 ELSE 0 END) AS without_reason
RETURN 'Requirement.anchor_exempt' AS valve, exempted, without_reason
UNION ALL
OPTIONAL MATCH (s:ActivityStep) WHERE coalesce(s.coverage_exempt, false) = true
WITH count(s) AS exempted,
     sum(CASE WHEN s IS NOT NULL AND trim(coalesce(toString(s.coverage_exempt_reason), '')) = '' THEN 1 ELSE 0 END) AS without_reason
RETURN 'ActivityStep.coverage_exempt' AS valve, exempted, without_reason
UNION ALL
OPTIONAL MATCH (cp:CachePolicy) WHERE coalesce(cp.overlap_accepted, false) = true
WITH count(cp) AS exempted,
     sum(CASE WHEN cp IS NOT NULL AND trim(coalesce(toString(cp.overlap_accepted_reason), '')) = '' THEN 1 ELSE 0 END) AS without_reason
RETURN 'CachePolicy.overlap_accepted' AS valve, exempted, without_reason

without_reason > 0 for coverage_exempt / overlap_accepted is also reported by L3.9 / L13.10.

To backfill missing exemption properties, use /nacl-sa-flags:

/nacl-sa-flags audit                           # confirm scope
/nacl-sa-flags backfill-all --detect-internal  # write safe defaults
/nacl-sa-validate full                         # re-run validation

nacl-sa-flags is the canonical orchestrator-tier skill for setting validation-only metadata. It writes only has_ui, system_only, shared, internal, field_category -- no domain semantics. After nacl-migrate-sa it is invoked automatically; after manual graph edits or skill-version upgrades, run it explicitly.


Severity Levels

Every detected problem is assigned a severity:

SeverityMeaningReport threshold
CRITICALSpecification is broken; blocks downstream workAny CRITICAL --> overall FAIL
WARNINGInconsistency that should be fixed but is not blocking5+ WARNINGs --> overall WARN
INFOObservation, optional improvementDoes not affect overall status

Computing the overall status is delegated — do not roll up ~40 check results by hand (miscounting "5+ WARNING" or missing one CRITICAL silently changes the gate verdict). Collect every finding as {check, severity, flags?} and pass them to the single-authority classifier; emit its overall token verbatim. It also re-applies the property-based exemption filters (L3.7/L3.8/L4.1/L5.1/L6.1/L9.1/L10.2/L10.6/L13.9/XL8.2) as a defense-in-depth net, so a finding whose Cypher omitted its coalesce(...) filter is still dropped before it can flip the gate. Pass the exemption properties the query returns as flags (L3.8: coverage_exempt; L13.9: overlap_accepted_1 and overlap_accepted_2 — the pair is exempt only when both are true). Equivalence pinned by classify-findings.test.mjs, next to the classifier script.

node nacl-core/scripts/classify-findings.mjs '{"findings":[{"check":"L1.1","severity":"CRITICAL"},{"check":"L4.1","severity":"CRITICAL","flags":{"field_category":"display"}}]}'
# stdout: full JSON (findings + counts + overall); stderr: the bare overall token

Validation Levels -- Internal (L1-L13)

Level 1: Data Consistency

Goal: Verify that property types, mandatory fields, and naming conventions are uniform across all SA nodes.

Check 1.1: Nodes missing mandatory properties

Every SA node type has mandatory properties. Find nodes that lack them.

// L1.1 -- Severity: CRITICAL
// Nodes missing mandatory 'id' or 'name' property
MATCH (n)
WHERE (n:Module OR n:UseCase OR n:DomainEntity OR n:Form OR n:SystemRole OR n:Component)
  AND (n.id IS NULL OR n.name IS NULL)
RETURN labels(n)[0] AS node_type,
       coalesce(n.id, 'NO ID') AS id,
       coalesce(n.name, 'NO NAME') AS name,
       'Missing mandatory property: id or name' AS problem
Check 1.2: DomainAttributes missing type

Every DomainAttribute must have a data_type property.

// L1.2 -- Severity: CRITICAL
// DomainAttributes without data_type
MATCH (de:DomainEntity)-[:HAS_ATTRIBUTE]->(da:DomainAttribute)
WHERE da.data_type IS NULL
RETURN de.name AS entity, da.name AS attribute, da.id AS attr_id,
       'DomainAttribute missing data_type' AS problem
Check 1.3: Duplicate IDs within a label

IDs must be unique within each node type (enforced by constraints, but check anyway).

// L1.3 -- Severity: CRITICAL
// Duplicate IDs across SA node types
UNWIND ['Module','UseCase','DomainEntity','DomainAttribute','Enumeration',
        'Form','FormField','Requirement','SystemRole','Component'] AS labelName
CALL {
  WITH labelName
  MATCH (n)
  WHERE labelName IN labels(n)
  WITH labelName, n.id AS nodeId, count(*) AS cnt
  WHERE cnt > 1
  RETURN labelName AS node_type, nodeId AS id, cnt AS duplicate_count
}
RETURN node_type, id, duplicate_count
Check 1.4: Inconsistent enumeration values (duplicate or empty)

Canonical writer (/nacl-sa-domain) populates EnumValue.value. Some legacy or hand-written graphs may use .code or .label instead. To avoid false positives, this check coalesces all three property names; if none yields a non-empty string, the value is considered empty.

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-validate
Source
github.com/itsalt/nacl