@@ -23651,68 +23651,40 @@ fn capitalize(s: &str) -> String {
2365123651/// declarations (`name Text`, `age Int`). The descriptor is the knot record
2365223652/// format: fields separated by 2+ spaces, name and type by a single space.
2365323653/// Handles nested relations (`col Rel {inner}`) and ADT tags.
23654+/// Convert a schema descriptor (`{name Text age Int}`) to knot field
23655+/// declarations (`name Text`, `age (Int 1)`). Fields are split with the
23656+/// single-owner `knot-descriptor` grammar (robust to nested `{..}`/`(..)`),
23657+/// then each field's *type* is re-emitted in descriptor form verbatim — except
23658+/// a bare top-level numeric, which gains the dimensionless unit knot source
23659+/// requires (`Int` → `(Int 1)`). Matches the original hand-rolled splitter's
23660+/// output exactly.
2365423661fn schema_descriptor_to_fields(schema: &str) -> Vec<String> {
23655− // Strip ONE outer brace pair.
2365623662 let t = schema.trim();
23657− let inner = if t.starts_with('{') && t.ends_with('}') {
23658− &t[1..t.len() - 1]
23663+ let parsed = if t.starts_with('{') {
23664+ knot_descriptor::parse(t).ok()
2365923665 } else {
23660− t
23666+ // A bare (braceless) field list, e.g. a ctor payload fragment.
23667+ knot_descriptor::parse(&format!("{{{t}}}")).ok()
23668+ };
23669+ let Some(knot_descriptor::Desc::Record(fields)) = parsed else {
23670+ return Vec::new();
2366123671 };
23662− let inner = inner.trim();
23663− // Split on 2+ spaces (the field separator).
23664− let mut fields = Vec::new();
23665− let mut current = String::new();
23666− let mut depth = 0;
23667− let chars: Vec<char> = inner.chars().collect();
23668− let mut i = 0;
23669− while i < chars.len() {
23670− let c = chars[i];
23671− match c {
23672− '{' => { depth += 1; current.push(c); }
23673− '}' => { depth -= 1; current.push(c); }
23674− '[' => { depth += 1; current.push(c); }
23675− ']' => { depth -= 1; current.push(c); }
23676− ' ' if depth == 0 => {
23677− // 2+ spaces separate fields; a single space is within a field.
23678− let mut j = i;
23679− while j < chars.len() && chars[j] == ' ' {
23680− j += 1;
23681− }
23682− if j - i >= 2 {
23683− // A field boundary.
23684− if !current.trim().is_empty() {
23685− fields.push(current.trim().to_string());
23686− }
23687− current = String::new();
23688− i = j;
23689− continue;
23690− }
23691− current.push(c);
23692− }
23693− _ => current.push(c),
23694− }
23695− i += 1;
23696− }
23697− if !current.trim().is_empty() {
23698− fields.push(current.trim().to_string());
23699− }
23700− // The descriptor stores `Int`/`Float` without a unit. Knot requires one —
23701− // emit `Int 1`/`Float 1` (dimensionless) for a bare numeric type.
2370223672 fields
2370323673 .into_iter()
23704− .map(|f| {
23705− let parts: Vec<&str> = f.splitn(2, ' ').collect();
23706− if parts.len() == 2 {
23707− let (name, ty) = (parts[0], parts[1]);
23708− match ty {
23709− "Int" => format!("{} (Int 1)", name),
23710− "Float" => format!("{} (Float 1)", name),
23711− _ => f,
23674+ .map(|(name, ty)| {
23675+ let ty_str = match ty {
23676+ // Bare numerics need a dimensionless unit in knot source.
23677+ knot_descriptor::Desc::Scalar(knot_descriptor::Scalar::Int) => {
23678+ "(Int 1)".to_string()
2371223679 }
23713− } else {
23714− f
23715− }
23680+ knot_descriptor::Desc::Scalar(knot_descriptor::Scalar::Float) => {
23681+ "(Float 1)".to_string()
23682+ }
23683+ // Everything else round-trips to the descriptor spelling
23684+ // (`(Rel T)`, `(A | B)`, nested records) verbatim.
23685+ other => knot_descriptor::emit(&other),
23686+ };
23687+ format!("{} {}", name, ty_str)
2371623688 })
2371723689 .collect()
2371823690}
@@ -23732,51 +23704,39 @@ fn qualify_adt_ctor(value: &str, type_name: &str) -> String {
2373223704}
2373323705
2373423706/// Convert an ADT schema descriptor (`(Circle{radius Int} | Point)`) to knot
23735−/// constructor declarations (`Circle {radius (Int 1)}`, `Point {}`).
23707+/// constructor declarations (`Circle {radius (Int 1)}`, `Point {}`), using the
23708+/// single-owner `knot-descriptor` grammar.
2373623709fn schema_descriptor_to_adt_ctors(schema: &str) -> Vec<String> {
23737− // Strip the outer parens and split on ` | `.
23738− let inner = schema.trim().trim_start_matches('(').trim_end_matches(')').trim();
23739− let mut ctors = Vec::new();
23740− let mut current = String::new();
23741− let mut depth = 0;
23742− let chars: Vec<char> = inner.chars().collect();
23743− let mut i = 0;
23744− while i < chars.len() {
23745− let c = chars[i];
23746− match c {
23747− '{' => { depth += 1; current.push(c); }
23748− '}' => { depth -= 1; current.push(c); }
23749− '|' if depth == 0 => {
23750− if !current.trim().is_empty() {
23751− ctors.push(adt_ctor_to_knot(current.trim()));
23752− }
23753− current = String::new();
23754− i += 1;
23755− continue;
23756− }
23757− _ => current.push(c),
23758− }
23759− i += 1;
23760− }
23761− if !current.trim().is_empty() {
23762− ctors.push(adt_ctor_to_knot(current.trim()));
23763− }
23764− ctors
23710+ let Ok(knot_descriptor::Desc::Variant(ctors)) = knot_descriptor::parse(schema.trim())
23711+ else {
23712+ return Vec::new();
23713+ };
23714+ ctors.iter().map(|(name, fields)| adt_ctor_to_knot(name, fields)).collect()
2376523715}
2376623716
23767−/// Convert one ADT constructor descriptor (`Circle{radius Int}` or `Point`) to
23768−/// the knot form (`Circle {radius (Int 1)}` or `Point {}`).
23769−fn adt_ctor_to_knot(ctor: &str) -> String {
23770− let ctor = ctor.trim();
23771− if let Some(brace) = ctor.find('{') {
23772− let name = ctor[..brace].trim();
23773− let fields_str = &ctor[brace..]; // `{radius Int}`
23774− let fields = schema_descriptor_to_fields(fields_str);
23775− format!("{} {{{}}}", name, fields.join(" "))
23776− } else {
23717+/// Convert one ADT constructor (name + payload fields) to the knot form
23718+/// (`Circle {radius (Int 1)}` or `Point {}`).
23719+fn adt_ctor_to_knot(name: &str, fields: &[(String, knot_descriptor::Desc)]) -> String {
23720+ if fields.is_empty() {
2377723721 // A nullary constructor.
23778− format!("{} {{}}", ctor)
23722+ return format!("{} {{}}", name);
2377923723 }
23724+ let specs: Vec<String> = fields
23725+ .iter()
23726+ .map(|(n, t)| {
23727+ let ty_str = match t {
23728+ knot_descriptor::Desc::Scalar(knot_descriptor::Scalar::Int) => {
23729+ "(Int 1)".to_string()
23730+ }
23731+ knot_descriptor::Desc::Scalar(knot_descriptor::Scalar::Float) => {
23732+ "(Float 1)".to_string()
23733+ }
23734+ other => knot_descriptor::emit(other),
23735+ };
23736+ format!("{} {}", n, ty_str)
23737+ })
23738+ .collect();
23739+ format!("{} {{{}}}", name, specs.join(" "))
2378023740}
2378123741
2378223742fn generate_openapi(name: &str, table: &RouteTable) -> String {