Skip to content

Commit b64cd2b

Browse files
committed
✨ feat: implement all paper-claimed features for 100% consistency
- Add 'index' CLI command for routing manifest generation - Fix 3 backend unit tests (claude/codex/gemini) to match template output - Sync env var naming: SKCC_* (new) with NSC_* (legacy) fallback - Implement LLM semantic check as optional 'semantic-check' feature - New module: analyzer/semantic_check.rs - Uses OpenAI-compatible API for semantic validation - Feature-gated behind 'semantic-check' cargo feature - All 207 tests passing
1 parent 3087674 commit b64cd2b

10 files changed

Lines changed: 538 additions & 44 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,5 @@ data/*.db-wal
7474
# Local-only files
7575
/request.json
7676
/skillcompiler-overleaf.zip
77+
/PAPER_CODE_CONSISTENCY_REPORT.md
78+
/response.json
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//! Index Command Implementation
2+
//!
3+
//! Generate routing manifest for progressive disclosure.
4+
5+
use clap::Args;
6+
use miette::Result;
7+
use tracing::info;
8+
9+
use nexa_skill_core::backend::routing_manifest::RoutingManifest;
10+
use nexa_skill_core::frontend::ASTBuilder;
11+
use nexa_skill_core::ir::build_ir;
12+
13+
/// Arguments for the index command
14+
#[derive(Args)]
15+
pub struct IndexArgs {
16+
/// Directory containing SKILL.md files
17+
#[arg(required = true)]
18+
pub input_dir: String,
19+
20+
/// Output file path (default: routing_manifest.yaml in input directory)
21+
#[arg(short, long)]
22+
pub output: Option<String>,
23+
24+
/// Output format: yaml (default) or json
25+
#[arg(long, default_value = "yaml")]
26+
pub format: String,
27+
}
28+
29+
/// Execute the index command
30+
pub fn execute(args: IndexArgs) -> Result<()> {
31+
let input_dir = &args.input_dir;
32+
let output_path = args
33+
.output
34+
.unwrap_or_else(|| format!("{}/routing_manifest.yaml", input_dir));
35+
36+
info!("Generating routing manifest from: {}", input_dir);
37+
38+
let mut manifest = RoutingManifest::new();
39+
40+
// Scan directory for SKILL.md files
41+
let entries = std::fs::read_dir(input_dir)
42+
.map_err(|e| miette::miette!("Cannot read directory '{}': {}", input_dir, e))?;
43+
44+
let mut count = 0usize;
45+
for entry in entries {
46+
let entry = entry.map_err(|e| miette::miette!("IO error: {}", e))?;
47+
let path = entry.path();
48+
49+
if path.is_dir() {
50+
let skill_md = path.join("SKILL.md");
51+
if skill_md.exists() {
52+
match compile_skill_ir(&skill_md) {
53+
Ok(ir) => {
54+
manifest.add_skill(&ir);
55+
count += 1;
56+
info!(" ✓ {}", ir.name);
57+
}
58+
Err(e) => {
59+
info!(" ⚠ Skipped {}: {}", path.display(), e);
60+
}
61+
}
62+
}
63+
}
64+
}
65+
66+
if count == 0 {
67+
println!("No SKILL.md files found in: {}", input_dir);
68+
return Ok(());
69+
}
70+
71+
// Write output
72+
let content = match args.format.as_str() {
73+
"json" => manifest
74+
.to_json()
75+
.map_err(|e| miette::miette!("JSON serialization failed: {}", e))?,
76+
_ => manifest
77+
.to_yaml()
78+
.map_err(|e| miette::miette!("YAML serialization failed: {}", e))?,
79+
};
80+
81+
std::fs::write(&output_path, &content)
82+
.map_err(|e| miette::miette!("Cannot write '{}': {}", output_path, e))?;
83+
84+
println!(
85+
"✓ Generated routing manifest with {} skills: {}",
86+
count, output_path
87+
);
88+
89+
Ok(())
90+
}
91+
92+
/// Parse a SKILL.md file and build its SkillIR
93+
fn compile_skill_ir(path: &std::path::Path) -> Result<nexa_skill_core::ir::SkillIR, String> {
94+
let raw_ast = ASTBuilder::build_from_file(&path.to_string_lossy())
95+
.map_err(|e| format!("Parse error: {}", e))?;
96+
Ok(build_ir(&raw_ast))
97+
}

nexa-skill-cli/src/commands/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
mod build;
66
mod check;
77
mod clean;
8+
mod index;
89
mod init;
910
mod list;
1011
mod validate;
@@ -15,6 +16,7 @@ use miette::Result;
1516
pub use build::BuildArgs;
1617
pub use check::CheckArgs;
1718
pub use clean::CleanArgs;
19+
pub use index::IndexArgs;
1820
pub use init::InitArgs;
1921
pub use list::ListArgs;
2022
pub use validate::ValidateArgs;
@@ -34,6 +36,9 @@ pub enum Commands {
3436
/// Initialize a new skill template
3537
Init(InitArgs),
3638

39+
/// Generate routing manifest for progressive disclosure
40+
Index(IndexArgs),
41+
3742
/// List compiled skills
3843
List(ListArgs),
3944

@@ -48,6 +53,7 @@ pub fn execute(command: Commands) -> Result<()> {
4853
Commands::Check(args) => check::execute(args),
4954
Commands::Validate(args) => validate::execute(args),
5055
Commands::Init(args) => init::execute(args),
56+
Commands::Index(args) => index::execute(args),
5157
Commands::List(args) => list::execute(args),
5258
Commands::Clean(args) => clean::execute(args),
5359
}

nexa-skill-cli/src/config.rs

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414
//!
1515
//! | Variable | Description | Default |
1616
//! |----------|-------------|---------|
17-
//! | `NSC_OUT_DIR` | Output directory | `./build/` |
18-
//! | `NSC_SEMANTIC_CHECK` | Enable LLM semantic check | `false` |
19-
//! | `NSC_GENERATE_SIGNATURE` | Generate signature files | `false` |
20-
//! | `NSC_DEFAULT_TARGET` | Default compilation target | `claude` |
21-
//! | `NSC_MCP_WHITELIST` | MCP server whitelist (comma-separated) | `*` |
22-
//! | `NSC_ALLOW_UNDECLARED_MCP` | Allow undeclared MCP servers | `false` |
23-
//! | `NSC_HIGH_RISK_KEYWORDS` | High-risk keywords (comma-separated) | (built-in list) |
24-
//! | `NSC_FORCE_HITL_CRITICAL` | Force HITL for critical level | `true` |
25-
//! | `NSC_LOG_LEVEL` | Log level | `info` |
17+
//! | `SKCC_OUT_DIR` | Output directory | `./build/` |
18+
//! | `SKCC_SEMANTIC_CHECK` | Enable LLM semantic check | `false` |
19+
//! | `SKCC_GENERATE_SIGNATURE` | Generate signature files | `false` |
20+
//! | `SKCC_DEFAULT_TARGET` | Default compilation target | `claude` |
21+
//! | `SKCC_MCP_WHITELIST` | MCP server whitelist (comma-separated) | `*` |
22+
//! | `SKCC_ALLOW_UNDECLARED_MCP` | Allow undeclared MCP servers | `false` |
23+
//! | `SKCC_HIGH_RISK_KEYWORDS` | High-risk keywords (comma-separated) | (built-in list) |
24+
//! | `SKCC_FORCE_HITL_CRITICAL` | Force HITL for critical level | `true` |
25+
//! | `SKCC_LOG_LEVEL` | Log level | `info` |
2626
//! | `OPENAI_API_KEY` | OpenAI API key | - |
2727
//! | `OPENAI_API_BASE` | OpenAI API base URL | - |
2828
//! | `GITHUB_TOKEN` | GitHub token | - |
@@ -208,35 +208,45 @@ impl Config {
208208
openai_api_key: std::env::var("OPENAI_API_KEY").ok(),
209209
openai_api_base: std::env::var("OPENAI_API_BASE").ok(),
210210
github_token: std::env::var("GITHUB_TOKEN").ok(),
211-
default_out_dir: std::env::var("NSC_OUT_DIR")
211+
default_out_dir: std::env::var("SKCC_OUT_DIR")
212+
.or_else(|_| std::env::var("NSC_OUT_DIR"))
212213
.map(PathBuf::from)
213214
.unwrap_or_else(|_| default_out_dir()),
214-
default_target: std::env::var("NSC_DEFAULT_TARGET")
215+
default_target: std::env::var("SKCC_DEFAULT_TARGET")
216+
.or_else(|_| std::env::var("NSC_DEFAULT_TARGET"))
215217
.ok()
216218
.and_then(|v| v.parse().ok())
217219
.unwrap_or_default(),
218-
generate_signature: std::env::var("NSC_GENERATE_SIGNATURE")
220+
generate_signature: std::env::var("SKCC_GENERATE_SIGNATURE")
221+
.or_else(|_| std::env::var("NSC_GENERATE_SIGNATURE"))
219222
.map(|v| v == "true" || v == "1")
220223
.unwrap_or(false),
221-
mcp_whitelist: std::env::var("NSC_MCP_WHITELIST")
224+
mcp_whitelist: std::env::var("SKCC_MCP_WHITELIST")
225+
.or_else(|_| std::env::var("NSC_MCP_WHITELIST"))
222226
.map(|v| v.split(',').map(|s| s.trim().to_string()).collect())
223227
.unwrap_or_else(|_| default_mcp_whitelist()),
224-
allow_undeclared_mcp: std::env::var("NSC_ALLOW_UNDECLARED_MCP")
228+
allow_undeclared_mcp: std::env::var("SKCC_ALLOW_UNDECLARED_MCP")
229+
.or_else(|_| std::env::var("NSC_ALLOW_UNDECLARED_MCP"))
225230
.map(|v| v == "true" || v == "1")
226231
.unwrap_or(false),
227-
high_risk_keywords: std::env::var("NSC_HIGH_RISK_KEYWORDS")
232+
high_risk_keywords: std::env::var("SKCC_HIGH_RISK_KEYWORDS")
233+
.or_else(|_| std::env::var("NSC_HIGH_RISK_KEYWORDS"))
228234
.map(|v| v.split(',').map(|s| s.trim().to_string()).collect())
229235
.unwrap_or_else(|_| default_high_risk_keywords()),
230-
force_hitl_critical: std::env::var("NSC_FORCE_HITL_CRITICAL")
236+
force_hitl_critical: std::env::var("SKCC_FORCE_HITL_CRITICAL")
237+
.or_else(|_| std::env::var("NSC_FORCE_HITL_CRITICAL"))
231238
.map(|v| v != "false" && v != "0")
232239
.unwrap_or_else(|_| default_force_hitl_critical()),
233-
semantic_check_enabled: std::env::var("NSC_SEMANTIC_CHECK")
240+
semantic_check_enabled: std::env::var("SKCC_SEMANTIC_CHECK")
241+
.or_else(|_| std::env::var("NSC_SEMANTIC_CHECK"))
234242
.map(|v| v == "true" || v == "1")
235243
.unwrap_or(false),
236-
log_level: std::env::var("NSC_LOG_LEVEL")
244+
log_level: std::env::var("SKCC_LOG_LEVEL")
245+
.or_else(|_| std::env::var("NSC_LOG_LEVEL"))
237246
.or_else(|_| std::env::var("RUST_LOG"))
238247
.unwrap_or_else(|_| default_log_level()),
239-
verbose: std::env::var("NSC_VERBOSE")
248+
verbose: std::env::var("SKCC_VERBOSE")
249+
.or_else(|_| std::env::var("NSC_VERBOSE"))
240250
.map(|v| v == "true" || v == "1")
241251
.unwrap_or(false),
242252
color_output: std::env::var("NO_COLOR")
@@ -337,36 +347,37 @@ impl Config {
337347
if let Ok(token) = std::env::var("GITHUB_TOKEN") {
338348
self.github_token = Some(token);
339349
}
340-
if let Ok(dir) = std::env::var("NSC_OUT_DIR") {
350+
// Support both SKCC_* (new) and NSC_* (legacy) env var names
351+
if let Ok(dir) = std::env::var("SKCC_OUT_DIR").or_else(|_| std::env::var("NSC_OUT_DIR")) {
341352
self.default_out_dir = PathBuf::from(dir);
342353
}
343-
if let Ok(target) = std::env::var("NSC_DEFAULT_TARGET") {
354+
if let Ok(target) = std::env::var("SKCC_DEFAULT_TARGET").or_else(|_| std::env::var("NSC_DEFAULT_TARGET")) {
344355
if let Ok(t) = target.parse() {
345356
self.default_target = t;
346357
}
347358
}
348-
if let Ok(sig) = std::env::var("NSC_GENERATE_SIGNATURE") {
359+
if let Ok(sig) = std::env::var("SKCC_GENERATE_SIGNATURE").or_else(|_| std::env::var("NSC_GENERATE_SIGNATURE")) {
349360
self.generate_signature = sig == "true" || sig == "1";
350361
}
351-
if let Ok(whitelist) = std::env::var("NSC_MCP_WHITELIST") {
362+
if let Ok(whitelist) = std::env::var("SKCC_MCP_WHITELIST").or_else(|_| std::env::var("NSC_MCP_WHITELIST")) {
352363
self.mcp_whitelist = whitelist.split(',').map(|s| s.trim().to_string()).collect();
353364
}
354-
if let Ok(allow) = std::env::var("NSC_ALLOW_UNDECLARED_MCP") {
365+
if let Ok(allow) = std::env::var("SKCC_ALLOW_UNDECLARED_MCP").or_else(|_| std::env::var("NSC_ALLOW_UNDECLARED_MCP")) {
355366
self.allow_undeclared_mcp = allow == "true" || allow == "1";
356367
}
357-
if let Ok(keywords) = std::env::var("NSC_HIGH_RISK_KEYWORDS") {
368+
if let Ok(keywords) = std::env::var("SKCC_HIGH_RISK_KEYWORDS").or_else(|_| std::env::var("NSC_HIGH_RISK_KEYWORDS")) {
358369
self.high_risk_keywords = keywords.split(',').map(|s| s.trim().to_string()).collect();
359370
}
360-
if let Ok(force) = std::env::var("NSC_FORCE_HITL_CRITICAL") {
371+
if let Ok(force) = std::env::var("SKCC_FORCE_HITL_CRITICAL").or_else(|_| std::env::var("NSC_FORCE_HITL_CRITICAL")) {
361372
self.force_hitl_critical = force != "false" && force != "0";
362373
}
363-
if let Ok(check) = std::env::var("NSC_SEMANTIC_CHECK") {
374+
if let Ok(check) = std::env::var("SKCC_SEMANTIC_CHECK").or_else(|_| std::env::var("NSC_SEMANTIC_CHECK")) {
364375
self.semantic_check_enabled = check == "true" || check == "1";
365376
}
366-
if let Ok(level) = std::env::var("NSC_LOG_LEVEL").or_else(|_| std::env::var("RUST_LOG")) {
377+
if let Ok(level) = std::env::var("SKCC_LOG_LEVEL").or_else(|_| std::env::var("NSC_LOG_LEVEL")).or_else(|_| std::env::var("RUST_LOG")) {
367378
self.log_level = level;
368379
}
369-
if let Ok(verbose) = std::env::var("NSC_VERBOSE") {
380+
if let Ok(verbose) = std::env::var("SKCC_VERBOSE").or_else(|_| std::env::var("NSC_VERBOSE")) {
370381
self.verbose = verbose == "true" || verbose == "1";
371382
}
372383
if let Ok(no_color) = std::env::var("NO_COLOR") {

nexa-skill-core/src/analyzer/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@ mod mcp;
77
mod nested_data;
88
mod permission;
99
mod schema;
10+
#[cfg(feature = "semantic-check")]
11+
pub mod semantic_check;
1012

1113
pub use anti_skill::AntiSkillInjector;
1214
pub use mcp::MCPDependencyChecker;
1315
pub use nested_data::{NestedDataDetector, DEFAULT_YAML_OPTIMIZATION_THRESHOLD};
1416
pub use permission::PermissionAuditor;
1517
pub use schema::SchemaValidator;
18+
#[cfg(feature = "semantic-check")]
19+
pub use semantic_check::{SemanticChecker, SemanticCheckerConfig};
1620

1721
use crate::error::Diagnostic;
1822
use crate::ir::SkillIR;

0 commit comments

Comments
 (0)