nacl-sa-validate
SkillAI & modelsValidate 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.
No other account needed.
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
| Tool | Usage |
|---|---|
mcp__neo4j__read-cypher | ALL validation queries (read-only) |
mcp__neo4j__get-schema | Introspect 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
| Parameter | Values | Description |
|---|---|---|
level | internal | L1-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-cross | XL6-XL9: BA-to-SA cross-layer coverage | |
full (default) | All levels: L1-L13 + XL6-XL9 | |
--scope | intra-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-xxx | Limit 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.valueproperty)/nacl-sa-uc-- writes:UseCase,:Requirement,:Form,:FormField,:ActivityStep;slicescommand writes:Slicewith edgesHAS_SLICE,COVERS,CALLS,VERIFIED_BY(seegraph-infra/schema/sa-schema.cypher§ 3-ter; note theCALLSname is shared withScreenEffect→APIEndpoint— every L11 query label-qualifies the source as(sl:Slice));errorscommand writes:DomainError,:ErrorPresentationwith edgesHAS_ERROR,MAY_RAISE,HANDLES,PRESENTED_AS,SHOWS(see § 3-quater; all five names are unshared — no label-qualification hazard);resiliencecommand writes:CachePolicy,:DegradationRulewith edgesHAS_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-machinecommand writes:Screen,:ScreenState,:ScreenEvent,:Transition,:ScreenEffect,:AnalyticsEventwith edgesHAS_SCREEN,RENDERS,HAS_STATE,HAS_EVENT,HAS_TRANSITION,FROM_STATE,TO_STATE,ON_EVENT,TRIGGERS,CALLS,NAVIGATES_TO,EMITS(seegraph-infra/schema/sa-schema.cypher§ 3-bis; noteHAS_STATE/TRIGGERSnames 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)::Decisionnode, 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 withcoalesce(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-canonical | Canonical |
|---|---|
:SAModule | :Module |
:SAEntity | :DomainEntity |
:SARequirement | :Requirement |
:SAActor | :SystemRole |
:SAComponent | :Component |
edge TRACES_TO | use 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:
- STOP -- validation is impossible without data in the graph.
- Suggest the user runs
/nacl-sa-architector/nacl-sa-domainfirst. - Explain that
/nacl-sa-validateworks 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:
| Condition | Action |
|---|---|
canonical_present non-empty AND dialect_present empty | OK -- continue to Step 0b |
canonical_present empty AND dialect_present non-empty | HALT -- emit drift report below; do NOT run any L*/XL* checks |
| Both non-empty | HALT -- mixed schema is worse than pure dialect; emit drift report; do NOT run checks |
dialect_handoff_edges non-empty AND no canonical handoff edges | HALT -- 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-docor/nacl-ba-from-boardfirst.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:
| Severity | Meaning | Report threshold |
|---|---|---|
| CRITICAL | Specification is broken; blocks downstream work | Any CRITICAL --> overall FAIL |
| WARNING | Inconsistency that should be fixed but is not blocking | 5+ WARNINGs --> overall WARN |
| INFO | Observation, optional improvement | Does 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