#!/usr/bin/env python3 """ Parse Cognito form definition JS and generate Healthie migration YAML. Output: docs/intake/Cognito_to_Healthie_Form_Mapping.yaml """ import re import json import sys def parse_form_data(filepath): with open(filepath) as f: content = f.read() result = { "form_name": "MetaFlow Enrollment Assessment", "form_version": "cognito_v1", "sections": {}, "conditional_visibility": {}, "page_order": {} } # Extract page order from Page_Index page_match = re.search(r'Page_Index:\s*\{[^{]*constant:\s*\{([^}]+)\}', content) if page_match: for m in re.finditer(r'(\w+):\s*(\d+)', page_match.group(1)): result["page_order"][m.group(1)] = int(m.group(2)) # Extract conditional visibility rules for m in re.finditer(r'(\w+_Visible):\s*\{[^}]*\$transient[^}]*dependsOn:\s*"([^"]+)"', content): section = m.group(1).replace('_Visible', '') depends = m.group(2) result["conditional_visibility"][section] = depends # Parse section definitions section_pattern = re.compile( r"'(Forms\.FormEntry\.MetaFlowHealth1\.MetaFlowEnrollmentAssessment\.(\w+))':\s*\{" ) for sm in section_pattern.finditer(content): section_name = sm.group(2) # Find section body start = sm.end() depth = 1 pos = start while pos < len(content) and depth > 0: if content[pos] == '{': depth += 1 elif content[pos] == '}': depth -= 1 pos += 1 section_body = content[start:pos-1] # Get section label from top-level form entry label_match = re.search( rf'{re.escape(section_name)}:\s*\{{[^}}]*label:\s*"([^"]*)"', content ) section_label = label_match.group(1) if label_match else section_name section_data = { "label": section_label, "fields": [] } # First pass: extract _Choices constant arrays choices_lookup = {} for cm in re.finditer(r'(\w+_Choices):\s*\{', section_body): choice_field_name = cm.group(1) base_name = choice_field_name.replace('_Choices', '') cstart = cm.end() cdepth = 1 cpos = cstart while cpos < len(section_body) and cdepth > 0: if section_body[cpos] == '{': cdepth += 1 elif section_body[cpos] == '}': cdepth -= 1 cpos += 1 cbody = section_body[cstart:cpos-1] # Extract constant array labels labels = re.findall(r'Label:\s*"([^"]*)"', cbody) if labels: choices_lookup[base_name] = labels # Second pass: parse actual fields for fm in re.finditer(r'(\w+):\s*\{', section_body): field_name = fm.group(1) # Skip non-field properties and Cognito UI artifacts noise_names = {'Form', 'init', 'set', 'get', 'function', 'meta', 'label', 'required', 'default', 'type', 'constant', 'allowedValues', 'ignoreValidation', 'preventInvalidValues', 'helptext', 'error', 'dependsOn', 'identifier', 'length', 'max', 'min', 'description', 'reformat', 'expression', 'storageProperties', 'format', 'range', 'Minimum', 'Maximum', 'IncrementBy'} if (field_name.startswith('$') or field_name.startswith('_') or field_name in noise_names or field_name.endswith('_Choices') or field_name.endswith('_IncrementBy') or field_name.endswith('_Minimum') or field_name.endswith('_Maximum') or field_name.endswith('_Maximum') or field_name == 'range' or field_name == 'storageProperties'): continue # Extract field body fstart = fm.end() fdepth = 1 fpos = fstart while fpos < len(section_body) and fdepth > 0: if section_body[fpos] == '{': fdepth += 1 elif section_body[fpos] == '}': fdepth -= 1 fpos += 1 fbody = section_body[fstart:fpos-1] # Skip transient, storageProperties, Choice[] helpers if '$transient' in fbody or field_name == 'storageProperties' or \ field_name.endswith('_Choices') or 'constant' in fbody.split('label:')[0] if 'label:' in fbody else False: continue field_info = {} # Label lmatch = re.search(r'label:\s*"([^"]*)"', fbody) field_info["label"] = lmatch.group(1) if lmatch else field_name # Type tmatch = re.search(r'type:\s*\'?(\w+(?:\[\])?)\'?', fbody) raw_type = tmatch.group(1) if tmatch else 'String' # Map Cognito types to Healthie types if raw_type == 'Boolean': field_info["healthie_type"] = "choice" # Yes/No field_info["choices"] = ["Yes", "No"] elif raw_type == 'Date': field_info["healthie_type"] = "date" elif raw_type == 'String[]' or (raw_type == 'String' and 'allowedValues' in fbody): # Has allowedValues or _Choices = choice or multi-choice av_match = re.search(r'allowedValues:[^{]*function:\s*function\(\)\s*\{[^}]*return\s*\[([^\]]+)\]', fbody) if av_match: choices_str = av_match.group(1) choices = re.findall(r'"([^"]*)"', choices_str) field_info["choices"] = choices elif field_name in choices_lookup: # Cognito stores choices in sibling _Choices constant array field_info["choices"] = choices_lookup[field_name] if raw_type == 'String[]': field_info["healthie_type"] = "checkbox" # multi-select else: field_info["healthie_type"] = "choice" # single select elif raw_type == 'FileDataRef[]': field_info["healthie_type"] = "file_upload" else: field_info["healthie_type"] = "text" # Cognito field ID field_info["cognito_field_id"] = field_name # Helptext hmatch = re.search(r'helptext:\s*"(<[^>]*>[^"]*?)"', fbody) if not hmatch: hmatch = re.search(r'helptext:\s*"([^"]*)"', fbody) if hmatch: ht = re.sub(r'<[^>]+>', '', hmatch.group(1)).strip() if ht and not ht.startswith('*'): field_info["helptext"] = ht # Required if 'required:' in fbody: field_info["required"] = True # Max length lm = re.search(r'max:\s*(\d+)', fbody) if lm: field_info["max_length"] = int(lm.group(1)) section_data["fields"].append(field_info) result["sections"][section_name] = section_data # Parse top-level fields (not in a section) top_level_fields = [] main_type = "'Forms.FormEntry.MetaFlowHealth1.MetaFlowEnrollmentAssessment':" main_idx = content.find(main_type) if main_idx >= 0: brace_start = content.find('{', main_idx) depth = 1 pos = brace_start + 1 while pos < len(content) and depth > 0: if content[pos] == '{': depth += 1 elif content[pos] == '}': depth -= 1 pos += 1 main_body = content[brace_start+1:pos-1] # Section type references to skip (they're nested types) section_refs = {'WholeSystem', 'DigestionIntakeEnergyPattern', 'BowelUrinaryFluidPattern', 'SafetyScreening2', 'Female', 'Male', 'MouthThroatNoseEyesEars', 'SkinSweatingSurfaceReadout', 'SleepStressEmotionalState', 'BodyMetrics', 'PainInjuryMusculoskeletalFunctionalBoundary', 'MedicalHistoryMedicationSafetyReview', 'InfectionImmuneEndocrineAxisReview', 'VascularPlaqueAtherosclerosisVascularNarrowing', 'FattyLiver', 'KidneyUrineRenalFunctionReports', 'ReportUpload', 'PreEnrollmentACCESSDataRecordsParticipationReadinessAcknowledgment2', 'FinalReviewSubmission'} skip_ids = ('Id', 'Entry', 'Form', 'Form_Theme', 'Form_Available', 'Allow_Workflow_Links', 'Entry_Statuses', 'storageProperties', 'Female_Visible', 'Male_Visible', '_p1_Visible', '_p0_Visible', '_p2_Visible', '_p3_Visible', 'Page3Visible', 'Page_Index', 'VascularPlaqueAtherosclerosisVascularNarrowing_Visible', 'FattyLiver_Visible', 'KidneyUrineRenalFunctionReports_Visible', 'NameEntryNumber_Choices', 'SexOnMedicalOrLabRecords_Choices', 'DoYouHaveAnyKnownAllergiesOrSensitivitiesToMedicationsFoodsHerbsSupplementsTopicalProductsFragrancesOrEssentialOils_Choices', '$storageProperties', 'init', 'set', 'get', 'function', 'meta', 'format', 'label', 'required', 'default', 'type', 'constant', 'allowedValues', 'ignoreValidation', 'preventInvalidValues', 'helptext', 'error', 'dependsOn', 'identifier', 'length', 'max', 'min', 'description', 'reformat', 'expression') for fm in re.finditer(r'(\w+):\s*\{', main_body): field_name = fm.group(1) if field_name in skip_ids or field_name in section_refs: continue # Skip nested section definitions if fm.start() > main_body.find('Female:'): break fstart = fm.end() fdepth = 1 fpos = fstart while fpos < len(main_body) and fdepth > 0: if main_body[fpos] == '{': fdepth += 1 elif main_body[fpos] == '}': fdepth -= 1 fpos += 1 fbody = main_body[fstart:fpos-1] if '$transient' in fbody or 'storageProperties' in fbody: continue field_info = {} lmatch = re.search(r'label:\s*"([^"]*)"', fbody) field_info["label"] = lmatch.group(1) if lmatch else field_name tmatch = re.search(r'type:\s*\'?(\w+(?:\[\])?)\'?', fbody) raw_type = tmatch.group(1) if tmatch else 'String' if raw_type == 'Boolean': field_info["healthie_type"] = "choice" field_info["choices"] = ["Yes", "No"] elif raw_type == 'Date': field_info["healthie_type"] = "date" elif raw_type == 'String[]' or (raw_type == 'String' and 'allowedValues' in fbody): av_match = re.search(r'allowedValues:[^{]*function:\s*function\(\)\s*\{[^}]*return\s*\[([^\]]+)\]', fbody) if av_match: choices = re.findall(r'"([^"]*)"', av_match.group(1)) field_info["choices"] = choices field_info["healthie_type"] = "checkbox" if raw_type == 'String[]' else "choice" elif raw_type == 'String[]': field_info["healthie_type"] = "checkbox" else: field_info["healthie_type"] = "text" elif raw_type in ('Name', 'Address'): # Name and Address are composite types - split into sub-fields if raw_type == 'Name': field_info["healthie_type"] = "composite" field_info["sub_fields"] = ["First", "Middle", "Last"] elif raw_type == 'Address': field_info["healthie_type"] = "composite" field_info["sub_fields"] = ["Line1", "Line2", "City", "State", "PostalCode", "Country"] else: field_info["healthie_type"] = "text" field_info["cognito_field_id"] = field_name hmatch = re.search(r'helptext:\s*"(<[^>]*>[^"]*?)"', fbody) if not hmatch: hmatch = re.search(r'helptext:\s*"([^"]*)"', fbody) if hmatch: ht = re.sub(r'<[^>]+>', '', hmatch.group(1)).strip() if ht and not ht.startswith('*'): field_info["helptext"] = ht if 'required:' in fbody: field_info["required"] = True top_level_fields.append(field_info) result["top_level_fields"] = top_level_fields return result def output_yaml(data, outpath): """Write YAML output manually to avoid pyyaml dependency.""" lines = [] lines.append("# Cognito to Healthie Form Migration Mapping") lines.append("# Auto-generated from cognito_form_data.js form definition") lines.append("# Provides field-level detail for rebuilding the form in Healthie") lines.append("") lines.append(f"form_name: \"{data['form_name']}\"") lines.append(f"form_version: \"{data['form_version']}\"") lines.append("") lines.append("# ============================================================") lines.append("# PAGE ORDER (from Cognito Page_Index)") lines.append("# ============================================================") lines.append("page_order:") for field, page in sorted(data["page_order"].items(), key=lambda x: x[1]): lines.append(f" {field}: {page}") lines.append("") lines.append("# ============================================================") lines.append("# CONDITIONAL VISIBILITY") lines.append("# Sections hidden/shown based on field values") lines.append("# ============================================================") lines.append("conditional_visibility:") for section, depends in data["conditional_visibility"].items(): lines.append(f" {section}:") lines.append(f" depends_on: \"{depends}\"") if 'Female' in section or 'Male' in section: if 'Female' in section: lines.append(f" condition: \"equals\"") lines.append(f" value: \"Female\"") else: lines.append(f" condition: \"equals\"") lines.append(f" value: \"Male\"") elif 'Vascular' in section: lines.append(f" condition: \"contains\"") lines.append(f" values:") lines.append(f" - \"Coronary artery disease / history of heart attack\"") lines.append(f" - \"Vascular Plaque / Atherosclerosis / Vascular Narrowing\"") elif 'FattyLiver' in section: lines.append(f" condition: \"contains\"") lines.append(f" value: \"Fatty liver\"") elif 'Kidney' in section: lines.append(f" condition: \"contains\"") lines.append(f" values:") lines.append(f" - \"Chronic kidney disease / CKD\"") lines.append(f" - \"Abnormal urine protein or abnormal uACR\"") elif section in ('_p0', '_p1', '_p2', '_p3'): pass # internal Cognito page gating, not needed in Healthie lines.append("") lines.append("# ============================================================") lines.append("# TOP-LEVEL FIELDS (not in a section)") lines.append("# ============================================================") lines.append("top_level_fields:") for f in data["top_level_fields"]: lines.append(f" - cognito_field_id: \"{f['cognito_field_id']}\"") lines.append(f" label: \"{f['label']}\"") lines.append(f" healthie_type: {f['healthie_type']}") if "helptext" in f: lines.append(f" helptext: \"{f['helptext']}\"") if "choices" in f: lines.append(f" choices:") for c in f["choices"]: lines.append(f" - \"{c}\"") if "required" in f: lines.append(f" required: true") if "sub_fields" in f: lines.append(f" sub_fields: {f['sub_fields']}") lines.append("") lines.append("# ============================================================") lines.append("# SECTION DEFINITIONS") lines.append("# ============================================================") for section_name, section_data in data["sections"].items(): lines.append("") lines.append(f"# --- {section_data['label']} ---") lines.append(f"{section_name}:") lines.append(f" section_label: \"{section_data['label']}\"") lines.append(f" fields:") for f in section_data["fields"]: lines.append(f" - cognito_field_id: \"{f['cognito_field_id']}\"") lines.append(f" label: \"{f['label']}\"") lines.append(f" healthie_type: {f['healthie_type']}") if "helptext" in f: lines.append(f" helptext: \"{f['helptext']}\"") if "choices" in f: lines.append(f" choices:") for c in f["choices"]: lines.append(f" - \"{c}\"") if "required" in f: lines.append(f" required: true") if "max_length" in f: lines.append(f" max_length: {f['max_length']}") return "\n".join(lines) if __name__ == "__main__": data = parse_form_data("sample_data/cognito_form_data.js") yaml_content = output_yaml(data, None) with open("docs/intake/Cognito_to_Healthie_Form_Mapping.yaml", "w") as f: f.write(yaml_content) print(f"Generated migration YAML with {len(data['top_level_fields'])} top-level fields and {len(data['sections'])} sections.")