8686 "date" , "datetime" , "time" , "decimal" , "double" , "Any" ,
8787})
8888
89+ # The schema's default_range (coremeta4cat.yaml / coremeta4cat_common.yaml).
90+ # A slot with no explicit `range` key is equivalent to one with `range: string`,
91+ # so the two must be treated as equal when diffing against the Excel (which always
92+ # emits a range value via schema_to_excel).
93+ DEFAULT_RANGE = "string"
94+
8995# ─────────────────────────────────────────────────────────────────────────────
9096# Diagnostic / reporting system
9197# ─────────────────────────────────────────────────────────────────────────────
@@ -531,25 +537,35 @@ def plan_changes(
531537 effective_class , slot_origin , class_origin ,
532538 changes , reporter ,
533539 )
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
540+ elif derived_name in get_all_class_slots ( schema , effective_class ) :
541+ # Slot already belongs to the subclass hierarchy (its own
542+ # slots: list or a mixin ) and cannot be modified via the
543+ # inbox workflow. Skip silently — these rows are
544+ # structural display information from the Excel
539545 # generator, not editable fields.
540546 reporter .info (
541547 sheet_title , f"slot '{ label } '" ,
542548 f"Skipped: belongs to sub-class `{ effective_class } ` "
543549 f"and is not modifiable via the inbox workflow "
544550 f"(edit the YAML directly)." ,
545551 )
552+ else :
553+ # Unknown slot assigned to a domain class that does not
554+ # already define it → a genuinely new slot to be added to
555+ # that subclass (e.g. anode/cathode on ElectrochemicalReactor).
556+ # _plan_new_slot resolves the owner class from the domain.
557+ _plan_new_slot (
558+ row , sheet_title , schema_class ,
559+ schema , class_origin , slot_origin , label_to_slot ,
560+ label_to_class , changes , reporter ,
561+ )
546562
547563 else :
548564 # ── Unknown label + empty domain → new top-level slot ──
549565 _plan_new_slot (
550566 row , sheet_title , schema_class ,
551567 schema , class_origin , slot_origin , label_to_slot ,
552- changes , reporter ,
568+ label_to_class , changes , reporter ,
553569 )
554570
555571 elif row_type == "class" :
@@ -689,7 +705,9 @@ def _plan_slot_changes(
689705 # -- range --
690706 new_range = row ["range" ]
691707 if new_range :
692- cur_range = _str (_effective (slot_def , su , "range" , "" ))
708+ # An absent range key means the schema default (string); normalise so a
709+ # default-range slot is not seen as "changing" to string on the next run.
710+ cur_range = _str (_effective (slot_def , su , "range" , "" )) or DEFAULT_RANGE
693711 if new_range != cur_range :
694712 if not _valid_range (new_range , schema ):
695713 reporter .error (
@@ -778,23 +796,31 @@ def _plan_new_slot(
778796 class_origin : dict [str , Path ],
779797 slot_origin : dict [str , Path ],
780798 label_to_slot : dict [str , str ],
799+ label_to_class : dict [str , str ],
781800 changes : list ,
782801 reporter : Reporter ,
783802) -> None :
784803 label = row ["label" ]
785804 domain = row ["domain" ]
786805 slot_name = _label_to_slot_name (label )
787806
788- # Only top-level new slots are supported (domain must be empty)
807+ # Resolve the class that will own the new slot:
808+ # • empty domain → the sheet's top-level data class (schema_class)
809+ # • non-empty domain → the named subclass (e.g. ElectrochemicalReactor),
810+ # added to that class exactly like the top-level case.
789811 if domain :
790- reporter .error (
791- sheet , f"new slot '{ label } '" ,
792- f"New slots may only be added at the top level of a data class "
793- f"(the **domain** column must be empty). Got domain=`{ domain } `." ,
794- hint = "To add a slot inside a subclass, edit the YAML directly. "
795- "For a top-level slot, leave the domain column empty." ,
796- )
797- return
812+ owner_class = label_to_class .get (domain ) or domain
813+ if owner_class not in schema .get ("classes" , {}):
814+ reporter .error (
815+ sheet , f"new slot '{ label } '" ,
816+ f"The domain `{ domain } ` is not a recognised class. "
817+ f"Set the domain column to an existing class label, or leave it "
818+ f"empty to add a top-level slot." ,
819+ hint = "Look at the class rows in this sheet for valid domain names." ,
820+ )
821+ return
822+ else :
823+ owner_class = schema_class
798824
799825 # Name conflict: same derived name as an existing slot
800826 if slot_name in schema .get ("slots" , {}):
@@ -818,7 +844,7 @@ def _plan_new_slot(
818844 return
819845
820846 # Range validation
821- range_val = row ["range" ] or "string"
847+ range_val = row ["range" ] or DEFAULT_RANGE
822848 if not _valid_range (range_val , schema ):
823849 reporter .error (
824850 sheet , f"new slot '{ label } '" ,
@@ -827,16 +853,16 @@ def _plan_new_slot(
827853 )
828854 return
829855
830- target = class_origin .get (schema_class , SCHEMA_DIR / MODULE_FILES [- 1 ])
856+ target = class_origin .get (owner_class , SCHEMA_DIR / MODULE_FILES [- 1 ])
831857 reporter .info (
832858 sheet , f"new slot `{ slot_name } `" ,
833- f"Will add `{ slot_name } ` to `{ schema_class } `." ,
859+ f"Will add `{ slot_name } ` to `{ owner_class } `." ,
834860 )
835861 changes .append ({
836862 "type" : "slot_add" ,
837863 "name" : slot_name ,
838864 "label" : label ,
839- "schema_class" : schema_class ,
865+ "schema_class" : owner_class ,
840866 "range" : range_val ,
841867 "mro" : mro ,
842868 "description" : row ["description" ],
@@ -1171,7 +1197,7 @@ def apply_changes(changes: list[dict], reporter: Reporter) -> None:
11711197 slot_def : dict [str , Any ] = {}
11721198 if ch ["description" ]:
11731199 slot_def ["description" ] = _as_literal (ch ["description" ])
1174- if ch ["range" ] and ch [ "range" ] != "string" :
1200+ if ch ["range" ]:
11751201 slot_def ["range" ] = ch ["range" ]
11761202 if ch ["uri" ]:
11771203 slot_def ["slot_uri" ] = ch ["uri" ]
@@ -1206,7 +1232,7 @@ def apply_changes(changes: list[dict], reporter: Reporter) -> None:
12061232 _save_yaml (target , doc )
12071233 reporter .applied (
12081234 f"New slot `{ slot_name } ` added to `{ schema_class } ` "
1209- f"(M/R/O: { mro } , range: { ch ['range' ] or 'string' } )"
1235+ f"(M/R/O: { mro } , range: { ch ['range' ]} )"
12101236 )
12111237
12121238 # ── add new class ─────────────────────────────────────────────────
0 commit comments