Skip to content

Commit 8bc3cc7

Browse files
fix(inbox): correctly resolve slot_usage-only slots and subclass rows
Three bugs caused false errors when researchers submitted inbox workbooks: 1. build_label_index only indexed global slots. Slots like had_input_entity, realized_plan, and carried_out_by that are defined only in class slot_usage blocks (not globally) were missing from the index, so the script treated their Excel rows as new-slot additions rather than existing-slot lookups. Fix: also index slot_usage slot names across all classes. 2. Subclass slot rows (e.g. has_concentration with domain=CoPrecipitation) appear in the Excel because _collect_rows recurses through the class hierarchy. These slots are mixin / attribute slots that cannot be modified via the inbox workflow. The script was erroneously raising errors ("new slot with non-empty domain"). Fix: skip such rows with a silent info diagnostic. 3. Class rows (e.g. Precursor with domain="had input entity") imply the parent slot is present in the workbook, but the script never added it to seen_slot_names, triggering false deletion warnings. Fix: when processing a class row, mark the domain slot as seen. Also adds sys.stdout.reconfigure(encoding="utf-8") so emoji in the Markdown output does not crash on Windows cp1252 consoles. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 035c27b commit 8bc3cc7

1 file changed

Lines changed: 82 additions & 7 deletions

File tree

scripts/inbox_to_schema.py

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,30 @@ def build_label_index(schema: dict) -> tuple[dict[str, str], dict[str, str]]:
231231
"""
232232
Return (label_to_slot, label_to_class): inverse of snake_to_readable for
233233
every slot and class defined in the merged schema.
234+
235+
label_to_slot covers:
236+
• globally-defined slots (schema["slots"])
237+
• slots referenced only via class slot_usage blocks (e.g. had_input_entity,
238+
realized_plan) that are not in the global slots dict but appear in the
239+
Excel because _collect_rows uses get_class_ranged_slot_usage.
234240
"""
235241
label_to_slot: dict[str, str] = {}
236242
label_to_class: dict[str, str] = {}
237243

244+
# 1. Global slots
238245
for name in schema.get("slots", {}):
239246
label_to_slot[snake_to_readable(name)] = name
247+
248+
# 2. Slot-usage-only slots: referenced in class slot_usage but not globally
249+
# defined (e.g. had_input_entity in Synthesis.slot_usage). These are
250+
# rendered by schema_to_excel as top-level slot rows with an empty domain
251+
# and must be recognised as existing, not new.
252+
for class_def in schema.get("classes", {}).values():
253+
for slot_name in (class_def.get("slot_usage") or {}):
254+
label = snake_to_readable(slot_name)
255+
if label not in label_to_slot:
256+
label_to_slot[label] = slot_name
257+
240258
for name in schema.get("classes", {}):
241259
label_to_class[snake_to_readable(name)] = name
242260

@@ -477,23 +495,73 @@ def plan_changes(
477495
for row in rows:
478496
label = row["label"]
479497
row_type = row["type"]
498+
domain = row["domain"]
480499

481500
if row_type == "slot":
482501
slot_name = label_to_slot.get(label)
483-
if slot_name is None:
484-
_plan_new_slot(
485-
row, sheet_title, schema_class,
486-
schema, class_origin, slot_origin, label_to_slot,
487-
changes, reporter,
502+
503+
if slot_name is not None:
504+
# ── Known global slot ───────────────────────────────────
505+
# If the row has a domain (e.g. domain="Precursor"), the
506+
# slot lives in that subclass's slot_usage, not in the
507+
# top-level class. Use the domain class as context so
508+
# _plan_slot_changes targets the right YAML node.
509+
effective_class = (
510+
(label_to_class.get(domain) or domain)
511+
if domain else schema_class
488512
)
489-
else:
490513
seen_slot_names.add(slot_name)
491514
_plan_slot_changes(
492-
row, slot_name, schema, sheet_title, schema_class,
515+
row, slot_name, schema, sheet_title, effective_class,
493516
slot_origin, class_origin, changes, reporter,
494517
)
495518

519+
elif domain:
520+
# ── Unknown label + non-empty domain ───────────────────
521+
# Could be a slot_usage-only slot (not in global slots:)
522+
# defined on the domain class (e.g. has_concentration in
523+
# CoPrecipitation). Derive the slot name and check.
524+
effective_class = label_to_class.get(domain) or domain
525+
derived_name = _label_to_slot_name(label)
526+
cls_def = schema.get("classes", {}).get(effective_class, {})
527+
if derived_name in (cls_def.get("slot_usage") or {}):
528+
seen_slot_names.add(derived_name)
529+
_plan_slot_changes(
530+
row, derived_name, schema, sheet_title,
531+
effective_class, slot_origin, class_origin,
532+
changes, reporter,
533+
)
534+
else:
535+
# Slot belongs to the subclass hierarchy (mixin,
536+
# attribute, or imported slot) and cannot be modified
537+
# via the inbox workflow. Skip silently — these rows
538+
# are structural display information from the Excel
539+
# generator, not editable fields.
540+
reporter.info(
541+
sheet_title, f"slot '{label}'",
542+
f"Skipped: belongs to sub-class `{effective_class}` "
543+
f"and is not modifiable via the inbox workflow "
544+
f"(edit the YAML directly).",
545+
)
546+
547+
else:
548+
# ── Unknown label + empty domain → new top-level slot ──
549+
_plan_new_slot(
550+
row, sheet_title, schema_class,
551+
schema, class_origin, slot_origin, label_to_slot,
552+
changes, reporter,
553+
)
554+
496555
elif row_type == "class":
556+
# ── When a class row has a domain, that domain is the label
557+
# of the parent slot (e.g. "had input entity" for Precursor).
558+
# Mark that parent slot as seen so the deletion detector does
559+
# not falsely warn that it is missing from the workbook.
560+
if domain:
561+
parent_slot_name = label_to_slot.get(domain)
562+
if parent_slot_name:
563+
seen_slot_names.add(parent_slot_name)
564+
497565
class_name = label_to_class.get(label)
498566
if class_name is None:
499567
_plan_new_class(
@@ -1262,6 +1330,13 @@ def main(inbox_path: Path) -> int:
12621330

12631331

12641332
if __name__ == "__main__":
1333+
# Ensure stdout is UTF-8 even on Windows (emoji in Markdown output otherwise
1334+
# crash with UnicodeEncodeError on cp1252 consoles / cmd.exe).
1335+
if hasattr(sys.stdout, "reconfigure"):
1336+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
1337+
if hasattr(sys.stderr, "reconfigure"):
1338+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
1339+
12651340
inbox_path = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_INBOX
12661341
if not inbox_path.exists():
12671342
print(

0 commit comments

Comments
 (0)