import tkinter as tk from tkinter import ttk, messagebox import json import re import uuid class MoralithicChainSetValidatorMode3: def __init__(self, root): self.root = root self.root.title("Moralithic Chain Set Validator - Mode 3") self.sets = [] self.tab_control = ttk.Notebook(self.root) self.tab_control.pack(fill=tk.BOTH, expand=True) self.valid_statement_source_pairs = { 'ST31': 'SO30', 'ST32': 'SO31', 'ST33': 'SO32', 'ST34': 'SO33', 'ST43': 'SO42', 'ST44': 'SO43', 'ST45': 'SO44', 'ST46': 'SO45', 'ST47': 'SO46', 'ST48': 'SO47', 'ST49': 'SO48', 'ST50': 'SO49', 'ST52': 'SO51', 'ST54': 'SO53', 'ST56': 'SO55', 'ST58': 'SO57', 'ST59': 'SO58', 'ST60': 'SO59', 'ST61': 'SO60', 'ST62': 'SO61', 'ST63': 'SO62', 'ST64': 'SO63', 'ST65': 'SO64', 'ST66': 'SO65', 'ST67': 'SO66', 'ST68': 'SO67', 'ST69': 'SO68', 'ST70': 'SO69', 'ST71': 'SO70', 'ST72': 'SO71', 'ST73': 'SO72', 'ST74': 'SO73' } self.valid_source_statement_pairs = {v: k for k, v in self.valid_statement_source_pairs.items()} self.valid_statement_value_pairs = { 'ST31': {'values': ['VL31'], 'required': 'VL31'}, 'ST32': {'values': ['VL31', 'VL32'], 'required': 'VL32'}, 'ST33': {'values': ['VL33'], 'required': 'VL33'}, 'ST34': {'values': ['VL31', 'VL32', 'VL33', 'VL34'], 'required': 'VL34'}, 'ST43': {'values': ['VL43'], 'required': 'VL43'}, 'ST44': {'values': ['VL43', 'VL44'], 'required': 'VL44'}, 'ST45': {'values': ['VL45'], 'required': 'VL45'}, 'ST46': {'values': ['VL43', 'VL44', 'VL45', 'VL46'], 'required': 'VL46'}, 'ST47': {'values': ['VL47'], 'required': 'VL47'}, 'ST48': {'values': ['VL47', 'VL48'], 'required': 'VL48'}, 'ST49': {'values': ['VL47', 'VL48', 'VL49'], 'required': 'VL49'}, 'ST50': {'values': ['VL47', 'VL48', 'VL49', 'VL50'], 'required': 'VL50'}, 'ST52': {'values': ['VL52'], 'required': 'VL52'}, 'ST54': {'values': ['VL54'], 'required': 'VL54'}, 'ST56': {'values': ['VL56'], 'required': 'VL56'}, 'ST58': {'values': ['VL58'], 'required': 'VL58'}, 'ST59': {'values': ['VL59'], 'required': 'VL59'}, 'ST60': {'values': ['VL59', 'VL60'], 'required': 'VL60'}, 'ST61': {'values': ['VL61'], 'required': 'VL61'}, 'ST62': {'values': ['VL59', 'VL60', 'VL61', 'VL62'], 'required': 'VL62'}, 'ST63': {'values': ['VL63'], 'required': 'VL63'}, 'ST64': {'values': ['VL63', 'VL64'], 'required': 'VL64'}, 'ST65': {'values': ['VL65'], 'required': 'VL65'}, 'ST66': {'values': ['VL63', 'VL64', 'VL65', 'VL66'], 'required': 'VL66'}, 'ST67': {'values': ['VL67'], 'required': 'VL67'}, 'ST68': {'values': ['VL67', 'VL68'], 'required': 'VL68'}, 'ST69': {'values': ['VL69'], 'required': 'VL69'}, 'ST70': {'values': ['VL67', 'VL68', 'VL69', 'VL70'], 'required': 'VL70'}, 'ST71': {'values': ['VL71'], 'required': 'VL71'}, 'ST72': {'values': ['VL71', 'VL72'], 'required': 'VL72'}, 'ST73': {'values': ['VL73'], 'required': 'VL73'}, 'ST74': {'values': ['VL71', 'VL72', 'VL73', 'VL74'], 'required': 'VL74'} } self.valid_ni_tags = ['NI04', 'NI05', 'NI06', 'NI07'] self.valid_ef_tags = [f"EF{i}" for i in range(32, 64)] self.valid_rp_tags = ['RP04', 'RP05', 'RP06', 'RP07'] self.valid_re_tags = [f"RE{i:02d}" for i in range(8, 16)] self.valid_ac_tags = [f"AC{i:02d}" for i in range(8, 16)] self.valid_means_tags = ["ME02", "ME03"] self.valid_displayment_tags = ["DP02", "DP03"] self.valid_statement_tags = list(self.valid_statement_source_pairs.keys()) self.valid_source_tags = list(self.valid_source_statement_pairs.keys()) self.valid_value_tags = sorted(set(sum([d['values'] for d in self.valid_statement_value_pairs.values()], []))) self.required_values = ['VL31', 'VL33', 'VL43', 'VL45', 'VL47', 'VL49', 'VL59', 'VL61', 'VL63', 'VL65', 'VL67', 'VL69', 'VL71', 'VL73'] self.required_ini_tags = ['NI04', 'NI06', 'EF32', 'EF34', 'EF36', 'EF38', 'EF40', 'EF42', 'EF44', 'EF46', 'EF48', 'EF50', 'EF52', 'EF54', 'EF56', 'EF58', 'EF60', 'EF62', 'RP04', 'RP06', 'RE08', 'RE10', 'RE12', 'RE14', 'AC08', 'AC10', 'AC12', 'AC14'] self.ac_compat = { 'AC12': ['EF36', 'EF38'], 'AC13': ['EF37', 'EF39'], 'AC14': ['EF44', 'EF46', 'RP06'], 'AC15': ['EF45', 'EF47', 'RP07'] } self.conversion_rules = { 'AC08': ['RE08', 'RE12'], 'AC09': ['RE09', 'RE13'], 'AC10': ['RE10', 'RE14'], 'AC11': ['RE11', 'RE15'], 'AC12': ['RE08', 'RE12'], 'AC13': ['RE09', 'RE13'], 'AC14': ['RE10', 'RE14'], 'AC15': ['RE11', 'RE15'], 'RE08': ['AC08', 'AC12'], 'RE09': ['AC09', 'AC13'], 'RE10': ['AC10', 'AC14'], 'RE11': ['AC11', 'AC15'], 'RE12': ['AC08', 'AC12'], 'RE13': ['AC09', 'AC13'], 'RE14': ['AC10', 'AC14'], 'RE15': ['AC11', 'AC15'] } self.transformation_rules = { 'NI04': ['NI06'], 'NI05': ['NI07'], 'NI06': ['NI04'], 'NI07': ['NI05'], 'EF32': ['EF34', 'EF36', 'EF38'], 'EF33': ['EF35', 'EF37', 'EF39'], 'EF34': ['EF32', 'EF36', 'EF38'], 'EF35': ['EF33', 'EF37', 'EF39'], 'EF36': ['EF32', 'EF34', 'EF38'], 'EF37': ['EF33', 'EF35', 'EF39'], 'EF38': ['EF32', 'EF34', 'EF36'], 'EF39': ['EF33', 'EF35', 'EF37'], 'EF40': ['EF42', 'EF44', 'EF46'], 'EF41': ['EF43', 'EF45', 'EF47'], 'EF42': ['EF40', 'EF44', 'EF46'], 'EF43': ['EF41', 'EF45', 'EF47'], 'EF44': ['EF40', 'EF42', 'EF46'], 'EF45': ['EF41', 'EF43', 'EF47'], 'EF46': ['EF40', 'EF42', 'EF44'], 'EF47': ['EF41', 'EF43', 'EF45'], 'EF48': ['EF50', 'EF52', 'EF54'], 'EF49': ['EF51', 'EF53', 'EF55'], 'EF50': ['EF48', 'EF52', 'EF54'], 'EF51': ['EF49', 'EF53', 'EF55'], 'EF52': ['EF48', 'EF50', 'EF54'], 'EF53': ['EF49', 'EF51', 'EF55'], 'EF54': ['EF48', 'EF50', 'EF52'], 'EF55': ['EF49', 'EF51', 'EF53'], 'EF56': ['EF58', 'EF60', 'EF62'], 'EF57': ['EF59', 'EF61', 'EF63'], 'EF58': ['EF56', 'EF60', 'EF62'], 'EF59': ['EF57', 'EF61', 'EF63'], 'EF60': ['EF56', 'EF58', 'EF62'], 'EF61': ['EF57', 'EF59', 'EF63'], 'EF62': ['EF56', 'EF58', 'EF60'], 'EF63': ['EF57', 'EF59', 'EF61'], 'RE08': ['RE12'], 'RE09': ['RE13'], 'RE10': ['RE14'], 'RE11': ['RE15'], 'RE12': ['RE08'], 'RE13': ['RE09'], 'RE14': ['RE10'], 'RE15': ['RE11'], 'AC08': ['AC12'], 'AC09': ['AC13'], 'AC10': ['AC14'], 'AC11': ['AC15'], 'AC12': ['AC08'], 'AC13': ['AC09'], 'AC14': ['AC10'], 'AC15': ['AC11'], 'RP04': [], 'RP05': [], 'RP06': [], 'RP07': [] } self.function_source_tags = ['SO51', 'SO53', 'SO55', 'SO57', 'SO66', 'SO67', 'SO68', 'SO69', 'SO70', 'SO71', 'SO72', 'SO73'] self.process_source_tags = ['SO58', 'SO59', 'SO60', 'SO61', 'SO62', 'SO63', 'SO64', 'SO65'] self.function_statement_tags = ['ST52', 'ST54', 'ST56', 'ST58', 'ST67', 'ST68', 'ST69', 'ST70', 'ST71', 'ST72', 'ST73', 'ST74'] self.process_statement_tags = ['ST59', 'ST60', 'ST61', 'ST62', 'ST63', 'ST64', 'ST65', 'ST66'] self.general_source_mapping = { 'SO10': ['SO30', 'SO31', 'SO32', 'SO33'], 'SO16': ['SO42', 'SO43', 'SO44', 'SO45'], 'SO18': ['SO46', 'SO47', 'SO48', 'SO49'], 'SO22': ['SO51', 'SO53', 'SO66', 'SO67', 'SO68', 'SO69'], 'SO23': ['SO55', 'SO57', 'SO70', 'SO71', 'SO72', 'SO73'], 'SO24': ['SO58', 'SO59', 'SO60', 'SO61'], 'SO25': ['SO62', 'SO63', 'SO64', 'SO65'] } self.general_source_options = list(self.general_source_mapping.keys()) self.setup_gui() def setup_gui(self): self.main_frame = ttk.Frame(self.tab_control) self.tab_control.add(self.main_frame, text="Text/System 1") self.setup_tab(self.main_frame) def setup_tab(self, tab): content_frame = ttk.Frame(tab) content_frame.pack(fill=tk.BOTH, expand=True) controls_frame = ttk.Frame(content_frame) controls_frame.pack(anchor=tk.W, padx=10, pady=5) # Text/System ttk.Label(controls_frame, text="Text/System:").pack(anchor=tk.W, pady=5) tab.system_focus = ttk.Entry(controls_frame, width=30) tab.system_focus.pack(anchor=tk.W, pady=2) tab.system_focus.bind("", lambda event, t=tab: self.update_tab_name(t, event.widget.get())) # Interlinked Tabs ttk.Label(controls_frame, text="Interlinked Tabs:").pack(anchor=tk.W, pady=5) tab.interlinked_tabs = ttk.Entry(controls_frame, width=30) tab.interlinked_tabs.pack(anchor=tk.W, pady=2) # Set Format ttk.Label(controls_frame, text="Select Set Format:").pack(anchor=tk.W, pady=5) tab.format_var = tk.StringVar(value="") formats = ["Sequential", "Hierarchical", "Sequential + Hierarchical"] tab.format_menu = ttk.Combobox(controls_frame, textvariable=tab.format_var, values=formats, state="readonly") tab.format_menu.pack(anchor=tk.W) # Buttons ttk.Button(controls_frame, text="Add Set", command=lambda: self.add_set(tab, tab.format_var, interlinked=False)).pack(anchor=tk.W, pady=5) ttk.Button(controls_frame, text="Add Tab", command=self.add_tab).pack(anchor=tk.W, pady=5) ttk.Button(controls_frame, text="Validate Tab", command=lambda: self.validate_tab(tab)).pack(anchor=tk.W, pady=5) ttk.Button(controls_frame, text="Save Sets", command=self.save_sets).pack(anchor=tk.W, pady=5) canvas_frame = ttk.Frame(content_frame) canvas_frame.pack(fill=tk.BOTH, expand=True) tab.sets_canvas = tk.Canvas(canvas_frame) v_scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.VERTICAL, command=tab.sets_canvas.yview) h_scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.HORIZONTAL, command=tab.sets_canvas.xview) tab.sets_frame = ttk.Frame(tab.sets_canvas) tab.sets_canvas.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) tab.sets_canvas.create_window((0, 0), window=tab.sets_frame, anchor="nw") v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) tab.sets_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) def on_mousewheel(event): if event.delta: tab.sets_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") elif event.num == 4: tab.sets_canvas.yview_scroll(-1, "units") elif event.num == 5: tab.sets_canvas.yview_scroll(1, "units") tab.sets_canvas.bind_all("", on_mousewheel) tab.sets_canvas.bind_all("", on_mousewheel) tab.sets_canvas.bind_all("", on_mousewheel) tab.sets_frame.bind("", lambda e: tab.sets_canvas.configure(scrollregion=tab.sets_canvas.bbox("all"))) def update_tab_name(self, tab, text): tab_id = str(tab) if text.strip(): self.tab_control.tab(tab_id, text=text.strip()) else: self.tab_control.tab(tab_id, text="Text/System " + tab_id.split('.')[-1]) def update_tributary_entry(self, set_info): set_info['tributary_source_entry'].pack_forget() if set_info['add_interlinked_button']: set_info['add_interlinked_button'].pack_forget() if set_info['tributary_source'].get() == "Yes": set_info['tributary_source_entry'].pack(anchor=tk.W, pady=2) if not set_info['linked_row_index'] and set_info['add_interlinked_button']: set_info['add_interlinked_button'].pack(anchor=tk.W, pady=2) else: if not set_info['linked_row_index'] and set_info['add_interlinked_button']: set_info['add_interlinked_button'].pack(anchor=tk.W, pady=2) def validate_tab(self, tab): tab_id = str(tab) tab_name = self.tab_control.tab(tab_id, "text") tab_sets = [s for s in self.sets if s.get('tab') == tab] if not tab_sets: messagebox.showerror("Error", f"No sets in {tab_name} to validate.") return top_level_count = len([s for s in tab_sets if s['parent_set_id'] is None]) basic_errors = [] source_tags = [] for set_info in tab_sets: set_focus = set_info['series_focus'].get() or 'None' pos_prefix = f"Set with Series Focus {set_focus}" if not set_info['series_focus'].get().strip(): basic_errors.append(f"{pos_prefix}: Series focus must be specified.") if set_info['source_position'].get() == "No Position" and top_level_count > 1: basic_errors.append(f"{pos_prefix}: Source Position 'No Position' is invalid when the tab contains more than one set.") if not set_info['source_identity'].get().strip(): basic_errors.append(f"{pos_prefix}: Source identity must be specified.") if not set_info['general_moral_source'].get(): basic_errors.append(f"{pos_prefix}: General moral source must be selected.") if set_info['tributary_source'].get() == "Yes" and not set_info['tributary_source_entry'].get().strip(): basic_errors.append(f"{pos_prefix}: Tributary source text must be specified if Tributary Source is 'Yes'.") general_source = set_info['general_moral_source'].get() format_type = set_info['format'] main_idx = 1 if "Sequential" in format_type else 0 inter_idx = main_idx + 1 for ri, row in enumerate(set_info['rows'], 1): main_chain = row[main_idx].get().strip() if main_chain: source_match = re.match(r"^moral source \[([^\(]+?)\s*\((\w+)\)", main_chain) if source_match: source_tags.append((source_match.group(2), f"{pos_prefix}, row {ri}, main chain")) interlinked_chains = row[inter_idx] if not isinstance(interlinked_chains, list): interlinked_chains = [] for ii, ich in enumerate(interlinked_chains, 1): ich_text = ich.get().strip() if ich_text: source_match = re.match(r"^moral source \[([^\(]+?)\s*\((\w+)\)", ich_text) if source_match: source_tags.append((source_match.group(2), f"{pos_prefix}, row {ri}, interlinked chain {ii}")) if basic_errors: messagebox.showerror("Validation Errors", "\n".join(basic_errors)) return if not source_tags: messagebox.showerror("Error", f"No valid chains found in {tab_name}.") return # Check for consistent moral source tags within each set for set_info in tab_sets: set_focus = set_info['series_focus'].get() or 'None' set_source_tags = [ tag for tag, pos in source_tags if pos.startswith(f"Set with Series Focus {set_focus}") ] if len(set(set_source_tags)) > 1: messagebox.showerror( "Validation Error", f"{set_focus}: All chains in a set must share the same moral source tag. Found: {', '.join(set(set_source_tags))}." ) return # Validate chains all_chains = [] for set_info in tab_sets: general_source = set_info['general_moral_source'].get() format_type = set_info['format'] main_idx = 1 if "Sequential" in format_type else 0 inter_idx = main_idx + 1 set_focus = set_info['series_focus'].get() or 'None' for ri, row in enumerate(set_info['rows'], 1): main_chain = row[main_idx].get().strip() if main_chain: all_chains.append((main_chain, general_source, f"Set '{set_focus}', row {ri}, main chain")) interlinked_chains = row[inter_idx] if not isinstance(interlinked_chains, list): interlinked_chains = [] for ii, ich in enumerate(interlinked_chains, 1): ich_text = ich.get().strip() if ich_text: all_chains.append((ich_text, general_source, f"Set '{set_focus}', row {ri}, interlinked chain {ii}")) if not all_chains: messagebox.showerror("Error", f"No chains in {tab_name} to validate.") return source_errors = [] for chain, source, pos in all_chains: valid, error = self.validate_source_compatibility(chain, source) if not valid: source_errors.append(f"{pos}: {error}") if source_errors: messagebox.showerror("Source Validation Errors", "\n".join(source_errors)) return internal_errors = [] for chain, source, pos in all_chains: valid, error = self.validate_internal(chain) if not valid: internal_errors.append(f"{pos}: {error}") if internal_errors: messagebox.showerror("Internal Validation Errors", "\n".join(internal_errors)) return messagebox.showinfo("Success", f"All sets in {tab_name} are valid.") def validate_source_compatibility(self, chain, general_source): chain = re.sub(r'\s+', ' ', chain.strip()) pattern = r"^moral source \[([^\(]+?)\s*\((\w+)\)(?:,\s*moral (?:function|statement production process)\s*\(([^\)]+?)\))?\s*\](?:-.*)?$" match = re.match(pattern, chain) if not match: return False, f"Could not parse source tag from chain; moral source must be at the start of the chain. Got: '{chain[:50]}{'...' if len(chain) > 50 else ''}'. Expected format: 'moral source [text (SOxx)[, moral function/process (text)]]-'" source_identity, chain_source_tag, extra_source = match.groups() specific_sources = self.general_source_mapping.get(general_source, [general_source]) if chain_source_tag not in specific_sources: return False, f"Moral source tag '{chain_source_tag}' is not valid for general source '{general_source}'. Valid tags are: {', '.join(specific_sources)}" if chain_source_tag in self.function_source_tags and not extra_source: return False, f"Source tag '{chain_source_tag}' requires a moral function, but none was provided." if chain_source_tag in self.process_source_tags and not extra_source: return False, f"Source tag '{chain_source_tag}' requires a moral statement production process, but none was provided." if extra_source and chain_source_tag not in (self.function_source_tags + self.process_source_tags): return False, f"Source tag '{chain_source_tag}' does not support a moral function or process, but one was provided: '{extra_source}'." return True, "" def validate_internal(self, chain): try: chain = re.sub(r'\s+', ' ', chain.strip()) # First, check if this is a chain with footnotes (ends with "Footnotes:") has_footnotes = False footnote_section = "" if "Footnotes:" in chain: has_footnotes = True main_chain, footnote_section = chain.rsplit("Footnotes:", 1) main_chain = main_chain.strip() footnote_section = f"Footnotes: {footnote_section.strip()}" else: main_chain = chain # Original pattern for main chain validation pattern = ( r"moral source \[([^\(]+)\s*\((\w+)\)(?:,\s*moral (?:function|statement production process)\s*\(([^\)]+)\))?\]" r"- moral statement \[([^\(]+)\s*\((\w+)\)(?:,\s*moral (?:function|statement production process)\s*\(([^\)]+)\))?,\s*" r"Moral Values\s*\((?:(?:\d+\.\s*)?[^\(]+?\s*\((\w+)\)(?:\.\s*(?:\d+\.\s*)?[^\(]+?\s*\((\w+)\))*)\)\]" r"(?:- Displayments \[([^\(]+)\s*\((\w+)\)\])?" r"(?:- Non-interactor \[([^\]]*)\])?" r"(?:- Enforcers \[([^\]]*)\])?" r"(?:- Repeaters \[([^\]]*)\])?" r"(?:- Rejecters \[([^\]]*)\])?" r"(?:- Accepters \[([^\]]*)\])?" ) match = re.match(pattern, main_chain, re.DOTALL) if not match: return False, "Invalid moralithic chain format. Expected format: 'moral source [...] - moral statement [text (STxx), Moral Values (...)] - Displayments [...] - ...'" groups = match.groups() if len(groups) < 6: return False, "Invalid chain structure: too few components matched." source_identity, chain_source_tag, extra_source, statement, statement_tag, extra_statement = groups[:6] value_tags = [tag for tag in groups[6:8] if tag and tag.startswith('VL')] if len(groups) > 7 else [] displayment_desc = groups[8] if len(groups) > 8 else "" displayment_tag = groups[9] if len(groups) > 9 else "" ni_str = groups[10] if len(groups) > 10 else "" ef_str = groups[11] if len(groups) > 11 else "" rp_str = groups[12] if len(groups) > 12 else "" re_str = groups[13] if len(groups) > 13 else "" ac_str = groups[14] if len(groups) > 14 else "" # Original validation logic (unchanged) expected_statement = self.valid_source_statement_pairs.get(chain_source_tag) if not expected_statement: return False, f"Source tag '{chain_source_tag}' is not a valid source tag. Valid tags are: {', '.join(self.valid_source_tags)}." if statement_tag != expected_statement: return False, f"Statement tag '{statement_tag}' is incompatible with source tag '{chain_source_tag}'. Expected statement tag: '{expected_statement}'." if chain_source_tag in self.function_source_tags and not extra_source: return False, f"Source tag '{chain_source_tag}' requires a moral function, but none was provided." if chain_source_tag in self.process_source_tags and not extra_source: return False, f"Source tag '{chain_source_tag}' requires a moral statement production process, but none was provided." if extra_source and chain_source_tag not in (self.function_source_tags + self.process_source_tags): return False, f"Source tag '{chain_source_tag}' does not support a moral function or process, but one was provided: '{extra_source}'." if statement_tag in self.function_statement_tags and not extra_statement: return False, f"Statement tag '{statement_tag}' requires a moral function, but none was provided." if statement_tag in self.process_statement_tags and not extra_statement: return False, f"Statement tag '{statement_tag}' requires a moral statement production process, but none was provided." if extra_statement and statement_tag not in (self.function_statement_tags + self.process_statement_tags): return False, f"Statement tag '{statement_tag}' does not support a moral function or process, but one was provided: '{extra_statement}'." if not value_tags: return False, "At least one moral value is required." valid_values = self.valid_statement_value_pairs.get(statement_tag, {}).get('values', []) if not valid_values: return False, f"Statement tag '{statement_tag}' is not a valid statement tag. Valid tags are: {', '.join(self.valid_statement_tags)}." required_value = self.valid_statement_value_pairs.get(statement_tag, {}).get('required', '') for vt in value_tags: if vt not in valid_values: return False, f"Value tag '{vt}' is invalid for statement '{statement_tag}'. Valid value tags are: {', '.join(valid_values)}." if required_value and required_value not in value_tags: return False, f"Required value tag '{required_value}' is missing for statement '{statement_tag}'." if displayment_desc and not displayment_tag: return False, "Displayment tag must be provided if a displayment description is specified." if displayment_tag and not displayment_desc: return False, f"Displayment tag '{displayment_tag}' provided without a description." if displayment_tag and displayment_tag not in self.valid_displayment_tags: return False, f"Invalid displayment tag '{displayment_tag}'. Valid tags are: {', '.join(self.valid_displayment_tags)}." # Parse interactors (same as before) def parse_interactors(interactor_str, tag_type, valid_tags, has_subcomponents=False): if not interactor_str: return [], "" items = [] entries = [e.strip() for e in interactor_str.split(".") if e.strip()] for e in entries: match = re.match(r"^(?:\d+\.\s*)?(.*?)\s*\((\w+)\)(?:,\s*(?:Means|Displayments)\s*\((.*?)\))?$", e) if not match: return None, f"Invalid {tag_type} format: '{e}'. Expected format: 'Entity (XXxx)[, Means/Displayments (Description (Tag), ...)]'." entity, tag, subcomponents = match.groups() if not entity.strip(): return None, f"{tag_type} '{e}' has an empty entity description." if tag not in valid_tags: return None, f"Invalid {tag_type} tag '{tag}' in '{e}'. Valid tags are: {', '.join(valid_tags)}." item = {'entity': entity, 'tag': tag} if has_subcomponents: if not subcomponents: return None, f"{tag_type} '{entity} ({tag})' requires at least one subcomponent (Means for Enforcers, Displayments for Repeaters)." sub_items = [] sub_entries = [s.strip() for s in subcomponents.split(",") if s.strip()] for s in sub_entries: sub_match = re.match(r"^(.*?)\s*\((\w+)\)$", s) if not sub_match: return None, f"Invalid {tag_type} subcomponent format in '{e}': '{s}'. Expected format: 'Description (MExx/DPxx)'." sub_desc, sub_tag = sub_match.groups() if not sub_desc.strip(): return None, f"{tag_type} subcomponent '{s}' in '{e}' has an empty description." valid_sub_tags = self.valid_means_tags if tag_type == "Enforcers" else self.valid_displayment_tags if sub_tag not in valid_sub_tags: return None, f"Invalid {tag_type} subcomponent tag '{sub_tag}' in '{e}'. Valid tags are: {', '.join(valid_sub_tags)}." sub_items.append({'desc': sub_desc, 'tag': sub_tag}) item['subcomponents'] = sub_items elif subcomponents: return None, f"{tag_type} '{entity} ({tag})' does not support subcomponents, but some were provided: '{subcomponents}'." items.append(item) return items, "" ni_items, ni_error = parse_interactors(ni_str, "Non-interactor", self.valid_ni_tags, False) if ni_error: return False, ni_error ef_items, ef_error = parse_interactors(ef_str, "Enforcers", self.valid_ef_tags, True) if ef_error: return False, ef_error rp_items, rp_error = parse_interactors(rp_str, "Repeaters", self.valid_rp_tags, True) if rp_error: return False, rp_error re_items, re_error = parse_interactors(re_str, "Rejecters", self.valid_re_tags, False) if re_error: return False, re_error ac_items, ac_error = parse_interactors(ac_str, "Accepters", self.valid_ac_tags, False) if ac_error: return False, ac_error # Check required INI tags (same as before) has_required_value = any(vt in self.required_values for vt in value_tags) if has_required_value: all_ini_tags = [item['tag'] for item in ni_items + ef_items + rp_items + re_items + ac_items] if not any(tag in self.required_ini_tags for tag in all_ini_tags): return False, f"Chain requires at least one required INI tag due to specific moral values ({', '.join(value_tags)}). Valid INI tags are: {', '.join(self.required_ini_tags)}." # Accepter compatibility check (same as before) for ac_item in ac_items: ac_tag = ac_item['tag'] ac_entity = ac_item['entity'] if ac_tag in ['AC12', 'AC13', 'AC14', 'AC15']: compat_tags = self.ac_compat.get(ac_tag, []) if not compat_tags: return False, f"Accepter tag '{ac_tag}' is not recognized in compatibility rules." found = False for ef_item in ef_items: if ef_item['entity'] == ac_entity and ef_item['tag'] in compat_tags: found = True break if not found: for rp_item in rp_items: if rp_item['entity'] == ac_entity and rp_item['tag'] in compat_tags: found = True break if not found: return False, f"Accepter '{ac_entity}' ({ac_tag}) requires a compatible Enforcer or Repeater with the same entity text and a tag in: {', '.join(compat_tags)}." # Validate footnotes if present if has_footnotes: valid, footnote_error = self.validate_footnotes(footnote_section, ni_items + ef_items + rp_items + re_items + ac_items) if not valid: return False, footnote_error return True, "" except Exception as e: return False, f"Unexpected error during chain validation: {str(e)}" def validate_footnotes(self, footnote_section, all_interactors): """ Validate the footnotes section for transformation rules, handling both final_tag->initial_tag and initial_tag->final_tag formats, with specific handling for INI (Non-interactor and other) transformations and Accepter compatibility. """ try: # Parse footnote section # Expected format: "Footnotes: Type 'entity' underwent N transformation(s): tag1->tag2; ..." footnote_content = footnote_section.replace("Footnotes:", "").strip() if not footnote_content: return True, "" # Empty footnotes are valid # Split by semicolon to get individual footnote entries entries = [entry.strip() for entry in footnote_content.split(";") if entry.strip()] # Create a lookup dictionary for interactors by entity and type interactor_lookup = {} type_mapping = { 'Non-interactor': 'NI', 'Enforcers': 'EF', 'Repeaters': 'RP', 'Rejecters': 'RE', 'Accepters': 'AC' } for interactor in all_interactors: tag = interactor['tag'] entity = interactor['entity'] if tag.startswith('NI'): interactor_type = 'Non-interactor' elif tag.startswith('EF'): interactor_type = 'Enforcers' elif tag.startswith('RP'): interactor_type = 'Repeaters' elif tag.startswith('RE'): interactor_type = 'Rejecters' elif tag.startswith('AC'): interactor_type = 'Accepters' else: continue # Skip invalid tags key = f"{interactor_type}_{entity}" if key not in interactor_lookup: interactor_lookup[key] = [] interactor_lookup[key].append(interactor) normalization = { 'Enforcer': 'Enforcers', 'Accepter': 'Accepters', 'Repeater': 'Repeaters', 'Rejecter': 'Rejecters', 'Non-interactor': 'Non-interactor' } # Collect all interactor tags for compatibility checks interactor_tags = {item['entity']: {} for item in all_interactors} for item in all_interactors: tag = item['tag'] entity = item['entity'] if tag.startswith('NI'): interactor_tags[entity]['NI'] = tag elif tag.startswith('EF'): interactor_tags[entity]['EF'] = tag elif tag.startswith('RP'): interactor_tags[entity]['RP'] = tag elif tag.startswith('RE'): interactor_tags[entity]['RE'] = tag elif tag.startswith('AC'): interactor_tags[entity]['AC'] = tag for entry in entries: # Parse: "Type 'entity' underwent N transformation(s): tag1->tag2" match = re.match(r"(\w+) '(.+?)' underwent \d+ transformation(?:s)?: (.+)", entry) if not match: return False, f"Invalid footnote format: '{entry}'. Expected: 'Type 'entity' underwent N transformation(s): tag1->tag2'" interactor_type, entity, transformation_chain = match.groups() interactor_type = normalization.get(interactor_type, interactor_type) key = f"{interactor_type}_{entity}" if key not in interactor_lookup: return False, f"Footnote references non-existent interactor: {interactor_type} '{entity}'" # Get the interactor with matching type and entity original_interactor = interactor_lookup[key][0] if interactor_lookup[key] else None if not original_interactor: return False, f"Could not find matching interactor for footnote: {interactor_type} '{entity}'" original_tag = original_interactor['tag'] # Parse transformation chain tags = [tag.strip() for tag in transformation_chain.split("->") if tag.strip()] if not tags: return False, f"Empty transformation chain in footnote for {interactor_type} '{entity}'" # Determine chain direction if tags[0] == original_tag: # Format: final_tag->initial_tag (e.g., EF36->EF32) validation_tags = tags[::-1] # Reverse to validate initial_tag -> final_tag elif tags[-1] == original_tag: # Format: initial_tag->final_tag (e.g., EF32->EF36) validation_tags = tags # Use directly to validate initial_tag -> final_tag else: return False, f"Footnote transformation chain does not contain the interactor's current tag '{original_tag}'. Got: {transformation_chain}" # Validate transformations in forward order (initial_tag -> final_tag) for i in range(len(validation_tags) - 1): current_tag = validation_tags[i] next_tag = validation_tags[i + 1] # Check if transformation is valid valid_conversion = (current_tag in self.conversion_rules and next_tag in self.conversion_rules[current_tag]) valid_transformation = (current_tag in self.transformation_rules and next_tag in self.transformation_rules[current_tag]) if not (valid_conversion or valid_transformation): return False, f"Invalid transformation {current_tag} -> {next_tag} in footnote for {interactor_type} '{entity}'" # Additional validation: cannot transform RP tags if current_tag.startswith("RP"): return False, f"Repeaters (RP tags) cannot be transformed in footnote for {interactor_type} '{entity}'" # Type consistency (NI stays NI, etc.) if current_tag.startswith("NI") != next_tag.startswith("NI"): return False, f"Cannot transform between NI and non-NI types: {current_tag} -> {next_tag}" # Compatibility check for Accepters if interactor_type == "Accepters" and validation_tags[-1].startswith('AC'): final_ac_tag = validation_tags[-1] if final_ac_tag in self.ac_compat: compat_tags = self.ac_compat[final_ac_tag] ef_tag = interactor_tags.get(entity, {}).get('EF') rp_tag = interactor_tags.get(entity, {}).get('RP') compatible = False if ef_tag and ef_tag in compat_tags: compatible = True elif rp_tag and rp_tag in compat_tags: compatible = True if not compatible and (ef_tag or rp_tag): return False, f"Accepter tag '{final_ac_tag}' for '{entity}' is not compatible with Enforcer/Repeater tag '{ef_tag or rp_tag}'. Compatible tags are: {', '.join(compat_tags)}" # Additional validation for Non-interactors (INI) if interactor_type == "Non-interactor" and validation_tags[-1].startswith('NI'): final_ni_tag = validation_tags[-1] if final_ni_tag not in self.valid_ni_tags: return False, f"Final Non-interactor tag '{final_ni_tag}' for '{entity}' is invalid. Valid NI tags are: {', '.join(self.valid_ni_tags)}" if final_ni_tag not in self.required_ini_tags and any(item['tag'] in self.required_ini_tags for item in all_interactors): return False, f"Non-interactor tag '{final_ni_tag}' for '{entity}' must be a required INI tag when other required INI tags are present. Required INI tags are: {', '.join(self.required_ini_tags)}" return True, "" except Exception as e: return False, f"Error validating footnotes: {str(e)}" def add_tab(self): new_tab = ttk.Frame(self.tab_control) tab_count = len(self.tab_control.tabs()) + 1 self.tab_control.add(new_tab, text=f"Text/System {tab_count}") self.setup_tab(new_tab) def add_set(self, tab, format_var, interlinked=False, linked_row_index=None, parent_set_id=None, interlinked_frame=None): format_type = format_var.get() if not format_type: messagebox.showerror("Error", "Please select a set format before adding a set.") return set_container = ttk.Frame(tab.sets_frame if not interlinked else interlinked_frame) set_container.pack(side=tk.TOP if not interlinked else tk.LEFT, padx=10, pady=10, fill=tk.X) # Source Info Frame source_info_frame = ttk.Frame(set_container) source_info_frame.pack(side=tk.LEFT, fill=tk.Y) ttk.Label(source_info_frame, text="Source Identity:").pack(anchor=tk.W) source_identity = ttk.Entry(source_info_frame, width=20) source_identity.pack(anchor=tk.W, pady=2) ttk.Label(source_info_frame, text="General Moral Source:").pack(anchor=tk.W) general_moral_source = ttk.Combobox(source_info_frame, values=self.general_source_options, state="readonly", width=20) general_moral_source.pack(anchor=tk.W, pady=2) ttk.Label(source_info_frame, text="Tributary Source:").pack(anchor=tk.W) tributary_source_var = tk.StringVar(value="No") tributary_source = ttk.Combobox(source_info_frame, textvariable=tributary_source_var, values=["No", "Yes"], state="readonly", width=20) tributary_source.pack(anchor=tk.W, pady=2) tributary_source_entry = ttk.Entry(source_info_frame, width=20) # Set Frame set_frame = ttk.Frame(set_container) set_frame.pack(side=tk.LEFT, fill=tk.X) interlinked_frame = ttk.Frame(set_container) interlinked_frame.pack(side=tk.LEFT, fill=tk.X) # Series Focus ttk.Label(set_frame, text="Series Focus:").pack(anchor=tk.W) series_focus = ttk.Entry(set_frame, width=30) series_focus.pack(anchor=tk.W) # Source Position ttk.Label(set_frame, text="Source Position:").pack(anchor=tk.W) source_position_var = tk.StringVar(value="Top") source_position = ttk.Combobox(set_frame, textvariable=source_position_var, values=["Top", "Middle", "Bottom", "No Position"], state="readonly") source_position.pack(anchor=tk.W, pady=2) ttk.Button(set_frame, text="Delete Set", command=lambda: self.delete_set(set_container, tab)).pack(anchor=tk.W, pady=5) # Initialize set_info with unique set_id set_id = str(uuid.uuid4()) set_info = { 'set_id': set_id, 'tab': tab, 'container': set_container, 'source_info_frame': source_info_frame, 'interlinked_frame': interlinked_frame, 'frame': set_frame, 'source_identity': source_identity, 'general_moral_source': general_moral_source, 'tributary_source': tributary_source_var, 'tributary_source_entry': tributary_source_entry, 'series_focus': series_focus, 'source_position': source_position_var, 'rows': [], 'format': format_type, 'canvas': None, 'table_frame': None, 'linked_row_index': linked_row_index, 'parent_set_id': parent_set_id } # Append to self.sets self.sets.append(set_info) # Add Interlinked Set button for non-interlinked sets if not interlinked: add_interlinked_button = ttk.Button(source_info_frame, text="Add Interlinked Set", command=lambda sid=set_id, r=1: self.add_interlinked_set(tab, sid, r)) set_info['add_interlinked_button'] = add_interlinked_button self.update_tributary_entry(set_info) else: set_info['add_interlinked_button'] = None # Bind Tributary Source Combobox tributary_source.bind("<>", lambda event: self.update_tributary_entry(set_info)) ttk.Button(set_frame, text="Add Moralithic Chain", command=lambda: self.add_moralithic_chain(tab, set_container)).pack(anchor=tk.W, pady=5) canvas_frame = ttk.Frame(set_frame) canvas_frame.pack(fill=tk.BOTH, expand=True) canvas = tk.Canvas(canvas_frame) v_scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.VERTICAL, command=canvas.yview) h_scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.HORIZONTAL, command=canvas.xview) table_frame = ttk.Frame(canvas) canvas.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set) canvas.create_window((0, 0), window=table_frame, anchor="nw") v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X) canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) set_info['canvas'] = canvas set_info['table_frame'] = table_frame columns = ["Main Chain", "Interlinked Chains"] if format_type == "Sequential": columns.insert(0, "Sequential Rank") elif format_type == "Hierarchical": columns.append("Hierarchical Rank") elif format_type == "Sequential + Hierarchical": columns.insert(0, "Sequential Rank") columns.append("Hierarchical Rank") for col, header in enumerate(columns): ttk.Label(table_frame, text=header, font=("Arial", 10, "bold")).grid(row=0, column=col, padx=5, pady=5, sticky="w") rows = [] for i in range(2): row = [] col_offset = 0 if format_type in ["Sequential", "Sequential + Hierarchical"]: seq_rank = ttk.Entry(table_frame, width=5) seq_rank.grid(row=i+1, column=col_offset, padx=5, pady=2, sticky="w") row.append(seq_rank) col_offset += 1 main_chain = ttk.Entry(table_frame, width=30) main_chain.grid(row=i+1, column=col_offset, padx=5, pady=2, sticky="w") row.append(main_chain) interlinked_chains = [ttk.Entry(table_frame, width=30)] interlinked_chains[0].grid(row=i+1, column=col_offset+1, padx=5, pady=2, sticky="w") ttk.Button(table_frame, text="Add Interlinked Chain", command=lambda r=row, c=canvas, f=table_frame, o=col_offset: self.add_interlinked_chain(r, c, f, o)).grid(row=i+1, column=col_offset+2, padx=5, pady=2, sticky="w") row.append(interlinked_chains) if format_type in ["Hierarchical", "Sequential + Hierarchical"]: hier_rank = ttk.Entry(table_frame, width=5) hier_rank.grid(row=i+1, column=col_offset+3, padx=5, pady=2, sticky="w") row.append(hier_rank) rows.append(row) set_info['rows'] = rows tab.sets_canvas.configure(scrollregion=tab.sets_canvas.bbox("all")) def add_interlinked_set(self, tab, set_id, row_index): set_info = next((s for s in self.sets if s['set_id'] == set_id), None) if not set_info: messagebox.showerror("Error", "Selected set not found.") return self.add_set(tab, tab.format_var, interlinked=True, linked_row_index=row_index, parent_set_id=set_id, interlinked_frame=set_info['interlinked_frame']) def add_interlinked_chain(self, row, canvas, table_frame, col_offset): format_type = next(s for s in self.sets if s['canvas'] == canvas)['format'] interlinked_idx = 1 if format_type == "Hierarchical" else 2 if format_type == "Sequential" else 2 interlinked_chains = row[interlinked_idx] if not isinstance(interlinked_chains, list): interlinked_chains = [interlinked_chains] if isinstance(interlinked_chains, ttk.Entry) else [] row[interlinked_idx] = interlinked_chains row_index = row[0].grid_info()['row'] if row[0].grid_info() else row[1].grid_info()['row'] add_button = None for widget in table_frame.winfo_children(): grid_info = widget.grid_info() if (isinstance(widget, ttk.Button) and grid_info.get('row') == row_index and widget.cget('text') == "Add Interlinked Chain"): add_button = widget break new_chain = ttk.Entry(table_frame, width=30) new_chain.grid(row=row_index, column=col_offset+1+len(interlinked_chains), padx=5, pady=2, sticky="w") interlinked_chains.append(new_chain) if add_button: add_button.grid_forget() add_button.grid(row=row_index, column=col_offset+1+len(interlinked_chains), padx=5, pady=2, sticky="w") if format_type in ["Hierarchical", "Sequential + Hierarchical"]: hier_rank = row[-1] hier_rank.grid_forget() hier_rank.grid(row=row_index, column=col_offset+1+len(interlinked_chains)+1, padx=5, pady=2, sticky="w") header_label = None for widget in table_frame.winfo_children(): grid_info = widget.grid_info() if (isinstance(widget, ttk.Label) and grid_info.get('row') == 0 and widget.cget('text') == "Hierarchical Rank"): header_label = widget break if header_label: header_label.grid_forget() header_label.grid(row=0, column=col_offset+1+len(interlinked_chains)+1, padx=5, pady=5, sticky="w") canvas.update_idletasks() canvas.configure(scrollregion=(0, 0, table_frame.winfo_reqwidth(), table_frame.winfo_reqheight())) canvas.xview_moveto(0) canvas_frame = canvas.winfo_parent() canvas_frame_widget = canvas._nametowidget(canvas_frame) canvas_frame_widget.configure(width=max(canvas_frame_widget.winfo_width(), table_frame.winfo_reqwidth())) def add_moralithic_chain(self, tab, set_container): set_info = next((s for s in self.sets if s['container'] == set_container), None) if not set_info: messagebox.showerror("Error", "Selected set not found.") return table_frame = set_info['table_frame'] format_type = set_info['format'] canvas = set_info['canvas'] row_index = len(set_info['rows']) + 1 row = [] col_offset = 0 if format_type in ["Sequential", "Sequential + Hierarchical"]: seq_rank = ttk.Entry(table_frame, width=5) seq_rank.grid(row=row_index, column=col_offset, padx=5, pady=2, sticky="w") row.append(seq_rank) col_offset += 1 main_chain = ttk.Entry(table_frame, width=30) main_chain.grid(row=row_index, column=col_offset, padx=5, pady=2, sticky="w") row.append(main_chain) interlinked_chains = [ttk.Entry(table_frame, width=30)] interlinked_chains[0].grid(row=row_index, column=col_offset+1, padx=5, pady=2, sticky="w") ttk.Button(table_frame, text="Add Interlinked Chain", command=lambda r=row, c=canvas, f=table_frame, o=col_offset: self.add_interlinked_chain(r, c, f, o)).grid(row=row_index, column=col_offset+2, padx=5, pady=2, sticky="w") row.append(interlinked_chains) if format_type in ["Hierarchical", "Sequential + Hierarchical"]: hier_rank = ttk.Entry(table_frame, width=5) hier_rank.grid(row=row_index, column=col_offset+3, padx=5, pady=2, sticky="w") row.append(hier_rank) set_info['rows'].append(row) canvas.configure(scrollregion=canvas.bbox("all")) def delete_set(self, set_container, tab): set_info = next((s for s in self.sets if s['container'] == set_container), None) if not set_info: return sets_to_remove = [s for s in self.sets if s['set_id'] == set_info['set_id'] or s['parent_set_id'] == set_info['set_id']] for s in sets_to_remove: s['container'].destroy() self.sets.remove(s) tab_sets = [s for s in self.sets if s.get('tab') == tab] for set_info in tab_sets: if set_info['parent_set_id'] is None: set_info['container'].pack_forget() set_info['container'].pack(side=tk.TOP, padx=10, pady=10, fill=tk.X) interlinked_groups = {} linked_sets = [s for s in self.sets if s['parent_set_id'] == set_info['set_id']] for s in linked_sets: row_index = s['linked_row_index'] if row_index not in interlinked_groups: interlinked_groups[row_index] = [] interlinked_groups[row_index].append(s) for row_index in interlinked_groups: for s in interlinked_groups[row_index]: s['container'].pack_forget() s['container'].pack(side=tk.LEFT, padx=10, pady=10, fill=tk.X) tab.sets_canvas.configure(scrollregion=tab.sets_canvas.bbox("all")) def save_sets(self): output = {} for tab_id in self.tab_control.tabs(): tab = next(t for t in self.tab_control.winfo_children() if str(t) == tab_id) tab_name = self.tab_control.tab(tab_id, "text") tab_sets = [s for s in self.sets if s.get('tab') == tab] system_focus = tab.system_focus.get() or tab_name interlinked_tabs = tab.interlinked_tabs.get() tab_data = [] for set_info in tab_sets: set_focus = set_info['series_focus'].get() or 'None' set_data = { 'set_id': set_info['set_id'], 'source_identity': set_info['source_identity'].get(), 'general_moral_source': set_info['general_moral_source'].get(), 'tributary_source': set_info['tributary_source'].get(), 'tributary_source_text': set_info['tributary_source_entry'].get() if set_info['tributary_source'].get() == "Yes" else "", 'series_focus': set_info['series_focus'].get(), 'source_position': set_info['source_position'].get(), 'format': set_info['format'], 'rows': [], 'linked_row_index': set_info['linked_row_index'], 'parent_set_id': set_info['parent_set_id'] } for i, row in enumerate(set_info['rows']): interlinked_idx = 1 if set_info['format'] == "Hierarchical" else 2 interlinked_chains = row[interlinked_idx] if not isinstance(interlinked_chains, list): interlinked_chains = [interlinked_chains] if isinstance(interlinked_chains, ttk.Entry) else [] row[interlinked_idx] = interlinked_chains row_data = { 'row_id': str(uuid.uuid4()), 'main_chain': row[1 if set_info['format'] == "Hierarchical" else 1].get(), 'interlinked_chains': [chain.get() for chain in interlinked_chains if chain.get()] } if set_info['format'] in ["Sequential", "Sequential + Hierarchical"]: row_data['sequential_rank'] = row[0].get() if set_info['format'] in ["Hierarchical", "Sequential + Hierarchical"]: row_data['hierarchical_rank'] = row[-1].get() set_data['rows'].append(row_data) tab_data.append(set_data) output[system_focus] = { 'interlinked_tabs': interlinked_tabs, 'sets': tab_data } with open("moralithic_entity_sets.json", "w") as f: json.dump(output, f, indent=2) messagebox.showinfo("Success", "Source sets saved to moralithic_entity_sets.json") if __name__ == "__main__": root = tk.Tk() app = MoralithicChainSetValidatorMode3(root) root.mainloop()