···244244 }
245245 })?;
246246247247- // Create a context to pass the current namespace to type generation
248247 let ctx = ConversionContext {
249248 current_namespace: nsid.to_string(),
249249+ local_main_name: last_segment.to_string(),
250250 warnings: RefCell::new(Vec::new()),
251251 };
252252···260260261261 match def_type {
262262 "record" => {
263263- let mlf = generate_record(name, def, last_segment, &ctx)?;
263263+ let mlf = generate_record(name, def, &ctx)?;
264264 output.push_str(&mlf);
265265 output.push('\n');
266266 }
267267 "query" => {
268268- let mlf = generate_query(name, def, last_segment, &ctx)?;
268268+ let mlf = generate_query(name, def, &ctx)?;
269269 output.push_str(&mlf);
270270 output.push('\n');
271271 }
272272 "procedure" => {
273273- let mlf = generate_procedure(name, def, last_segment, &ctx)?;
273273+ let mlf = generate_procedure(name, def, &ctx)?;
274274 output.push_str(&mlf);
275275 output.push('\n');
276276 }
277277 "subscription" => {
278278- let mlf = generate_subscription(name, def, last_segment, &ctx)?;
278278+ let mlf = generate_subscription(name, def, &ctx)?;
279279 output.push_str(&mlf);
280280 output.push('\n');
281281 }
···286286 }
287287 _ => {
288288 // All other types (object, string, array, union, etc.) are treated as def type
289289- let mlf = generate_def_type(name, def, last_segment, &ctx)?;
289289+ let mlf = generate_def_type(name, def, &ctx)?;
290290 output.push_str(&mlf);
291291 output.push('\n');
292292 }
···301301302302struct ConversionContext {
303303 current_namespace: String,
304304+ /// MLF-side name of this lexicon's main def.
305305+ ///
306306+ /// When a def is named `main` in the source JSON we rename it in
307307+ /// MLF to the namespace's last segment (so `app.bsky.feed.post`'s
308308+ /// `defs.main` becomes `def type post` / `record post`). That rename
309309+ /// exploits MLF's implicit-main convention — a def whose name
310310+ /// matches the namespace's last segment is treated as `main`
311311+ /// automatically — and makes the MLF read more naturally.
312312+ ///
313313+ /// But sibling defs often reference the main via a local `#main`
314314+ /// ref; if we naively emit those as `main` in MLF they'd point at a
315315+ /// no-longer-existent def. Storing the rename here lets every
316316+ /// ref-emission path rewrite `main` → `local_main_name`
317317+ /// consistently, keeping the MLF internally resolvable regardless
318318+ /// of what the source JSON called the main def.
319319+ local_main_name: String,
304320 /// Non-fatal issues accumulated during conversion. Callers receive
305321 /// these via [`MlfGenerateOutput`] and decide what to do with them
306322 /// (print to stderr, collect for a summary, suppress).
···332348 }
333349}
334350335335-fn generate_record(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result<String, MlfGenerateError> {
351351+fn generate_record(name: &str, def: &Value, ctx: &ConversionContext) -> Result<String, MlfGenerateError> {
336352 let mut output = String::new();
337353338354 // Add doc comment if present
···351367352368 // Use last segment of NSID for "main" definitions
353369 let record_name = if name == "main" {
354354- escape_name(last_segment)
370370+ escape_name(&ctx.local_main_name)
355371 } else {
356372 escape_name(name)
357373 };
···399415 Ok(output)
400416}
401417402402-fn generate_query(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result<String, MlfGenerateError> {
418418+fn generate_query(name: &str, def: &Value, ctx: &ConversionContext) -> Result<String, MlfGenerateError> {
403419 let mut output = String::new();
404420405421 // Add doc comment
···417433 }
418434419435 let query_name = if name == "main" {
420420- escape_name(last_segment)
436436+ escape_name(&ctx.local_main_name)
421437 } else {
422438 escape_name(name)
423439 };
···493509 Ok(output)
494510}
495511496496-fn generate_procedure(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result<String, MlfGenerateError> {
512512+fn generate_procedure(name: &str, def: &Value, ctx: &ConversionContext) -> Result<String, MlfGenerateError> {
497513 let mut output = String::new();
498514499515 // Add doc comment
···511527 }
512528513529 let procedure_name = if name == "main" {
514514- escape_name(last_segment)
530530+ escape_name(&ctx.local_main_name)
515531 } else {
516532 escape_name(name)
517533 };
···592608 Ok(output)
593609}
594610595595-fn generate_subscription(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result<String, MlfGenerateError> {
611611+fn generate_subscription(name: &str, def: &Value, ctx: &ConversionContext) -> Result<String, MlfGenerateError> {
596612 let mut output = String::new();
597613598614 // Add doc comment
···610626 }
611627612628 let subscription_name = if name == "main" {
613613- escape_name(last_segment)
629629+ escape_name(&ctx.local_main_name)
614630 } else {
615631 escape_name(name)
616632 };
···681697 Ok(output)
682698}
683699684684-fn generate_def_type(name: &str, def: &Value, last_segment: &str, ctx: &ConversionContext) -> Result<String, MlfGenerateError> {
700700+fn generate_def_type(name: &str, def: &Value, ctx: &ConversionContext) -> Result<String, MlfGenerateError> {
685701 let mut output = String::new();
686702687703 // Add doc comment if present
···701717 // Use last segment of NSID for "main" definitions
702718 // Keywords are now allowed by the parser, so just escape with backticks
703719 let def_name = if name == "main" {
704704- escape_name(last_segment)
720720+ escape_name(&ctx.local_main_name)
705721 } else {
706722 escape_name(name)
707723 };
···10981114 Ok(resolve_ref_string(ref_str, ctx))
10991115}
1100111611011101-/// Resolve a Lexicon ref string (e.g. `#defName` or `namespace#defName`)
11021102-/// into the MLF type name to emit in source.
11171117+/// Resolve a Lexicon ref string (e.g. `#defName`, `namespace#defName`,
11181118+/// or a bare NSID) into the MLF type name to emit in source.
11191119+///
11201120+/// Local refs to this lexicon's `main` def get rewritten to the
11211121+/// renamed def name held in [`ConversionContext::local_main_name`] —
11221122+/// see that field's docs for the full rationale.
11031123fn resolve_ref_string(ref_str: &str, ctx: &ConversionContext) -> String {
11041124 if let Some(stripped) = ref_str.strip_prefix('#') {
11051105- stripped.to_string()
11061106- } else if let Some((namespace, def_name)) = ref_str.split_once('#') {
11251125+ return rewrite_local_main(stripped, ctx);
11261126+ }
11271127+ if let Some((namespace, def_name)) = ref_str.split_once('#') {
11071128 if namespace == ctx.current_namespace {
11081108- def_name.to_string()
11091109- } else {
11101110- format!("{}.{}", namespace, def_name)
11291129+ return rewrite_local_main(def_name, ctx);
11111130 }
11311131+ return format!("{}.{}", namespace, def_name);
11321132+ }
11331133+ // Bare NSID — implicit main of the named lexicon. If it names the
11341134+ // current lexicon (rare but possible), rewrite to the local main
11351135+ // name; otherwise leave as-is.
11361136+ if ref_str == ctx.current_namespace {
11371137+ return ctx.local_main_name.clone();
11381138+ }
11391139+ ref_str.to_string()
11401140+}
11411141+11421142+/// Rewrite a bare def name to account for the lexicon's main-def
11431143+/// rename. A def name of `"main"` in a lexicon whose main has been
11441144+/// renamed to its namespace's last segment becomes that segment
11451145+/// instead; any other name passes through unchanged.
11461146+fn rewrite_local_main(def_name: &str, ctx: &ConversionContext) -> String {
11471147+ if def_name == "main" {
11481148+ ctx.local_main_name.clone()
11121149 } else {
11131113- // Malformed ref — pass through unchanged rather than inventing a namespace.
11141114- ref_str.to_string()
11501150+ def_name.to_string()
11151151 }
11161152}