Install
$ agentstack add skill-itsalt-nacl-nacl-sa-finalize ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
/nacl-sa-finalize -- Specification Finalization (Graph)
Purpose
Finalize the specification by computing all summary artifacts from Neo4j aggregation queries: statistics summary, glossary extract, ADR records, traceability matrix, and readiness assessment. No file reading -- everything is aggregated from the graph. Data stays in the graph and is viewable via /nacl-render or /nacl-publish.
Shared references: nacl-core/SKILL.md
Neo4j Tools
| Tool | Usage | |------|-------| | mcp__neo4j__read-cypher | All aggregation queries (statistics, glossary, readiness, traceability) | | mcp__neo4j__write-cypher | Create/update :Decision nodes + JUSTIFIES edges; backfill legacy Requirement{type:'adr'} → :Decision | | mcp__neo4j__get-schema | Introspect current graph schema before finalization |
Modes
Mode full (default)
Full finalization: all five phases sequentially.
When: After successful validation (/nacl-sa-validate full).
Mode module
Finalize a single module (scoped statistics + readiness).
When: After completing work on one module.
Parameter: module -- module ID (e.g. mod-orders).
Mode stats-only
Statistics and readiness only -- no ADR creation, no glossary update.
When: User wants to see current progress without modifying the graph.
Workflow Overview
+-----------------+ +-----------------+ +-----------------+
| Phase 1 | | Phase 2 | | Phase 3 |
| Statistics |--->| Glossary + |--->| Traceability |
| Summary | | ADR | | Matrix |
+-----------------+ +-----------------+ +-----------------+
|
v
+-----------------+ +-----------------+
| Phase 5 | | Phase 4 |
| Report to | STOP: "No modules in graph. Run /nacl-sa-architect first."
3. Verify UseCase nodes exist: `MATCH (uc:UseCase) RETURN count(uc) AS cnt`
- If cnt = 0 -> WARN: "No use cases found. Statistics will be incomplete."
4. If mode = `module`, verify the module exists:
```cypher
MATCH (m:Module {id: $moduleId}) RETURN m
```
- Not found -> STOP: "Module {moduleId} not found in graph."
---
## Phase 1: Statistics Summary
**Goal:** Compute aggregate counts across all specification node types.
### Query: `sa_statistics_summary`
Source: `graph-infra/queries/sa-queries.cypher`
```cypher
MATCH (m:Module) WITH count(m) AS modules
MATCH (uc:UseCase) WITH modules, count(uc) AS ucs
MATCH (de:DomainEntity) WITH modules, ucs, count(de) AS entities
MATCH (da:DomainAttribute) WITH modules, ucs, entities, count(da) AS attributes
MATCH (f:Form) WITH modules, ucs, entities, attributes, count(f) AS forms
MATCH (ff:FormField) WITH modules, ucs, entities, attributes, forms, count(ff) AS fields
MATCH (sr:SystemRole) WITH modules, ucs, entities, attributes, forms, fields, count(sr) AS roles
MATCH (rq:Requirement) WITH modules, ucs, entities, attributes, forms, fields, roles, count(rq) AS requirements
MATCH (c:Component) WITH modules, ucs, entities, attributes, forms, fields, roles, requirements, count(c) AS components
RETURN modules, ucs, entities, attributes, forms, fields, roles, requirements, components;
Additional counts (run separately)
Enumerations:
MATCH (en:Enumeration) RETURN count(en) AS enumerations;
ADRs:
MATCH (rq:Requirement {type: 'adr'}) RETURN count(rq) AS adrs;
Open questions (if stored as Requirement with type='question'):
MATCH (rq:Requirement {type: 'question', status: 'open'}) RETURN count(rq) AS open_questions;
UC detail breakdown:
MATCH (uc:UseCase)
RETURN count(uc) AS total,
count(CASE WHEN uc.priority = 'primary' THEN 1 END) AS primary_ucs,
count(CASE WHEN uc.priority = 'secondary' THEN 1 END) AS secondary_ucs,
count(CASE WHEN uc.detail_status = 'complete' THEN 1 END) AS detailed_ucs;
Connected-spec extension counts (2.15+)
Query: sa_statistics_extensions
Source: graph-infra/queries/sa-queries.cypher
RETURN
COUNT { (n:Decision) } AS decisions,
COUNT { (n:Screen) } AS screens,
COUNT { (n:ScreenState) } AS screen_states,
COUNT { (n:ScreenEvent) } AS screen_events,
COUNT { (n:Transition) } AS screen_transitions,
COUNT { (n:ScreenEffect) } AS screen_effects,
COUNT { (n:AnalyticsEvent) } AS analytics_events,
COUNT { (n:Slice) } AS slices,
COUNT { (n:DomainError) } AS domain_errors,
COUNT { (n:ErrorPresentation) } AS error_presentations,
COUNT { (n:CachePolicy) } AS cache_policies,
COUNT { (n:DegradationRule) } AS degradation_rules;
These layers are opt-in (vacuous PASS in /nacl-sa-validate L10-L13 when empty). All-zero counts for a layer mean "not adopted", not "incomplete" — report them as such, do not treat zero as a readiness failure.
Output format
Present to user as a table:
| Metric | Count |
|--------------------|-------|
| Modules | {N} |
| Domain Entities | {N} |
| Attributes (total) | {N} |
| Enumerations | {N} |
| Roles | {N} |
| Use Cases (total) | {N} |
| - Primary | {N} |
| - Secondary | {N} |
| - Detailed | {N}/{total} |
| Forms | {N} |
| Form Fields | {N} |
| Requirements | {N} |
| Components | {N} |
| ADRs | {N} |
| Open Questions | {N} |
| Decisions | {N} |
| Screens | {N} ({N} states, {N} transitions) |
| Behavior Slices | {N} |
| Domain Errors | {N} ({N} presentations) |
| Cache Policies | {N} |
| Degradation Rules | {N} |
Extension rows with zero counts are rendered as — (not adopted) instead of 0.
For mode module, scope all queries with:
MATCH (m:Module {id: $moduleId})-[:CONTAINS_UC]->(uc:UseCase) ...
MATCH (m:Module {id: $moduleId})-[:CONTAINS_ENTITY]->(de:DomainEntity) ...
Phase 2: Glossary Extract + ADR
Goal: Extract glossary terms from graph; compile Architecture Decision Records.
2a: Glossary Extract
Query: sa_glossary_extract
Source: graph-infra/queries/sa-queries.cypher
MATCH (de:DomainEntity)
RETURN 'DomainEntity' AS source_type, de.id AS id, de.name AS term
UNION ALL
MATCH (en:Enumeration)
RETURN 'Enumeration' AS source_type, en.id AS id, en.name AS term
UNION ALL
MATCH (sr:SystemRole)
RETURN 'SystemRole' AS source_type, sr.id AS id, sr.name AS term;
Cross-reference with BA glossary (if BA layer exists)
MATCH (g:GlossaryTerm)
OPTIONAL MATCH (g)-[:REFERS_TO]->(de:DomainEntity)
RETURN g.id AS glo_id, g.term AS ba_term, g.definition AS definition,
de.id AS sa_entity_id, de.name AS sa_entity_name;
Actions
- Run
sa_glossary_extractto collect all SA-layer terms - For each term without a definition, generate a one-sentence definition from its graph context
(attributes, relationships, module membership)
- Cross-reference with BA GlossaryTerm nodes to map BA term -> SA entity
- Present combined glossary to user (do NOT write to files)
2b: Architecture Decisions (graph-native)
Architecture decisions are stored as first-class :Decision nodes (level:'architecture', created_by:'nacl-sa-finalize') linked to the artifacts they justify via JUSTIFIES — the same provenance model that nacl-sa-feature and nacl-tl-fix write. This is the authority; markdown ADRs are projections. (Legacy projects stored ADRs as Requirement {type:'adr'} with no link to what they justified — those are migrated below so the year-later "why" query reaches them.)
Read existing decisions (both modern and legacy)
MATCH (d:Decision)
RETURN d.id AS id, d.title AS title, d.rationale AS rationale,
d.status AS status, d.level AS level
ORDER BY d.id;
// Legacy ADRs not yet migrated to :Decision
MATCH (rq:Requirement {type: 'adr'})
RETURN rq.id AS id, rq.description AS title, rq.context AS context,
rq.decision AS decision, rq.alternatives AS alternatives,
rq.consequences AS consequences
ORDER BY rq.id;
One-time backfill: legacy Requirement{type:'adr'} → :Decision
For each legacy ADR, create a :Decision carrying the same content (rationale from decision+consequences), source:'ADR-NNN (imported)', and — where the ADR's subject is identifiable — a JUSTIFIES edge to the module/entity/role it concerns. Present to the user before writing; never delete the legacy node (keep lineage), just mark it migrated_to: .
// mcp__neo4j__write-cypher (per legacy ADR, after user confirms the JUSTIFIES target)
MATCH (rq:Requirement {id: $adrId, type:'adr'})
MERGE (d:Decision {id: $decId})
SET d.title = rq.description, d.context = rq.context,
d.chosen = rq.decision, d.rationale = coalesce(rq.decision,'') + ' — ' + coalesce(rq.consequences,''),
d.alternatives_considered = CASE WHEN rq.alternatives IS NULL THEN [] ELSE [rq.alternatives] END,
d.status = coalesce(rq.status,'accepted'), d.created_at = coalesce(rq.created_at, datetime()),
d.created_by = 'nacl-sa-finalize', d.source = rq.id + ' (imported)', d.level = 'architecture'
SET rq.migrated_to = $decId
RETURN d.id;
Also import any standalone docs/adr/*.md files the same way (source:' (imported)').
Backfill: FeatureRequests without a Decision (provenance gap-closure)
Projects created before the provenance feature have :FeatureRequest nodes with no linked :Decision — nacl-sa-validate L9.1 flags every one. Close the gap by writing one honest :Decision per FR, with rationale drawn from the project's own recorded text (never invented). Full runbook: nacl-tl-core/references/provenance-gap-closure.md.
Per FR without IMPLEMENTS -> :Decision, resolve rationale in priority order:
- FR node
description(if non-empty). - FR markdown at
markdown_path— the## Feature Descriptionsection, plus the
Source: metadata line (the original /sa-feature "..." intent). This is real recorded rationale, not fabrication.
git logof the FR markdown / its UCs.- None recoverable → do NOT invent. Grandfather via
nacl-sa-flags-style flag
(decision_exempt=true, decision_exempt_reason, decision_exempt_since), which L9.1 skips and L9.5 surfaces as visible debt.
For a recoverable FR, write the Decision and wire it to the FR's own scope:
// mcp__neo4j__write-cypher (per FR; $rationale extracted from md/description; $decId = next DEC-NNN)
MATCH (fr:FeatureRequest {id: $frId})
MERGE (d:Decision {id: $decId})
SET d.title = coalesce(fr.title, $frId), d.chosen = $chosen,
d.rationale = $rationale, // REQUIRED, non-empty, from project's own records
d.context = $context, d.alternatives_considered = [],
d.status = CASE WHEN fr.status IN ['shipped','implemented','dev-complete'] THEN 'accepted' ELSE 'accepted' END,
d.created_at = coalesce(fr.created_at, datetime()),
d.created_by = 'nacl-sa-finalize',
d.source = $frId + ' (backfilled from ' + $rationaleSource + ')', // 'description' | 'markdown' | 'git'
d.level = 'feature'
MERGE (fr)-[:IMPLEMENTS]->(d)
WITH d, fr
MATCH (fr)-[r:INCLUDES_UC]->(uc:UseCase)
MERGE (d)-[:JUSTIFIES {role: CASE WHEN r.kind = 'new' THEN 'creates' ELSE 'shapes' END}]->(uc)
RETURN d.id;
Verify-before-bulk: backfill ONE FR, confirm the Decision reads honestly and sa-validate --scope L9 clears for it, then batch the rest. Present a sample to the user before the bulk write.
Detect implicit decisions not yet recorded
Check for architectural patterns that imply a decision worth recording:
MATCH (m:Module) RETURN count(m) AS module_count, collect({id:m.id, name:m.name}) AS modules;
MATCH (sr:SystemRole) RETURN count(sr) AS role_count, collect({id:sr.id, name:sr.name}) AS roles;
Create missing Decision nodes (with JUSTIFIES)
For each detected decision not yet in the graph, allocate DEC-NNN (max(toInteger(replace(d.id,'DEC-',''))) + 1) and write with mcp__neo4j__write-cypher, linking it to every artifact it shaped:
MERGE (d:Decision {id: $decId})
SET d.title = $title, d.chosen = $chosen, d.rationale = $rationale,
d.context = $context, d.alternatives_considered = $alternatives,
d.status = 'accepted', d.created_at = coalesce(d.created_at, datetime()),
d.created_by = 'nacl-sa-finalize', d.source = 'finalize', d.level = 'architecture'
WITH d
UNWIND $justifiesIds AS x
MATCH (target {id: x}) // Module / DomainEntity / SystemRole / …
MERGE (d)-[:JUSTIFIES {role:'shapes'}]->(target)
RETURN d.id;
Typical decision categories to check
- Module decomposition rationale (
JUSTIFIES→ the Modules) - Domain model key decisions — entity boundaries, shared vs separate (
JUSTIFIES→ the DomainEntities) - Role model and authorization approach (
JUSTIFIES→ the SystemRoles) - Interface architecture decisions (SPA, navigation patterns)
- Technology-specific decisions
Present each new Decision to the user for confirmation before writing. Every Decision MUST have a non-empty rationale and at least one JUSTIFIES edge — nacl-sa-validate L9 enforces this.
IMPORTANT: In stats-only mode, SKIP Phase 2 entirely.
Phase 3: Traceability Matrix
Goal: Compute full BA-to-SA traceability and coverage from the graph.
Query: handoff_traceability_matrix
Source: graph-infra/queries/handoff-queries.cypher
MATCH (ws:WorkflowStep)-[:AUTOMATES_AS]->(uc:UseCase)
RETURN 'Step->UC' AS category, ws.id AS ba_id, ws.function_name AS ba_name, uc.id AS sa_id, uc.name AS sa_name
UNION ALL
MATCH (be:BusinessEntity)-[:REALIZED_AS]->(de:DomainEntity)
RETURN 'Entity->Domain' AS category, be.id AS ba_id, be.name AS ba_name, de.id AS sa_id, de.name AS sa_name
UNION ALL
MATCH (br:BusinessRole)-[:MAPPED_TO]->(sr:SystemRole)
RETURN 'Role->SysRole' AS category, br.id AS ba_id, br.full_name AS ba_name, sr.id AS sa_id, sr.name AS sa_name
UNION ALL
MATCH (brq:BusinessRule)-[:IMPLEMENTED_BY]->(rq:Requirement)
RETURN 'Rule->Req' AS category, brq.id AS ba_id, brq.name AS ba_name, rq.id AS sa_id, rq.description AS sa_name;
Query: handoff_coverage_stats
Source: graph-infra/queries/handoff-queries.cypher
MATCH (ws:WorkflowStep {stereotype: "Автоматизируется"})
WITH count(ws) AS total_auto
OPTIONAL MATCH (ws2:WorkflowStep {stereotype: "Автоматизируется"})-[:AUTOMATES_AS]->(:UseCase)
WITH total_auto, count(ws2) AS covered_auto
WITH total_auto, covered_auto,
CASE WHEN total_auto > 0 THEN round(100.0 * covered_auto / total_auto) ELSE 0 END AS step_pct
MATCH (be:BusinessEntity {type: "Бизнес-объект"})
WITH total_auto, covered_auto, step_pct, count(be) AS total_entities
OPTIONAL MATCH (be2:BusinessEntity {type: "Бизнес-объект"})-[:REALIZED_AS]->(:DomainEntity)
WITH total_auto, covered_auto, step_pct, total_entities, count(be2) AS covered_entities
WITH total_auto, covered_auto, step_pct, total_entities, covered_entities,
CASE WHEN total_entities > 0 THEN round(100.0 * covered_entities / total_entities) ELSE 0 END AS entity_pct
MATCH (br:BusinessRole)
WITH step_pct, entity_pct, total_auto, covered_auto, total_entities, covered_entities, count(br) AS total_roles
OPTIONAL MATCH (br2:BusinessRole)-[:MAPPED_TO]->(:SystemRole)
WITH step_pct, entity_pct, total_auto, covered_auto, total_entities, covered_entities, total_roles, count(br2) AS covered_roles
WITH step_pct, entity_pct, total_auto, covered_auto, total_entities, covered_entities, total_roles, covered_roles,
CASE WHEN total_roles > 0 THEN round(100.0 * covered_roles / total_roles) ELSE 0 END
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [ITSalt](https://github.com/ITSalt)
- **Source:** [ITSalt/NaCl](https://github.com/ITSalt/NaCl)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.