Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/suggest-token-operation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"design-data-core": minor
"design-data-cli": minor
---

Add `suggest_token` operation to sdk/core and CLI (closes #975).

- **sdk/core/src/suggest.rs**: new `suggest` function — ranks existing tokens
against a natural-language intent string using Jaccard similarity over
key segments and name-object fields. Supports a `property_hint` filter.
- **sdk/core/src/lib.rs**: expose `pub mod suggest`.
- **sdk/cli/src/main.rs**: add `suggest` subcommand with `--property` hint,
`--limit`, and `--format` flags.
- Prerequisite for TUI RFC #973 Screen 1 "reuse first" wizard banner.
84 changes: 84 additions & 0 deletions sdk/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use design_data_core::migrate;
use design_data_core::naming::NamingExceptionsFile;
use design_data_core::query;
use design_data_core::schema::SchemaRegistry;
use design_data_core::suggest;
use design_data_core::validate;
use design_data_core::write::{WriteTokenInput, write_token};
use miette::{IntoDiagnostic, WrapErr};
Expand Down Expand Up @@ -165,6 +166,24 @@ enum Commands {
#[arg(long, value_name = "DIR")]
components_dir: Option<PathBuf>,
},
/// Suggest existing tokens that match a natural-language intent string
Suggest {
/// Natural-language intent (e.g. "accent background hover")
#[arg(value_name = "INTENT")]
intent: String,
/// Path to the token dataset directory
#[arg(value_name = "PATH")]
path: Option<PathBuf>,
/// Restrict results to tokens whose name.property matches this hint
#[arg(long, value_name = "PROPERTY")]
property: Option<String>,
/// Maximum number of results to return
#[arg(long, default_value_t = 5)]
limit: usize,
/// Output format
#[arg(long, value_enum, default_value_t = OutputFormat::Pretty)]
format: OutputFormat,
},
/// Create or update a product-context.json document for a product-layer working copy
Write {
/// Path to the product context JSON file to create or update
Expand Down Expand Up @@ -1092,6 +1111,58 @@ fn run_component(id: &str, components_dir: Option<PathBuf>) -> miette::Result<Ex
Ok(ExitCode::from(1))
}

fn run_suggest(
intent: &str,
path: Option<&Path>,
property: Option<&str>,
limit: usize,
format: OutputFormat,
) -> miette::Result<ExitCode> {
let target = path
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
let graph = TokenGraph::from_json_dir(&target)
.into_diagnostic()
.wrap_err_with(|| format!("failed to load tokens from {}", target.display()))?;

let results = suggest::suggest(&graph, intent, property, limit);

if matches!(format, OutputFormat::Json) {
let json_vals: Vec<serde_json::Value> = results
.iter()
.map(|r| {
serde_json::json!({
"token_name": r.token_name,
"token_uuid": r.token_uuid,
"file": r.file.display().to_string(),
"layer": serde_json::to_value(r.layer).unwrap_or_default(),
"confidence": r.confidence,
"name_object": r.name_object,
"value": r.value,
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&json_vals).into_diagnostic()?);
} else if results.is_empty() {
println!("No matching tokens found for: {intent:?}");
} else {
println!("Suggestions for {:?} (top {}):", intent, results.len());
for (i, r) in results.iter().enumerate() {
println!(
" {}. {} (confidence: {:.2})",
i + 1,
r.token_name,
r.confidence
);
if let Some(v) = &r.value {
println!(" value: {v}");
}
}
}

Ok(ExitCode::SUCCESS)
}

fn run_write(output: &Path, rationale: Option<&str>) -> miette::Result<ExitCode> {
let mut doc: serde_json::Value = if output.exists() {
let raw = std::fs::read_to_string(output)
Expand Down Expand Up @@ -1346,6 +1417,19 @@ fn main() -> ExitCode {
run_primer(&target, format, components_dir, fields_dir, mode_sets_dir)
}
Commands::Component { id, components_dir } => run_component(&id, components_dir),
Commands::Suggest {
intent,
path,
property,
limit,
format,
} => run_suggest(
&intent,
path.as_deref(),
property.as_deref(),
limit,
format,
),
Commands::Write { output, rationale } => {
run_write(&output, rationale.as_deref())
}
Expand Down
3 changes: 2 additions & 1 deletion sdk/core/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ use crate::CoreError;
///
/// Layer ordering is encoded in the discriminant so `Ord` gives correct
/// precedence: `Foundation < Platform < Product`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, serde::Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Layer {
#[default]
Foundation = 1,
Expand Down
1 change: 1 addition & 0 deletions sdk/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub mod query;
pub mod registry;
pub mod report;
pub mod schema;
pub mod suggest;
pub mod validate;
pub mod write;

Expand Down
Loading
Loading