#!/usr/bin/env python3 import json import pathlib import sys ROOT = pathlib.Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) from lib.record_type_merge import record_type_identities FINAL_PACK = ROOT / "packs" / "NOT-EDIT-final-merged-packs" L2_PACK = ROOT / "packs" / "Salesforce-PingCAP-L2" CHANGE_OWNER_FLOW_NAME = "Std_ChangeOwner" CHANGE_OWNER_FLOW_PATH = FINAL_PACK / "flows" / f"{CHANGE_OWNER_FLOW_NAME}.flow.json" CHANGE_OWNER_TARGET_OBJECTS = [ "Lead", "Account", "Contact", "Opportunity", "Case", "Campaign", "Contract", "Order", "Task", "Event", ] ACTIVITY_TIMELINE_HEADER_FALLBACK_INELIGIBLE_ACTION_TARGETS = { "engagement": {"Contact"}, "taskCall": {"Case", "Contact", "Contract", "Lead", "Opportunity", "Order"}, "taskEmail": {"Case", "Contract", "Opportunity", "Order"}, "taskWeeklyUpdate": {"Opportunity"}, "taskLeadWeeklyUpdate": {"Lead"}, } sys.path.insert(0, str(ROOT / "scripts" / "lib")) from final_pack_formula_policy import DEFERRED_ACTIVE_FORMULA_FIELDS def load_json(path: pathlib.Path): return json.loads(path.read_text()) def object_by_api_name(spec_payload, api_name: str): for item in spec_payload: if item.get("apiName") == api_name: return item raise KeyError(f"object not found: {api_name}") def field_map(obj_payload): return {field.get("apiName"): field for field in obj_payload.get("fields", [])} def managed_relationship_map(obj_payload): return { (relationship.get("fieldApiName"), relationship.get("parentObject")): relationship for relationship in obj_payload.get("managedRelationships", []) } def record_type_map(payload): return { item["objectApiName"]: [entry.get("name") for entry in item.get("types", []) if entry.get("name")] for item in payload.get("recordTypes", []) } def record_type_code_map(payload): return { (item.get("objectApiName"), entry.get("name")): entry.get("code") for item in payload.get("recordTypes", []) for entry in item.get("types", []) if item.get("objectApiName") and entry.get("name") and entry.get("code") } def validate_quote_record_type_contract(record_types, upstream_identities, errors): expected = {"Expansion", "New", "Renewal"} actual = set(record_types.get("Quote", [])) if actual != expected: errors.append( "Quote record types must be exactly Expansion, New, and Renewal; got: " + (", ".join(sorted(actual)) or "") ) if "Quote/Standard" in set(upstream_identities or []): errors.append("final pack lineage must not retain the synthetic Quote/Standard identity") def action_names_for_object(actions_payload, object_api_name: str): names = set() for action in actions_payload: if action.get("objectApiName") == object_api_name: names.add(action.get("name")) if object_api_name in action.get("appliesTo", []): names.add(action.get("name")) return names def action_for_object(actions_payload, object_api_name: str, action_name: str): for action in actions_payload: if action.get("name") != action_name: continue if action.get("objectApiName") == object_api_name: return action if object_api_name in action.get("appliesTo", []): return action return None def validate_native_file_upload_field_updates( action_name: str, field_updates: dict, expected_status: str, errors: list[str], ) -> None: if field_updates.get("Status") != expected_status: errors.append( f"{action_name} does not transition Status to {expected_status}" ) if field_updates.get("Archived__c") is not True: errors.append(f"{action_name} does not mark Archived__c true") if "StandardUploadUrl__c" in field_updates: errors.append( f"{action_name} must not persist StandardUploadUrl__c for native Runtime Files uploads" ) def invocable_action_by_name(actions_payload, action_name: str): for action in actions_payload: if action.get("name") == action_name: return action raise KeyError(f"invocable standard action not found: {action_name}") def invocable_action_input_by_name(action_payload, input_name: str): for item in action_payload.get("inputs", []): if item.get("apiName") == input_name: return item return None def validate_weekly_update_required_inputs(action_payload, errors): action_name = action_payload.get("name") for input_name, target_field in { "Progress": "Progress__c", "Risk_and_Next_Step": "Risk_and_Next_Step__c", }.items(): action_input = invocable_action_input_by_name(action_payload, input_name) if action_input is None: errors.append(f"{action_name} is missing {input_name} input") continue if action_input.get("dataType") != "textarea": errors.append(f"{action_name} {input_name} input is not a textarea") if action_input.get("isRequired") is not True: errors.append(f"{action_name} {input_name} input is not required") if action_input.get("targetObject") != "Task" or action_input.get("targetFieldAPIName") != target_field: errors.append(f"{action_name} {input_name} input is not mapped to Task.{target_field}") def validate_non_weekly_update_activity_inputs(action_payload, errors): action_name = action_payload.get("name") weekly_update_inputs = { item.get("apiName") for item in action_payload.get("inputs", []) if item.get("apiName") in {"Progress", "Risk_and_Next_Step"} } if weekly_update_inputs: errors.append( f"{action_name} exposes Weekly Update-only inputs: {', '.join(sorted(weekly_update_inputs))}" ) def validate_activity_timeline_header_fallback_policy(actions_payload, errors): for action_name, expected_targets in ACTIVITY_TIMELINE_HEADER_FALLBACK_INELIGIBLE_ACTION_TARGETS.items(): action_payload = invocable_action_by_name(actions_payload, action_name) if action_payload.get("category") != "ActivityTimeline": errors.append(f"{action_name} category is not ActivityTimeline") if action_payload.get("headerFallbackEligible") is not False: errors.append(f"{action_name} must set headerFallbackEligible=false") targets = set(action_payload.get("targetObjects", [])) missing_targets = expected_targets - targets if missing_targets: errors.append( f"{action_name} is missing ActivityTimeline target objects: {', '.join(sorted(missing_targets))}" ) def validate_lead_change_owner_standard_action(actions_payload, invocable_actions_payload, errors): detail_action = action_for_object(actions_payload, "Lead", "ChangeOwner") if detail_action is None: errors.append("Lead detail page is missing ChangeOwner UI action") return if detail_action.get("label") != "Change Owner": errors.append("Lead ChangeOwner UI action label must be Change Owner") if detail_action.get("type") != "ChangeOwner": errors.append("Lead ChangeOwner UI action type must be ChangeOwner") try: standard_action = invocable_action_by_name(invocable_actions_payload, "ChangeOwner") except KeyError: errors.append("Lead ChangeOwner detail action is missing standard-action discovery metadata") return if standard_action.get("label") != detail_action.get("label"): errors.append("ChangeOwner standard-action label must match the Lead detail UI action label") if detail_action.get("appliesTo") != CHANGE_OWNER_TARGET_OBJECTS: errors.append("ChangeOwner UI action must apply to the standard owner-bearing objects") if standard_action.get("targetObjects") != CHANGE_OWNER_TARGET_OBJECTS: errors.append("ChangeOwner standard-action discovery must target every standard owner-bearing object") if standard_action.get("requiredPermissions") != ["ApiEnabled"]: errors.append("ChangeOwner standard-action discovery must require only ApiEnabled") if standard_action.get("flowDeveloperName") != CHANGE_OWNER_FLOW_NAME: errors.append("ChangeOwner standard-action must reference Std_ChangeOwner") if standard_action.get("visibilityCondition") != (detail_action.get("config") or {}).get("visibilityCondition"): errors.append("ChangeOwner standard-action visibility must match the Lead detail UI action") if standard_action.get("headerFallbackEligible") is not False: errors.append("ChangeOwner standard-action discovery must opt out of unauthored header fallback") input_names = {item.get("apiName") for item in standard_action.get("inputs", [])} if input_names != {"recordId", "objectApiName", "newOwnerId", "sendNotification"}: errors.append("ChangeOwner standard-action input schema is incomplete") if not CHANGE_OWNER_FLOW_PATH.is_file(): errors.append("Std_ChangeOwner flow is missing from the final pack") return flow = load_json(CHANGE_OWNER_FLOW_PATH) if flow.get("status") != "Active": errors.append("Std_ChangeOwner flow must be active") if flow.get("processType") != "AutoLaunchedFlow": errors.append("Std_ChangeOwner flow must be autolaunched") if flow.get("runAtomically") is not True: errors.append("Std_ChangeOwner flow must use the bounded-atomic Runtime transfer mechanism") start_target = ((flow.get("start") or {}).get("connector") or {}).get("targetReference") if start_target != "InvokeChangeOwner": errors.append("Std_ChangeOwner flow must execute the ChangeOwner actionCall") custom_error_names = {item.get("name") for item in flow.get("customErrors", [])} if "RejectDirectInvoke" in custom_error_names: errors.append("Std_ChangeOwner flow must not reject standard action execution") action_call = next((item for item in flow.get("actionCalls", []) if item.get("name") == "InvokeChangeOwner"), None) if action_call is None: errors.append("Std_ChangeOwner flow is missing the InvokeChangeOwner actionCall") else: if action_call.get("actionType") != "transferRecordOwner": errors.append("Std_ChangeOwner InvokeChangeOwner must use transferRecordOwner") if action_call.get("actionName") != "transferRecordOwner": errors.append("Std_ChangeOwner InvokeChangeOwner must name transferRecordOwner") inputs = { item.get("name"): item.get("value") for item in action_call.get("inputParameters", []) } if inputs.get("recordId") != {"elementReference": "recordId"}: errors.append("Std_ChangeOwner InvokeChangeOwner must pass recordId") if inputs.get("objectApiName") != {"elementReference": "objectApiName"}: errors.append("Std_ChangeOwner InvokeChangeOwner must pass objectApiName") if inputs.get("newOwnerId") != {"elementReference": "newOwnerId"}: errors.append("Std_ChangeOwner InvokeChangeOwner must pass newOwnerId") if inputs.get("sendNotification") != {"elementReference": "sendNotification"}: errors.append("Std_ChangeOwner InvokeChangeOwner must pass sendNotification") for name, expected in { "ownerFieldApiName": "OwnerId", "principalObjectApiName": "User", "principalActiveFieldApiName": "IsActive", "globalTransferCapability": "TransferAnyEntity", }.items(): if inputs.get(name) != expected: errors.append(f"Std_ChangeOwner InvokeChangeOwner must author {name}={expected}") if inputs.get("objectTransferCapability") != {"elementReference": "ObjectTransferCapability"}: errors.append("Std_ChangeOwner must derive the object-specific transfer capability") outputs = { item.get("name"): (item.get("value") or {}).get("elementReference") for item in action_call.get("outputParameters", []) } if outputs.get("recordId") != "recordId": errors.append("Std_ChangeOwner InvokeChangeOwner must map recordId output") if outputs.get("previousOwnerId") != "previousOwnerId": errors.append("Std_ChangeOwner InvokeChangeOwner must map previousOwnerId output") if outputs.get("newOwnerId") != "newOwnerId": errors.append("Std_ChangeOwner InvokeChangeOwner must map newOwnerId output") output_vars = { item.get("name") for item in flow.get("variables", []) if item.get("isOutput") is True } if "previousOwnerId" not in output_vars: errors.append("Std_ChangeOwner flow must expose previousOwnerId output") def field_mapping_by_source_target_action( mappings_payload, source_object: str, target_object: str, action_type: str, ): for mapping in mappings_payload: if ( mapping.get("sourceObject") == source_object and mapping.get("targetObject") == target_object and mapping.get("actionType") == action_type ): return mapping return None def validation_rule_by_name(rules_payload, object_api_name: str, rule_name: str): for rule in rules_payload.get("validationRules", []): if rule.get("objectApiName") == object_api_name and rule.get("name") == rule_name: return rule return None def section_field_set(layout_payload): values = set() for section in layout_payload.get("sections", []): for field in section.get("fields", []): values.add(field) return values def layout_tab_set(layout_payload): values = set() for tab in layout_payload.get("tabs", []): if isinstance(tab, str): values.add(tab) elif isinstance(tab, dict) and tab.get("label"): values.add(tab["label"]) return values def layout_record_panel_by_renderer(layout_payload, renderer_id: str): for panel in layout_payload.get("recordPanels", []): if panel.get("rendererId") == renderer_id: return panel return None def related_list_map(layout_payload): return { item.get("id"): item for item in layout_payload.get("relatedLists", []) if item.get("id") } def related_list_rule_keys(payload, parent_object_api_name: str): return { item.get("key") for item in payload.get("rules", []) if item.get("parentObjectApiName") == parent_object_api_name and item.get("key") } def related_list_rule_by_key(payload, key: str): for item in payload.get("rules", []): if item.get("key") == key: return item return None def autonumber_seed_map(payload): return { (item.get("objectName"), item.get("fieldApiName")): item for item in payload if item.get("objectName") and item.get("fieldApiName") } def record_type_picklist_value_component_map(pack_payload): return { component.get("businessKey"): component for component in pack_payload.get("components", []) if component.get("kind") == "record_type_picklist_value" and component.get("businessKey") } def component_by_kind_and_key(pack_payload, kind: str, business_key: str): for component in pack_payload.get("components", []): if component.get("kind") == kind and component.get("businessKey") == business_key: return component return None def parsed_token_refs(seed): raw = seed.get("tokenRefsJson") if not raw: return [] return json.loads(raw) def fail(errors): print("final pack metadata audit FAILED", file=sys.stderr) for error in errors: print(f"- {error}", file=sys.stderr) sys.exit(1) def duplicate_key_prefixes(spec_payload): seen = {} duplicates = {} for item in spec_payload: if not isinstance(item, dict): continue api_name = item.get("apiName") key_prefix = item.get("keyPrefix") if not api_name or not key_prefix: continue if key_prefix in seen: duplicates.setdefault(key_prefix, [seen[key_prefix]]) duplicates[key_prefix].append(api_name) continue seen[key_prefix] = api_name return duplicates def load_l2_object_specs(): specs = [] for path in sorted((L2_PACK / "objects").glob("*.json")): payload = load_json(path) specs.extend(payload if isinstance(payload, list) else [payload]) return specs def validate_active_formula_retention( l2_spec_payload, final_spec_payload, errors, *, deferred_fields=None, ): """Require every authored active formula to keep its semantics in the final pack.""" deferred = ( DEFERRED_ACTIVE_FORMULA_FIELDS if deferred_fields is None else frozenset(deferred_fields) ) final_objects = { obj.get("apiName"): obj for obj in final_spec_payload if isinstance(obj, dict) and obj.get("apiName") } authored_active_fields = set() semantic_keys = ( "type", "formulaMode", "formula", "returnType", "projection", "formulaStorageMode", ) for l2_object in l2_spec_payload: if not isinstance(l2_object, dict): continue object_api_name = l2_object.get("apiName") if not object_api_name: continue final_object = final_objects.get(object_api_name) final_fields = field_map(final_object or {}) for l2_field in l2_object.get("fields", []): if not isinstance(l2_field, dict) or l2_field.get("formulaMode") != "active": continue field_api_name = l2_field.get("apiName") if not field_api_name: continue field_key = (object_api_name, field_api_name) authored_active_fields.add(field_key) final_field = final_fields.get(field_api_name) if field_key in deferred: if final_field is None: errors.append( f"{object_api_name}.{field_api_name} deferred active formula field is missing from the final pack" ) continue expected_type = l2_field.get("returnType") if expected_type and final_field.get("type") != expected_type: errors.append( f"{object_api_name}.{field_api_name} deferred active formula must downgrade to type {expected_type}" ) retained_keys = [ key for key in semantic_keys[1:] if key in final_field ] if retained_keys: errors.append( f"{object_api_name}.{field_api_name} deferred active formula still carries {', '.join(retained_keys)}" ) continue if final_object is None: errors.append( f"{object_api_name}: object with authored active formulas is missing from the final pack" ) break if final_field is None: errors.append( f"{object_api_name}.{field_api_name} authored active formula field is missing from the final pack" ) continue for key in semantic_keys: if final_field.get(key) != l2_field.get(key): errors.append( f"{object_api_name}.{field_api_name} active formula {key} changed: " f"{l2_field.get(key)!r} -> {final_field.get(key)!r}" ) for object_api_name, field_api_name in sorted(deferred - authored_active_fields): errors.append( f"deferred active formula policy references non-active L2 field {object_api_name}.{field_api_name}" ) def expected_campaign_member_active_person_fields(): return { "Name": { "returnType": "string", "formula": "IF(ISBLANK(ContactId), Lead.Name, Contact.Name)", }, "FirstName": { "returnType": "string", "formula": "IF(ISBLANK(ContactId), Lead.FirstName, Contact.FirstName)", }, "LastName": { "returnType": "string", "formula": "IF(ISBLANK(ContactId), Lead.LastName, Contact.LastName)", }, "Email": { "returnType": "email", "formula": "IF(ISBLANK(ContactId), Lead.Email, Contact.Email)", }, "Phone": { "returnType": "phone", "formula": "IF(ISBLANK(ContactId), BLANKVALUE(Lead.Phone, Lead.MobilePhone), BLANKVALUE(Contact.Phone, Contact.MobilePhone))", }, "CompanyOrAccount": { "returnType": "string", "formula": "IF(ISBLANK(ContactId), Lead.Company, Contact.Account.Name)", }, } def validate_campaign_member_active_person_fields(campaign_member_fields, errors): for field_name, expected in expected_campaign_member_active_person_fields().items(): field = campaign_member_fields.get(field_name) if not field: errors.append(f"CampaignMember is missing active person field {field_name}") continue if field.get("type") != "formula": errors.append(f"CampaignMember.{field_name} must remain a formula field") if field.get("formulaMode") != "active": errors.append(f"CampaignMember.{field_name} formulaMode must remain active") if field.get("returnType") != expected["returnType"]: errors.append( f"CampaignMember.{field_name} returnType must be {expected['returnType']}" ) if field.get("formula") != expected["formula"]: errors.append(f"CampaignMember.{field_name} formula does not match the Salesforce read-through contract") def validate_campaign_member_query_indices(campaign_member, errors): indices = { tuple(index.get("columns") or []) for index in campaign_member.get("indices", []) } expected_indices = { ("LeadId", "CreatedDate"), ("ContactId", "CreatedDate"), } missing_indices = sorted(expected_indices - indices) for columns in missing_indices: errors.append( f"CampaignMember is missing query index on {', '.join(columns)}" ) def validate_campaign_member_lead_conversion_relationships( campaign_member_fields, errors ): for field_name in ("CampaignId", "ContactId", "LeadId"): field = campaign_member_fields.get(field_name) if not field: errors.append(f"CampaignMember is missing {field_name} in final object spec") continue if field.get("createable", True) is not True: errors.append(f"CampaignMember.{field_name} must remain createable") if field.get("updateable", True) is not False: errors.append(f"CampaignMember.{field_name} must remain non-updateable") if "writeProtectionPolicy" in field: errors.append( f"CampaignMember.{field_name} must not block standard Lead conversion" ) status = campaign_member_fields.get("Status") if not status: errors.append("CampaignMember is missing Status in final object spec") return if status.get("createable", True) is not True: errors.append("CampaignMember.Status must remain createable") if status.get("updateable", True) is not True: errors.append("CampaignMember.Status must remain updateable") if "writeProtectionPolicy" in status: errors.append("CampaignMember.Status must not be write-protected") def layout_component_keys(pack_payload): return { component.get("businessKey") for component in pack_payload.get("components", []) if component.get("kind") == "page_layout" } def layout_component_paths(pack_payload): return { component.get("path") for component in pack_payload.get("components", []) if component.get("kind") == "page_layout" and component.get("path") } def layout_fields(layout_payload): fields = set(layout_payload.get("compactLayout") or []) for section in layout_payload.get("sections", []): fields.update(section.get("fields") or []) return fields def validate_layout_required_create_inputs(object_spec, layout_payload, errors): """An editable required value needs a form input or a declared producer. A compact-layout reference is display-only and cannot satisfy this contract. Calculated, generated, defaulted and non-createable fields are server-owned. """ section_fields = set() for section in layout_payload.get("sections", []): for item in section.get("fields", []) or []: if isinstance(item, str): section_fields.add(item) elif isinstance(item, dict): name = item.get("fieldApiName") or item.get("apiName") if name: section_fields.add(name) for field in object_spec.get("fields", []): if not field.get("required") or field.get("createable") is False: continue if field.get("type") in {"formula", "autonumber"}: continue if field.get("default") not in (None, ""): continue name = field.get("apiName") if name and name not in section_fields: errors.append( f"{object_spec.get('apiName')}.{name} is required on create but " f"missing from editable sections in layout {layout_payload.get('layoutName')}" ) def validate_layout_visible_official_read_only_fields(spec_payload, errors): expected = { "Pricebook2": { "IsArchived": ("createable", "updateable"), "IsStandard": ("createable", "updateable"), }, } object_specs = {item.get("apiName"): item for item in spec_payload} for object_api_name, field_attrs in expected.items(): layout_path = FINAL_PACK / "layouts" / f"{object_api_name.lower()}_page_layout.json" if not layout_path.exists(): errors.append(f"{object_api_name} official layout is missing: {layout_path.relative_to(ROOT)}") continue visible_fields = layout_fields(load_json(layout_path)) fields = field_map(object_specs.get(object_api_name, {})) for field_name, attrs in field_attrs.items(): if field_name not in visible_fields: errors.append(f"{object_api_name}.{field_name} must stay visible on the official default layout") field = fields.get(field_name) if field is None: errors.append(f"{object_api_name}.{field_name} is missing from final object spec") continue for attr in attrs: if field.get(attr) is not False: errors.append(f"{object_api_name}.{field_name} must preserve official {attr}=false") def expected_l2_layout_component_keys(): keys = set() layouts_dir = L2_PACK / "layouts" for layout_path in sorted(layouts_dir.glob("*.json")): payload = load_json(layout_path) if payload.get("source") == "auto_generated": continue object_api_name = payload.get("objectApiName") if object_api_name: keys.add(f"{object_api_name}/default") return keys def expected_final_l2_layout_component_keys(l2_pack_payload): disabled_layout_keys = { "Campaign/default", "Opportunity/default", } return { component.get("businessKey") for component in l2_pack_payload.get("components", []) if component.get("kind") == "page_layout" and component.get("businessKey") and component.get("businessKey") not in disabled_layout_keys } def validate_order_status_picklist_scope(order_fields, pack_payload, errors): status = order_fields.get("Status") if not status: errors.append("Order is missing Status in final object spec") return required_values = ["Draft", "Approved", "Activated"] actual_values = [item.get("value") for item in status.get("options", [])] for value in required_values: if value not in actual_values: errors.append(f"Order.Status does not include {value}") expected_values = actual_values components = record_type_picklist_value_component_map(pack_payload) for record_type_name in ["Expansion", "New", "Renewal"]: business_key = f"Order/{record_type_name}/Status" component = components.get(business_key) if component is None: errors.append(f"pack.json is missing record_type_picklist_value component: {business_key}") continue path = component.get("path") if not path: errors.append(f"record_type_picklist_value {business_key} is missing path") continue payload_path = FINAL_PACK / path if not payload_path.is_file(): errors.append(f"record_type_picklist_value payload is missing: {path}") continue payload = load_json(payload_path) if payload.get("objectApiName") != "Order" or payload.get("recordTypeName") != record_type_name or payload.get("fieldApiName") != "Status": errors.append(f"record_type_picklist_value {business_key} payload tuple does not match component key") scoped_values = [item.get("value") for item in payload.get("values", [])] if scoped_values != expected_values: errors.append(f"record_type_picklist_value {business_key} values are not {expected_values}") defaults = [item.get("value") for item in payload.get("values", []) if item.get("default") is True] if defaults != ["Draft"]: errors.append(f"record_type_picklist_value {business_key} default is not Draft") for dependency in [ {"kind": "object_spec", "businessKey": "standard_objects_spec:crm"}, {"kind": "record_type", "businessKey": "record_types"}, ]: if dependency not in component.get("dependsOn", []): errors.append(f"record_type_picklist_value {business_key} is missing dependency {dependency}") def validate_related_list_metadata(layout_payload, object_api_name: str, errors): for item in layout_payload.get("relatedLists", []): related_id = item.get("id") child_object = item.get("objectApiName") lookup_field = item.get("lookupField") if not child_object: errors.append(f"{object_api_name} layout has related list without objectApiName: {related_id}") continue if not lookup_field: errors.append(f"{object_api_name} layout related list {child_object} is missing lookupField") continue expected_id = f"{child_object}-{lookup_field}" if related_id != expected_id: errors.append(f"{object_api_name} layout related list {related_id} should use canonical id {expected_id}") if not item.get("secondaryLabel"): errors.append(f"{object_api_name} layout related list {expected_id} is missing secondaryLabel") def validate_reference_field(fields, object_api_name: str, field_name: str, expected_target: str, errors): field = fields.get(field_name) if not field: errors.append(f"{object_api_name} is missing {field_name} in final object spec") return if field.get("type") != "reference": errors.append(f"{object_api_name}.{field_name} is not a reference field") if field.get("referenceTo") != [expected_target]: errors.append(f"{object_api_name}.{field_name} referenceTo is not exactly [{expected_target}]") if field.get("validator"): errors.append(f"{object_api_name}.{field_name} must not declare runtime lookup validator metadata") def validate_campaign_member_lead_lookup_contract(campaign_member_fields, lead, lead_fields, contact_fields, list_fields_payload, errors): validate_reference_field(campaign_member_fields, "CampaignMember", "LeadId", "Lead", errors) validate_reference_field(campaign_member_fields, "CampaignMember", "ContactId", "Contact", errors) if lead.get("searchable") is False: errors.append("Lead explicitly disables search in the final object spec") lead_name = lead_fields.get("Name") if not lead_name: errors.append("Lead is missing Name in final object spec") else: if lead_name.get("isNameField") is not True: errors.append("Lead.Name is not marked as the name field") if lead_name.get("createable") is not False or lead_name.get("updateable") is not False: errors.append("Lead.Name must be read-only for compound-name synthesis") for field_name in ["Salutation", "FirstName", "LastName"]: field = lead_fields.get(field_name) if not field: errors.append(f"Lead is missing compound name member {field_name}") elif field.get("compoundFieldName") != "Name": errors.append(f"Lead.{field_name} is not marked as a Name compound member") contact_name = contact_fields.get("Name") if not contact_name: errors.append("Contact is missing Name in final object spec") else: if contact_name.get("isNameField") is not True: errors.append("Contact.Name is not marked as the name field") if contact_name.get("createable") is not False or contact_name.get("updateable") is not False: errors.append("Contact.Name must be read-only for compound-name synthesis") for field_name in ["Salutation", "FirstName", "LastName"]: field = contact_fields.get(field_name) if not field: errors.append(f"Contact is missing compound name member {field_name}") elif field.get("compoundFieldName") != "Name": errors.append(f"Contact.{field_name} is not marked as a Name compound member") # Contact.Name must be the SOLE name field. A duplicate isNameField on a # compound member (e.g. LastName) makes describe's DisplayNameField and the # frontend getRecordName resolve to that member alone (the first isNameField # in field order wins), so the UI shows only the Last Name instead of the # full First + Last. Guard against reintroducing a second name field. contact_name_fields = sorted( name for name, field in contact_fields.items() if field.get("isNameField") is True ) if contact_name_fields != ["Name"]: errors.append( "Contact must have exactly one name field (Name); found " f"{contact_name_fields}" ) if "Email" not in lead_fields: errors.append("Lead is missing Email in final object spec") elif lead_fields["Email"].get("type") != "email": errors.append("Lead.Email is not an email field") if "Email" not in contact_fields: errors.append("Contact is missing Email in final object spec") elif contact_fields["Email"].get("type") != "email": errors.append("Contact.Email is not an email field") lead_list_fields = list_fields_payload.get("Lead", []) for field_name in ["Name", "Company", "Email"]: if field_name not in lead_list_fields: errors.append(f"Lead list fields are missing {field_name}") contact_list_fields = list_fields_payload.get("Contact", []) for field_name in ["Name", "Email"]: if field_name not in contact_list_fields: errors.append(f"Contact list fields are missing {field_name}") def expected_pingcap_search_all_default_fields(): return { "Account": [ "Id", "Name", "AccountNumber", "Account_ID__c", "Website", "Phone", "billing_email__c", "Portal_ID__c", "HubSpot_Id__c", ], "Campaign": [ "Id", "Name", ], "Invoice_Collection__c": [ "Id", "Name", "Email__c", ], "Contact": [ "Id", "Name", "FirstName", "LastName", "Email", "Secondary_Email__c", "Other_Emails__c", "Phone", "MobilePhone", "Title", "Department", "Portal_ID__c", "HubSpot_Id__c", ], "Lead": [ "Id", "Name", "FirstName", "LastName", "Email", ], "NetSuite_Revenue_Voucher_Line__c": [ "Id", "Name", "NetSuite_Source_Key__c", "Document_Number__c", "Transaction_Number__c", "Source_Customer_Account_Id__c", "Company__c", "Entity_Name__c", "GL_Account_Number__c", "GL_Account_Name__c", "Posting_Period__c", "Order_Number__c", "Product_Name__c", "Project__c", ], "Order": [ "Id", "Name", "OrderNumber", "AutoNumberText__c", "Email__c", "Stripe_Bill_ID__c", "Account_Name_for_email_template__c", ], "Org__c": [ "Id", "Name", "ID__c", "Billing_Email__c", ], "Opportunity": [ "Id", "Name", "Opportunity_Number__c", "Opportunity_Registration_Number__c", "Project_Name__c", "AWS_Project_Title__c", "Company_Name_for_AWS__c", "Customer_Email_for_AWS__c", "Primary_Contact_Email_for_AWS__c", "Customer_Phone_for_AWS__c", "Primary_Contact_Phone_for_AWS__c", "HubSpot_Id__c", ], "Quote": [ "Id", "Name", "QuoteNumber", "Email", "Phone", "AutoNumberText__c", "Quote_No__c", "Product_Name__c", "Target_Product_Name__c", "Version_of_TiDB__c", ], "Contract": [ "Id", "Name", "ContractNumber", "AutoNumberText__c", "Contract_Number__c", "Email__c", "IPAddress__c", ], } def validate_pingcap_search_all_default_fields(pack_payload, spec_payload, errors): component = component_by_kind_and_key( pack_payload, "search_all_default_fields", "pingcap_search_all_default_fields", ) if component is None: errors.append("final pack is missing pingcap_search_all_default_fields component") return if component.get("manageability") != "locked": errors.append("pingcap_search_all_default_fields must be locked") if component.get("path") != "ux/search_all_default_fields.json": errors.append("pingcap_search_all_default_fields path is not canonical") return payload_path = FINAL_PACK / component.get("path") if not payload_path.is_file(): errors.append("final pack is missing pingcap_search_all_default_fields payload") return payload = load_json(payload_path) declarations = { item.get("objectApiName"): item.get("fields", []) for item in payload if item.get("objectApiName") } expected = expected_pingcap_search_all_default_fields() if declarations != expected: errors.append("pingcap_search_all_default_fields payload does not match the expected bounded defaults") object_specs = { item.get("apiName"): item for item in spec_payload if item.get("apiName") } for object_name, field_names in expected.items(): obj = object_specs.get(object_name) if obj is None: errors.append(f"search_all_default_fields declares missing object {object_name}") continue fields = field_map(obj) for field_name in field_names: if field_name == "Id": continue field = fields.get(field_name) if field is None: errors.append(f"search_all_default_fields {object_name} references missing field {field_name}") continue field_type = str(field.get("type", "")).lower() if field_type in {"formula", "rollupsummary"}: errors.append(f"search_all_default_fields {object_name}.{field_name} must not be formula/rollup") if field_type not in {"autonumber", "email", "phone", "string", "text", "textarea", "url"}: errors.append(f"search_all_default_fields {object_name}.{field_name} has non-search text type {field.get('type')}") def main(): stack_lock_payload = load_json(ROOT / "stack.lock.json") l2_spec_payload = load_l2_object_specs() spec_payload = load_json(FINAL_PACK / "objects" / "standard_objects_spec.json") actions_payload = load_json(FINAL_PACK / "actions" / "ui_actions.json") invocable_actions_payload = load_json(FINAL_PACK / "actions" / "invocable_standard_actions.json") field_mappings_payload = load_json(FINAL_PACK / "field_mappings" / "standard_field_mappings.json") validation_rules_payload = load_json(FINAL_PACK / "validation" / "standard_validation_rules.json") related_list_rules_payload = load_json(FINAL_PACK / "related_lists" / "standard_related_list_rules.json") record_types_payload = load_json(FINAL_PACK / "record_types" / "record_types.json") autonumber_formats_payload = load_json(FINAL_PACK / "autonumber" / "formats.json") autonumber_counters_payload = load_json(FINAL_PACK / "autonumber" / "counters.json") list_fields_payload = load_json(FINAL_PACK / "ux" / "standard_listfields.json") pack_payload = load_json(FINAL_PACK / "pack.json") lineage_payload = load_json(FINAL_PACK / ".build-lineage.json") l2_pack_payload = load_json(L2_PACK / "pack.json") pack_components = pack_payload.get("components", []) final_layout_component_keys = layout_component_keys(pack_payload) l2_layout_component_keys = layout_component_keys(l2_pack_payload) campaign_layout = load_json(FINAL_PACK / "layouts" / "campaign_page_layout.json") if "Campaign/default" in final_layout_component_keys else None campaign_member_status_layout = load_json(FINAL_PACK / "layouts" / "campaignmemberstatus_default_layout.json") if "CampaignMemberStatus/default" in final_layout_component_keys else None case_layout = load_json(FINAL_PACK / "layouts" / "case_page_layout.json") if "Case/default" in final_layout_component_keys else None contact_layout = load_json(FINAL_PACK / "layouts" / "contact_page_layout.json") if "Contact/default" in final_layout_component_keys else None lead_layout = load_json(FINAL_PACK / "layouts" / "lead_page_layout.json") if "Lead/default" in final_layout_component_keys else None opportunity_layout = load_json(FINAL_PACK / "layouts" / "opportunity_page_layout.json") if "Opportunity/default" in final_layout_component_keys else None quote_layout = load_json(FINAL_PACK / "layouts" / "quote_page_layout.json") if "Quote/default" in final_layout_component_keys else None campaign = object_by_api_name(spec_payload, "Campaign") case = object_by_api_name(spec_payload, "Case") campaign_member = object_by_api_name(spec_payload, "CampaignMember") campaign_member_status = object_by_api_name(spec_payload, "CampaignMemberStatus") contact = object_by_api_name(spec_payload, "Contact") contract = object_by_api_name(spec_payload, "Contract") lead = object_by_api_name(spec_payload, "Lead") opportunity = object_by_api_name(spec_payload, "Opportunity") order = object_by_api_name(spec_payload, "Order") quote = object_by_api_name(spec_payload, "Quote") revenue = object_by_api_name(spec_payload, "Revenue__c") invoice_collection = object_by_api_name(spec_payload, "Invoice_Collection__c") task = object_by_api_name(spec_payload, "Task") campaign_fields = field_map(campaign) campaign_member_fields = field_map(campaign_member) campaign_member_status_fields = field_map(campaign_member_status) case_fields = field_map(case) contact_fields = field_map(contact) contract_fields = field_map(contract) lead_fields = field_map(lead) opportunity_fields = field_map(opportunity) order_fields = field_map(order) quote_fields = field_map(quote) revenue_fields = field_map(revenue) invoice_collection_fields = field_map(invoice_collection) task_fields = field_map(task) record_types = record_type_map(record_types_payload) record_type_codes = record_type_code_map(record_types_payload) autonumber_formats = autonumber_seed_map(autonumber_formats_payload) autonumber_counters = autonumber_seed_map(autonumber_counters_payload) errors = [] upstream_record_type_identities = ( lineage_payload.get("assembly", {}).get("recordTypeUpstreamIdentities") ) if not isinstance(upstream_record_type_identities, list) or not upstream_record_type_identities: errors.append("final pack lineage is missing the upstream record-type identity sentinel") else: final_record_type_identities = set(record_type_identities(record_types_payload, "final")) missing_upstream_record_types = sorted( set(upstream_record_type_identities) - final_record_type_identities ) if missing_upstream_record_types: errors.append( "final record-type payload dropped upstream identities: " + ", ".join(missing_upstream_record_types) ) validate_quote_record_type_contract( record_types, upstream_record_type_identities, errors, ) pingcap_search_scope_order = component_by_kind_and_key( pack_payload, "search_scope_order", "pingcap_search_scope_order", ) if pingcap_search_scope_order is None: errors.append("pack.json is missing the PingCAP search_scope_order component") elif pingcap_search_scope_order.get("path") != "ux/pingcap_search_scope_order.json": errors.append("PingCAP search_scope_order component has an unexpected payload path") data_trans_required_fields = [ (revenue_fields, "Revenue__c", "Estimated_Revenue_Amount__c", "currency"), (revenue_fields, "Revenue__c", "CurrencyIsoCode", "picklist"), (invoice_collection_fields, "Invoice_Collection__c", "CurrencyIsoCode", "picklist"), ] for fields, object_name, field_name, expected_type in data_trans_required_fields: field = fields.get(field_name) if field is None: errors.append(f"{object_name}.{field_name} is required for data-trans Salesforce payload compatibility") elif field.get("type") != expected_type: errors.append(f"{object_name}.{field_name} must be {expected_type}, got {field.get('type')}") duplicate_prefixes = duplicate_key_prefixes(spec_payload) for key_prefix, object_names in sorted(duplicate_prefixes.items()): errors.append( f"object spec has duplicate keyPrefix {key_prefix}: {', '.join(sorted(set(object_names)))}" ) validate_active_formula_retention(l2_spec_payload, spec_payload, errors) validate_activity_timeline_header_fallback_policy(invocable_actions_payload, errors) validate_lead_change_owner_standard_action(actions_payload, invocable_actions_payload, errors) convert_lead_action = invocable_action_by_name(invocable_actions_payload, "convertLead") convert_lead_prefill_sources = { "contactFirstName": "FirstName", "contactLastName": "LastName", "contactTitle": "Title", "contactEmail": "Email", "opportunityName": "Company", } for input_name, source_field in convert_lead_prefill_sources.items(): action_input = invocable_action_input_by_name(convert_lead_action, input_name) if action_input is None: errors.append(f"convertLead is missing input {input_name}") elif action_input.get("prefillSourceFieldAPIName") != source_field: errors.append( f"convertLead input {input_name} must prefill from Lead.{source_field}" ) for object_api_name, obj_payload in { "CampaignMember": campaign_member, "CampaignMemberStatus": campaign_member_status, }.items(): relationship = managed_relationship_map(obj_payload).get(("CampaignId", "Campaign")) if relationship is None: errors.append(f"{object_api_name}.CampaignId is missing managed relationship to Campaign") continue if relationship.get("isSharingParent") is not True: errors.append(f"{object_api_name}.CampaignId managed relationship is not the sharing parent") if relationship.get("cascadeDelete") is not True: errors.append(f"{object_api_name}.CampaignId must declare cascadeDelete=true") for object_api_name, fields in { "Contact": contact_fields, "Lead": lead_fields, }.items(): for field_name in ["First_Campaign__c", "Last_Campaign__c"]: field = fields.get(field_name) if field is None: errors.append(f"{object_api_name}.{field_name} is missing from the final object spec") elif field.get("deleteConstraint") != "SetNull": errors.append(f"{object_api_name}.{field_name} must declare deleteConstraint=SetNull") validate_campaign_member_active_person_fields(campaign_member_fields, errors) validate_campaign_member_query_indices(campaign_member, errors) validate_campaign_member_lead_conversion_relationships( campaign_member_fields, errors ) validate_campaign_member_lead_lookup_contract( campaign_member_fields, lead, lead_fields, contact_fields, list_fields_payload, errors, ) validate_pingcap_search_all_default_fields(pack_payload, spec_payload, errors) validate_layout_visible_official_read_only_fields(spec_payload, errors) for layout_key in sorted(expected_l2_layout_component_keys() - l2_layout_component_keys): errors.append(f"L2 layout file exists but pack.json is missing page_layout component: {layout_key}") for layout_key in sorted(expected_final_l2_layout_component_keys(l2_pack_payload) - final_layout_component_keys): errors.append(f"final pack is missing expected L2 page_layout component: {layout_key}") for layout_key in ("CampaignMember/default", "Order/default", "Quote/default"): if layout_key not in final_layout_component_keys: errors.append( f"final pack is missing {layout_key} page layout required by Catalog App metadata V3" ) final_layout_component_paths = layout_component_paths(pack_payload) for layout_path in sorted((FINAL_PACK / "layouts").glob("*.json")): relative_path = str(layout_path.relative_to(FINAL_PACK)) layout_payload = load_json(layout_path) object_spec = next((item for item in spec_payload if item.get("apiName") == layout_payload.get("objectApiName")), {}) validate_layout_required_create_inputs(object_spec, layout_payload, errors) if relative_path not in final_layout_component_paths: errors.append(f"final pack layout payload is not referenced by pack.json: {relative_path}") record_type_code_components = [ component for component in pack_components if component.get("kind") == "record_type_code" ] if record_type_code_components: errors.append(f"pack.json should not declare record_type_code components, found {len(record_type_code_components)}") expected_flows = stack_lock_payload.get("pack_build", {}).get("flow_whitelist", []) flow_components = [ component for component in pack_components if component.get("kind") == "standard_flow" ] flow_component_keys = [component.get("businessKey") for component in flow_components] flow_component_key_set = set(flow_component_keys) duplicate_flow_keys = sorted({ flow_key for flow_key in flow_component_keys if flow_key and flow_component_keys.count(flow_key) > 1 }) if duplicate_flow_keys: errors.append(f"pack.json has duplicate standard_flow components: {', '.join(duplicate_flow_keys)}") for flow_name in expected_flows: if flow_name not in flow_component_key_set: errors.append(f"pack.json is missing expected standard_flow: {flow_name}") continue flow_component = next(component for component in flow_components if component.get("businessKey") == flow_name) flow_path = flow_component.get("path") or f"flows/{flow_name}.flow.json" if not (FINAL_PACK / flow_path).is_file(): errors.append(f"expected standard_flow file is missing: {flow_path}") attribution_flow_names = [ "Lead_Set_First_And_Last_CampaignMember", "Lead_Set_First_And_Last_CampaignMember_delete", "Contact_Set_First_And_Last_CampaignMember", "Contact_Set_First_And_Last_CampaignMember_delete", ] expected_attribution_lookups = { "Get_first_CampaignMember", "Get_last_CampaignMember", } for flow_name in attribution_flow_names: flow_component = next( ( component for component in flow_components if component.get("businessKey") == flow_name ), None, ) if flow_component is None: errors.append(f"campaign attribution flow is not registered: {flow_name}") continue flow_payload = load_json(FINAL_PACK / flow_component.get("path")) if flow_payload.get("status") != "Active": errors.append(f"campaign attribution flow is not active: {flow_name}") campaign_member_lookups = { lookup.get("name"): lookup for lookup in flow_payload.get("recordLookups", []) if lookup.get("object") == "CampaignMember" } if set(campaign_member_lookups) != expected_attribution_lookups: errors.append( f"{flow_name} must contain exactly the first/last CampaignMember lookups" ) continue for lookup_name in sorted(expected_attribution_lookups): if campaign_member_lookups[lookup_name].get("queriedFields") != [ "Id", "CampaignId", ]: errors.append( f"{flow_name}.{lookup_name} must query only Id and CampaignId" ) send_record_email_action = next( (action for action in invocable_actions_payload if action.get("name") == "sendRecordEmail"), None, ) if send_record_email_action is None: errors.append("invocable standard actions are missing sendRecordEmail") else: if send_record_email_action.get("flowDeveloperName") != "Std_SendRecordEmail": errors.append("sendRecordEmail must reference Std_SendRecordEmail") if send_record_email_action.get("label") != "Email": errors.append("sendRecordEmail label must be Email") if send_record_email_action.get("category") != "ActivityTimeline": errors.append("sendRecordEmail category must be ActivityTimeline") if send_record_email_action.get("rendererId") != "app-pack:crm/action-renderer/email-compose": errors.append("sendRecordEmail must use the email-compose renderer") if set(send_record_email_action.get("targetObjects", [])) != {"Contact", "Lead"}: errors.append("sendRecordEmail must target exactly Contact and Lead") if "Std_SendRecordEmail" not in flow_component_key_set: errors.append("pack.json is missing the Std_SendRecordEmail standard_flow") create_order_flow_name = "Std_CreateOrderFromQuote" if create_order_flow_name in flow_component_key_set: flow_component = next( component for component in flow_components if component.get("businessKey") == create_order_flow_name ) flow_payload = load_json(FINAL_PACK / flow_component.get("path")) if flow_payload.get("status") != "Active": errors.append("Create Order from Quote flow is not active") record_creates = {create.get("name"): create for create in flow_payload.get("recordCreates", [])} order_create = record_creates.get("CreateOrder") or {} order_item_create = record_creates.get("CreateOrderItem") or {} if (order_create.get("fieldMapping") or {}).get("actionType") != "createOrder": errors.append("Create Order flow does not use createOrder mapping for Quote -> Order") if (order_item_create.get("fieldMapping") or {}).get("actionType") != "createOrder": errors.append("Create Order flow does not use createOrder mapping for QuoteLineItem -> OrderItem") else: errors.append("Create Order from Quote flow is not registered in final pack") try: create_order_action = invocable_action_by_name(invocable_actions_payload, "createOrder") except KeyError: errors.append("invocable standard actions are missing createOrder") else: if create_order_action.get("flowDeveloperName") != create_order_flow_name: errors.append("createOrder standard action does not point at Std_CreateOrderFromQuote") if create_order_action.get("targetObjects") != ["Quote"]: errors.append("createOrder standard action targetObjects must be Quote only") outputs = create_order_action.get("outputs") or [] if not any(item.get("apiName") == "orderId" and item.get("referenceTo") == "Order" for item in outputs): errors.append("createOrder standard action does not expose orderId output") legacy_create_order_action = action_for_object(actions_payload, "Quote", "CreateOrder") if legacy_create_order_action and legacy_create_order_action.get("type") == "NavigateToNewRecord": errors.append("Quote still exposes legacy CreateOrder NavigateToNewRecord UI action") quote_to_order_mapping = field_mapping_by_source_target_action( field_mappings_payload, "Quote", "Order", "createOrder", ) if quote_to_order_mapping is None: errors.append("standard field mappings are missing Quote -> Order createOrder mapping") else: quote_to_order_pairs = { (item.get("sourceField"), item.get("targetField")) for item in quote_to_order_mapping.get("mappings", []) } for pair in [ ("AccountId", "AccountId"), ("ContractId", "ContractId"), ("OpportunityId", "OpportunityId"), ("Pricebook2Id", "Pricebook2Id"), ("CurrencyIsoCode", "CurrencyIsoCode"), ("Payment_Term__c", "Payment_Term__c"), ("Payment_Type__c", "Payment_Type__c"), ]: if pair not in quote_to_order_pairs: errors.append(f"Quote -> Order createOrder mapping is missing {pair[0]} -> {pair[1]}") if ("GrandTotal", "TotalAmount") in quote_to_order_pairs: errors.append("Quote -> Order createOrder must not stamp TotalAmount from Quote.GrandTotal") expected_orderitem_rollups = { "Count_Product_Lines__c": { "summaryObject": "OrderItem", "summaryField": "Id", "relationshipField": "OrderId", "calcType": "COUNT", }, "TotalAmount": { "summaryObject": "OrderItem", "summaryField": "TotalPrice", "relationshipField": "OrderId", "calcType": "SUM", }, "Net_Fees__c": { "summaryObject": "OrderItem", "summaryField": "Net_fees__c", "relationshipField": "OrderId", "calcType": "SUM", }, "Overall_Start_Date__c": { "summaryObject": "OrderItem", "summaryField": "ServiceDate", "relationshipField": "OrderId", "calcType": "MIN", }, "Overall_End_Date__c": { "summaryObject": "OrderItem", "summaryField": "EndDate", "relationshipField": "OrderId", "calcType": "MAX", }, } for field_name, expected_rollup in expected_orderitem_rollups.items(): field = order_fields.get(field_name) if field is None: errors.append(f"Order.{field_name} is missing from the final pack") elif field.get("type") != "rollupsummary": errors.append(f"Order.{field_name} must be a rollupsummary sourced from OrderItem") elif field.get("rollupConfig") != expected_rollup: errors.append(f"Order.{field_name} must use the canonical OrderItem rollup configuration") qli_to_order_item_mapping = field_mapping_by_source_target_action( field_mappings_payload, "QuoteLineItem", "OrderItem", "createOrder", ) if qli_to_order_item_mapping is None: errors.append("standard field mappings are missing QuoteLineItem -> OrderItem createOrder mapping") else: qli_to_order_item_pairs = { (item.get("sourceField"), item.get("targetField")) for item in qli_to_order_item_mapping.get("mappings", []) } for pair in [ ("Id", "QuoteLineItemId"), ("PricebookEntryId", "PricebookEntryId"), ("Product2Id", "Product2Id"), ("Quantity", "Quantity"), ("UnitPrice", "UnitPrice"), ("ListPrice", "ListPrice"), ("TotalPrice", "TotalPrice"), # Service window sources are the convention-populated custom fields # (QuoteLineItem.ServiceDate is unused in practice; the P1 Order date # rollups read OrderItem.ServiceDate/EndDate, so the start date must # flow from Service_Start_Date__c). Changed with the P1 PR-A remap. ("Service_Start_Date__c", "ServiceDate"), ("Service_End_Date__c", "EndDate"), ("Net_fees__c", "Net_fees__c"), ("CurrencyIsoCode", "CurrencyIsoCode"), ]: if pair not in qli_to_order_item_pairs: errors.append(f"QuoteLineItem -> OrderItem createOrder mapping is missing {pair[0]} -> {pair[1]}") quote_create_flow_name = "Std_CreateQuoteFromOpportunity" if quote_create_flow_name in flow_component_key_set: flow_component = next( component for component in flow_components if component.get("businessKey") == quote_create_flow_name ) flow_payload = load_json(FINAL_PACK / flow_component.get("path")) if flow_payload.get("status") != "Active": errors.append("Create Quote from Opportunity flow is not active") for object_api_name, required_names in { "Opportunity": {"New", "Expansion", "Churn", "Downscale", "Contract Only (OP Renew or Cloud CP Only)"}, "Quote": {"New", "Expansion", "Renewal"}, }.items(): missing = sorted(required_names - set(record_types.get(object_api_name, []))) if missing: errors.append(f"{object_api_name} record types missing inheritance target/source names: {', '.join(missing)}") lookups = {lookup.get("name"): lookup for lookup in flow_payload.get("recordLookups", [])} source_rt_lookup = lookups.get("GetSourceOpportunityRecordType") or {} source_rt_filters = source_rt_lookup.get("filters", []) if source_rt_lookup.get("object") != "_System_RecordType" or not any( item.get("field") == "Id" and (item.get("value") or {}).get("elementReference") == "$opportunity.RecordTypeId" for item in source_rt_filters ): errors.append("Create Quote flow does not look up the source Opportunity RecordType") target_rt_lookup = lookups.get("GetTargetQuoteRecordType") or {} target_rt_filters = target_rt_lookup.get("filters", []) if target_rt_lookup.get("object") != "_System_RecordType" or not all([ any(item.get("field") == "ObjectId" and (item.get("value") or {}).get("elementReference") == "$quoteObjectMetadata.Id" for item in target_rt_filters), any(item.get("field") == "Name" and (item.get("value") or {}).get("elementReference") == "$mappedQuoteRTName" for item in target_rt_filters), any(item.get("field") == "IsActive" and item.get("value") is True for item in target_rt_filters), ]): errors.append("Create Quote flow does not resolve an active target Quote RecordType by mapped name") assignments_by_name = {assignment.get("name"): assignment for assignment in flow_payload.get("assignments", [])} renewal_assignment_items = (assignments_by_name.get("SetMappedRTNameRenewal") or {}).get("assignmentItems", []) if not any( item.get("assignToReference") == "$mappedQuoteRTName" and (item.get("value") or {}).get("stringValue") == "Renewal" for item in renewal_assignment_items ): errors.append("Create Quote flow does not map renewal Opportunity RecordTypes to Quote Renewal") direct_assignment_items = (assignments_by_name.get("SetMappedRTNameDirect") or {}).get("assignmentItems", []) if not any( item.get("assignToReference") == "$mappedQuoteRTName" and (item.get("value") or {}).get("elementReference") == "$sourceOpportunityRecordType.Name" for item in direct_assignment_items ): errors.append("Create Quote flow does not map direct Opportunity RecordTypes by source name") map_decision = next( (decision for decision in flow_payload.get("decisions", []) if decision.get("name") == "MapOppRTToQuoteRT"), {}, ) renewal_rule = next( (rule for rule in map_decision.get("rules", []) if rule.get("name") == "OppRTIsRenewalType"), {}, ) renewal_sources = { (condition.get("rightValue") or {}).get("stringValue") for condition in renewal_rule.get("conditions", []) if condition.get("leftValueReference") == "$sourceOpportunityRecordType.Name" and condition.get("operator") == "EqualTo" } expected_renewal_sources = {"Churn", "Downscale", "Contract Only (OP Renew or Cloud CP Only)"} if not expected_renewal_sources.issubset(renewal_sources): errors.append("Create Quote flow does not map Churn/Downscale/Contract Only Opportunity RecordTypes to Renewal") if (renewal_rule.get("connector") or {}).get("targetReference") != "SetMappedRTNameRenewal": errors.append("Create Quote flow renewal RecordType decision does not target SetMappedRTNameRenewal") if (map_decision.get("defaultConnector") or {}).get("targetReference") != "SetMappedRTNameDirect": errors.append("Create Quote flow default RecordType decision does not preserve the source RecordType name") create_quote = next( (create for create in flow_payload.get("recordCreates", []) if create.get("name") == "CreateQuote"), {}, ) record_type_assignment = next( (item for item in create_quote.get("inputAssignments", []) if item.get("field") == "RecordTypeId"), {}, ) if (record_type_assignment.get("value") or {}).get("elementReference") != "$quoteRecordType.Id": errors.append("Create Quote flow does not assign Quote.RecordTypeId from the mapped Quote RecordType") contract_archived_flow_name = "Contract_Archived_Status_Autopromote" if contract_archived_flow_name in flow_component_key_set: flow_component = next( component for component in flow_components if component.get("businessKey") == contract_archived_flow_name ) flow_payload = load_json(FINAL_PACK / flow_component.get("path")) flow_start = flow_payload.get("start") or {} if flow_payload.get("status") != "Active": errors.append("Contract archived status flow is not active") if flow_start.get("object") != "Contract": errors.append("Contract archived status flow is not triggered on Contract") if flow_start.get("triggerType") != "RecordBeforeSave": errors.append("Contract archived status flow is not before-save") if flow_start.get("recordTriggerType") != "CreateAndUpdate": errors.append("Contract archived status flow is not CreateAndUpdate") decision_conditions = [] for decision in flow_payload.get("decisions", []): for rule in decision.get("rules", []): decision_conditions.extend(rule.get("conditions", [])) expected_conditions = { ("$Record.Status", "EqualTo", "Approved"), ("$Record.Archived__c", "EqualTo", True), } actual_conditions = { ( condition.get("leftValueReference"), condition.get("operator"), (condition.get("rightValue") or {}).get("stringValue") if "stringValue" in (condition.get("rightValue") or {}) else (condition.get("rightValue") or {}).get("booleanValue"), ) for condition in decision_conditions } if not expected_conditions.issubset(actual_conditions): errors.append("Contract archived status flow does not gate on Approved + Archived__c=true") status_assignments = [ item for assignment in flow_payload.get("assignments", []) for item in assignment.get("assignmentItems", []) if item.get("assignToReference") == "$Record.Status" ] if not any((item.get("value") or {}).get("stringValue") == "Effective" for item in status_assignments): errors.append("Contract archived status flow does not assign Status=Effective") if "CurrencyIsoCode" not in campaign_fields: errors.append("Campaign is missing CurrencyIsoCode in final object spec") elif campaign_fields["CurrencyIsoCode"].get("label") != "Campaign Currency": errors.append("Campaign.CurrencyIsoCode label is not 'Campaign Currency'") # OwnerId is an L0-injected system column in current runtime installs. # The final pack may reference it in layouts/actions, but must not redeclare # it in the L1 object spec payload. if "OwnerId" in campaign_fields and campaign_fields["OwnerId"].get("label") != "Campaign Owner": errors.append("Campaign.OwnerId label is not 'Campaign Owner' when present") if campaign_layout is not None: campaign_related_lists = related_list_map(campaign_layout) if not campaign_related_lists: errors.append("Campaign layout has no related lists") elif "CampaignMember-CampaignId" not in campaign_related_lists: errors.append("Campaign layout is missing CampaignMember-CampaignId related list") elif campaign_related_lists["CampaignMember-CampaignId"].get("label") != "Campaign Members": errors.append("Campaign related list label is not 'Campaign Members'") validate_related_list_metadata(campaign_layout, "Campaign", errors) campaign_actions = action_names_for_object(actions_payload, "Campaign") if "Clone" not in campaign_actions: errors.append("Campaign actions do not include Clone") if "CurrencyIsoCode" not in lead_fields: errors.append("Lead is missing CurrencyIsoCode in final object spec") elif lead_fields["CurrencyIsoCode"].get("label") != "Lead Currency": errors.append("Lead.CurrencyIsoCode label is not 'Lead Currency'") if "Date_Status_Changed__c" not in lead_fields: errors.append("Lead is missing Date_Status_Changed__c in final object spec") for field_name in ["Name", "Salutation", "FirstName", "LastName", "MiddleName", "Suffix"]: if field_name not in lead_fields: errors.append(f"Lead is missing {field_name} in final object spec") if "Salutation" in lead_fields: salutation = lead_fields["Salutation"] if salutation.get("type") != "picklist": errors.append("Lead.Salutation is not a picklist in final object spec") elif [item.get("value") for item in salutation.get("options", [])] != [ "Mr.", "Ms.", "Mrs.", "Dr.", "Prof.", ]: errors.append("Lead.Salutation picklist does not match the SFDC values in order") if lead_layout is not None: lead_section_fields = section_field_set(lead_layout) for field_name in ["CurrencyIsoCode", "Date_Status_Changed__c"]: if field_name not in lead_section_fields: errors.append(f"Lead layout is missing {field_name}") opportunity_actions = action_names_for_object(actions_payload, "Opportunity") if "AddOpportunityLineItem" not in opportunity_actions: errors.append("Opportunity actions do not include AddOpportunityLineItem") if "Clone" not in opportunity_actions: errors.append("Opportunity actions do not include Clone") if "ApplyForPoC" in opportunity_actions: errors.append("Opportunity actions still include legacy ApplyForPoC") if "PartnerAttribution" not in opportunity_actions: errors.append("Opportunity actions do not include PartnerAttribution") for field_name in ["Archived__c", "StandardUploadUrl__c"]: if field_name not in contract_fields: errors.append(f"Contract is missing {field_name} in final object spec") if field_name not in order_fields: errors.append(f"Order is missing {field_name} in final object spec") expected_contract_end_formula = "IF(AND(NOT(ISBLANK(StartDate)), NOT(ISBLANK(ContractTerm))), ADDMONTHS(StartDate, ROUND(ContractTerm, 0)) - 1, null)" contract_end_date = contract_fields.get("EndDate") if not contract_end_date: errors.append("Contract is missing EndDate in final object spec") else: if contract_end_date.get("formula") != expected_contract_end_formula: errors.append("Contract.EndDate formula is missing or does not match StartDate + ContractTerm") if contract_end_date.get("returnType") != "date": errors.append("Contract.EndDate returnType is not date") if contract_end_date.get("formulaMode") != "active": errors.append("Contract.EndDate formulaMode is not active") if contract_end_date.get("createable") is not False or contract_end_date.get("updateable") is not False: errors.append("Contract.EndDate must be read-only in final object spec") if "Status" not in contract_fields: errors.append("Contract is missing Status in final object spec") else: contract_status_values = [item.get("value") for item in contract_fields["Status"].get("options", [])] for status_value in ["Approved", "Effective"]: if status_value not in contract_status_values: errors.append(f"Contract.Status does not include {status_value}") validate_order_status_picklist_scope(order_fields, pack_payload, errors) contract_upload_action = action_for_object(actions_payload, "Contract", "UploadArchivedContract") if contract_upload_action is None: errors.append("Contract actions do not include UploadArchivedContract") else: if contract_upload_action.get("label") != "Upload Archived Contr.": errors.append("Contract UploadArchivedContract label is not 'Upload Archived Contr.'") if contract_upload_action.get("type") != "UploadFileUpdateRecord": errors.append("Contract UploadArchivedContract type is not UploadFileUpdateRecord") config = contract_upload_action.get("config") or {} if config.get("visibilityCondition") != "record.Status == 'Approved'": errors.append("Contract UploadArchivedContract visibility condition is not Approved-only") if config.get("attachmentKind") != "archived_contract": errors.append("Contract UploadArchivedContract attachmentKind is not archived_contract") field_updates = config.get("fieldUpdates") or {} validate_native_file_upload_field_updates( "Contract UploadArchivedContract", field_updates, "Effective", errors ) order_upload_action = action_for_object(actions_payload, "Order", "UploadArchivedOrder") if order_upload_action is None: errors.append("Order actions do not include UploadArchivedOrder") else: if order_upload_action.get("label") != "Upload Archived OF": errors.append("Order UploadArchivedOrder label is not 'Upload Archived OF'") if order_upload_action.get("type") != "UploadFileUpdateRecord": errors.append("Order UploadArchivedOrder type is not UploadFileUpdateRecord") config = order_upload_action.get("config") or {} if config.get("visibilityCondition") != "record.Status == 'Approved'": errors.append("Order UploadArchivedOrder visibility condition is not Approved-only") if config.get("attachmentKind") != "archived_order": errors.append("Order UploadArchivedOrder attachmentKind is not archived_order") field_updates = config.get("fieldUpdates") or {} validate_native_file_upload_field_updates( "Order UploadArchivedOrder", field_updates, "Activated", errors ) expected_activity_action_targets = { "taskCall": {"Case", "Contact", "Contract", "Lead", "Opportunity", "Order"}, "taskEmail": {"Case", "Contract", "Opportunity", "Order"}, } for activity_action_name, expected_targets in expected_activity_action_targets.items(): activity_action = invocable_action_by_name(invocable_actions_payload, activity_action_name) activity_targets = set(activity_action.get("targetObjects", [])) if activity_targets != expected_targets: errors.append( f"{activity_action_name} must target exactly {sorted(expected_targets)}" ) validate_non_weekly_update_activity_inputs(activity_action, errors) opportunity_weekly_update_action = invocable_action_by_name(invocable_actions_payload, "taskWeeklyUpdate") opportunity_weekly_update_targets = set(opportunity_weekly_update_action.get("targetObjects", [])) if "Opportunity" not in opportunity_weekly_update_targets: errors.append("taskWeeklyUpdate must target Opportunity") if opportunity_weekly_update_action.get("label") != "Weekly Update": errors.append("taskWeeklyUpdate label is not Weekly Update") if opportunity_weekly_update_action.get("category") != "ActivityTimeline": errors.append("taskWeeklyUpdate category is not ActivityTimeline") validate_weekly_update_required_inputs(opportunity_weekly_update_action, errors) lead_weekly_update_action = invocable_action_by_name(invocable_actions_payload, "taskLeadWeeklyUpdate") lead_weekly_update_targets = set(lead_weekly_update_action.get("targetObjects", [])) if "Lead" not in lead_weekly_update_targets: errors.append("taskLeadWeeklyUpdate must target Lead") if lead_weekly_update_action.get("label") != "Weekly Update": errors.append("taskLeadWeeklyUpdate label is not Weekly Update") if lead_weekly_update_action.get("category") != "ActivityTimeline": errors.append("taskLeadWeeklyUpdate category is not ActivityTimeline") validate_weekly_update_required_inputs(lead_weekly_update_action, errors) if "Lead/default" not in final_layout_component_keys: errors.append("final pack is missing Lead/default page layout component") elif lead_layout is None: errors.append("final pack is missing Lead layout payload") else: if lead_layout.get("objectApiName") != "Lead": errors.append("Lead layout objectApiName is not canonical") if "Activity" not in layout_tab_set(lead_layout): errors.append("Lead layout is missing the Activity tab") lead_header_action_names = [item.get("name") for item in (lead_layout.get("headerActions") or [])] if lead_header_action_names != ["convertLead"]: errors.append("Lead layout headerActions must contain only convertLead") lead_quick_action_names = [item.get("name") for item in (lead_layout.get("quickActions") or [])] if lead_quick_action_names != ["taskCall", "sendRecordEmail", "taskLeadWeeklyUpdate"]: errors.append( "Lead layout quickActions must expose taskCall, sendRecordEmail, " "taskLeadWeeklyUpdate in order" ) lead_activity_panel = layout_record_panel_by_renderer( lead_layout, "app-pack:crm/record-panel/activity-timeline", ) if lead_activity_panel is None: errors.append("Lead layout is missing the Activity Timeline record panel") else: if lead_activity_panel.get("tabId") != "activity": errors.append("Lead Activity Timeline record panel tabId is not activity") if lead_activity_panel.get("tabLabel") != "Activity": errors.append("Lead Activity Timeline record panel tabLabel is not Activity") expected_campaign_member_status_fields = ["Label", "IsDefault", "HasResponded", "SortOrder"] for field_name in ["CampaignId", *expected_campaign_member_status_fields]: if field_name not in campaign_member_status_fields: errors.append(f"CampaignMemberStatus is missing {field_name} in final object spec") campaign_member_status_sort_order = campaign_member_status_fields.get("SortOrder") if campaign_member_status_sort_order: if campaign_member_status_sort_order.get("type") != "integer": errors.append("CampaignMemberStatus.SortOrder must be integer-backed for sibling-max defaulting") if campaign_member_status_sort_order.get("createable") is not True: errors.append("CampaignMemberStatus.SortOrder must be createable for default child rows") if campaign_member_status_sort_order.get("updateable") is not True: errors.append("CampaignMemberStatus.SortOrder must be updateable for sibling-max defaulting") if campaign_member_status_sort_order.get("siblingMaxParentField") != "CampaignId": errors.append("CampaignMemberStatus.SortOrder must sibling-max default within CampaignId") if "CampaignMemberStatus/default" not in final_layout_component_keys: errors.append("final pack is missing CampaignMemberStatus/default page layout component") elif campaign_member_status_layout is None: errors.append("final pack is missing CampaignMemberStatus layout payload") else: if campaign_member_status_layout.get("objectApiName") != "CampaignMemberStatus": errors.append("CampaignMemberStatus layout objectApiName is not canonical") if campaign_member_status_layout.get("source") != "curated": errors.append("CampaignMemberStatus layout is not curated") layout_fields = section_field_set(campaign_member_status_layout) for field_name in expected_campaign_member_status_fields: if field_name not in layout_fields: errors.append(f"CampaignMemberStatus layout is missing {field_name}") if list_fields_payload.get("CampaignMemberStatus") != expected_campaign_member_status_fields: errors.append("CampaignMemberStatus list fields are not Label, IsDefault, HasResponded, SortOrder") campaign_status_rule = related_list_rule_by_key(related_list_rules_payload, "campaign-member-statuses") if not campaign_status_rule: errors.append("Campaign related list rules do not include CampaignMemberStatus") else: if campaign_status_rule.get("parentObjectApiName") != "Campaign": errors.append("CampaignMemberStatus related list rule parent is not Campaign") if campaign_status_rule.get("childObjectApiName") != "CampaignMemberStatus": errors.append("CampaignMemberStatus related list rule child is not CampaignMemberStatus") if campaign_status_rule.get("lookupField") != "CampaignId": errors.append("CampaignMemberStatus related list rule lookup is not CampaignId") if campaign_status_rule.get("fields") != expected_campaign_member_status_fields: errors.append("CampaignMemberStatus related list rule fields are not Label, IsDefault, HasResponded, SortOrder") if campaign_layout is not None: campaign_related_lists = related_list_map(campaign_layout) campaign_member_status_related_list = campaign_related_lists.get("CampaignMemberStatus-CampaignId") if ( not campaign_member_status_related_list or campaign_member_status_related_list.get("objectApiName") != "CampaignMemberStatus" or campaign_member_status_related_list.get("lookupField") != "CampaignId" ): errors.append("Campaign layout is missing CampaignMemberStatus related list") validate_related_list_metadata(campaign_layout, "Campaign", errors) opportunity_related_list_rules = related_list_rule_keys(related_list_rules_payload, "Opportunity") if "opportunity-partner-attributions" not in opportunity_related_list_rules: errors.append("Opportunity related list rules do not include partner attribution") if "opportunity-history" not in opportunity_related_list_rules: errors.append("Opportunity related list rules do not include field history") if opportunity_layout is not None: opportunity_related_lists = related_list_map(opportunity_layout) opportunity_history_related_list = opportunity_related_lists.get( "OpportunityFieldHistory-OpportunityId" ) if ( not opportunity_history_related_list or opportunity_history_related_list.get("objectApiName") != "OpportunityFieldHistory" or opportunity_history_related_list.get("lookupField") != "OpportunityId" ): errors.append("Opportunity layout is missing OpportunityFieldHistory related list") opportunity_tabs = layout_tab_set(opportunity_layout) for tab_label in [ "Why TiDB & Why Now", "Qualification & MEDDPICC", "Technical Information", "Activity", ]: if tab_label not in opportunity_tabs: errors.append(f"Opportunity layout is missing tab: {tab_label}") validate_related_list_metadata(opportunity_layout, "Opportunity", errors) if "CurrencyIsoCode" not in opportunity_fields: errors.append("Opportunity is missing CurrencyIsoCode in final object spec") elif opportunity_fields["CurrencyIsoCode"].get("label") != "Deal Currency": errors.append("Opportunity.CurrencyIsoCode label is not 'Deal Currency'") for field_name in ["ForecastCategory", "ForecastCategoryName", "Why_TiDB__c", "Why_Now__c", "Launch_Date__c"]: if field_name not in opportunity_fields: errors.append(f"Opportunity is missing {field_name} in final object spec") if not any(name.endswith("__c") for name in lead_fields): errors.append("Lead has no custom business fields in final object spec") if not any(name.endswith("__c") for name in opportunity_fields): errors.append("Opportunity has no custom business fields in final object spec") if "RecordTypeId" not in list_fields_payload.get("Opportunity", []): errors.append("Opportunity list fields are missing RecordTypeId") if quote_layout is not None: quote_section_fields = section_field_set(quote_layout) for field_name in [ "RecordTypeId", "How_will_the_customer_place_an_order__c", "Payment_Term__c", "Payment_Type__c", "Product_Family__c", "Cloud_Vendor__c", "Customer_s_Cluster_Configuration__c", "Customer_s_Consumption_in_Last_3_Months__c", "Minimum_Commitment_Amount__c", "TiDB_Cloud_Support_Tier__c", "Partner_Name__c", "Partner_Margin__c", "Rebate_Fee_Rate__c", ]: if field_name not in quote_section_fields: errors.append(f"Quote layout is missing {field_name}") quote_line_items = related_list_map(quote_layout).get("QuoteLineItem-QuoteId") if ( not quote_line_items or quote_line_items.get("objectApiName") != "QuoteLineItem" or quote_line_items.get("lookupField") != "QuoteId" ): errors.append("Quote layout is missing QuoteLineItem related list") if "RecordTypeId" not in quote_layout.get("compactLayout", []): errors.append("Quote compact layout is missing RecordTypeId") if "RecordTypeId" not in list_fields_payload.get("Quote", []): errors.append("Quote list fields are missing RecordTypeId") contract_name_field = contract_fields.get("Name") if not contract_name_field: errors.append("Contract is missing Name field") elif ( contract_name_field.get("type") != "string" or not contract_name_field.get("required") or not contract_name_field.get("isNameField") or not contract_name_field.get("isCompound") or contract_name_field.get("composedFrom") != ["ContractNumber"] ): errors.append("Contract.Name is not a required compound name composed from ContractNumber") order_name_field = order_fields.get("Name") if not order_name_field: errors.append("Order is missing Name field") elif ( order_name_field.get("type") != "string" or not order_name_field.get("isNameField") or not order_name_field.get("isCompound") or order_name_field.get("composedFrom") != ["OrderNumber"] ): errors.append("Order.Name is not a compound name composed from OrderNumber") order_number_alias_field = order_fields.get("Order_Number__c") if not order_number_alias_field: errors.append("Order is missing Order_Number__c") elif ( order_number_alias_field.get("type") != "formula" or order_number_alias_field.get("formula") != "OrderNumber" or order_number_alias_field.get("returnType") != "string" or order_number_alias_field.get("formulaMode") != "active" or order_number_alias_field.get("createable") is not False or order_number_alias_field.get("updateable") is not False ): errors.append("Order.Order_Number__c is not a read-only formula alias of OrderNumber") expected_governing_fields = { "Quote": ( quote_fields, { "Governing_Contract_Family__c": "IF(TEXT(Product_Family__c)=\"TiDB Cloud\", \"Cloud\", IF(TEXT(Product_Family__c)=\"TiDB Enterprise Subscription\", \"OP\", \"\"))", "Governing_Service_Start_Date__c": "Service_Start_Date__c", "Governing_Service_End_Date__c": "Service_End_Date__c", }, ), "Order": ( order_fields, { "Governing_Contract_Family__c": "IF(TEXT(Product_Family__c)=\"TiDB Cloud\", \"Cloud\", IF(TEXT(Product_Family__c)=\"TiDB Enterprise Subscription\", \"OP\", IF(TEXT(Quote.Product_Family__c)=\"TiDB Cloud\", \"Cloud\", IF(TEXT(Quote.Product_Family__c)=\"TiDB Enterprise Subscription\", \"OP\", \"\"))))", "Governing_Service_Start_Date__c": "IF(NOT(ISBLANK(EffectiveDate)), EffectiveDate, Quote.Service_Start_Date__c)", "Governing_Service_End_Date__c": "IF(NOT(ISBLANK(EndDate)), EndDate, IF(NOT(ISBLANK(Order_End_Date__c)), Order_End_Date__c, Quote.Service_End_Date__c))", }, ), } for object_name, (fields, expected_formulas) in expected_governing_fields.items(): for field_name, expected_formula in expected_formulas.items(): field = fields.get(field_name) if not field: errors.append(f"{object_name} is missing {field_name}") continue if ( field.get("type") != "formula" or field.get("formula") != expected_formula or field.get("formulaMode") != "frozen" or field.get("createable") is not False or field.get("updateable") is not False ): errors.append(f"{object_name}.{field_name} is not the expected frozen governing-contract formula") def assert_record_set_rule(rule_name, expected_condition, expected_bindings, active=True): rule = validation_rule_by_name(validation_rules_payload, "Order", rule_name) if rule is None: errors.append(f"Order validation rules do not include {rule_name}") return if rule.get("active") is not active: errors.append(f"{rule_name} active flag is not {active}") if rule.get("condition") != expected_condition: errors.append(f"{rule_name} condition is not the expected governing-contract formula") actual_bindings = rule.get("recordSetBindings") or [] if len(actual_bindings) != len(expected_bindings): errors.append(f"{rule_name} must declare {len(expected_bindings)} recordSetBinding(s)") return for binding, expected in zip(actual_bindings, expected_bindings): expected_name, expected_filters = expected if binding.get("name") != expected_name: errors.append(f"{rule_name} binding is not named {expected_name}") if binding.get("mode") != "exists": errors.append(f"{rule_name} binding mode is not exists") if binding.get("objectApiName") != "Contract": errors.append(f"{rule_name} binding does not query Contract") actual_filters = [ ( item.get("field"), item.get("operator"), item.get("valueSource"), item.get("value"), ) for item in binding.get("filters", []) ] if actual_filters != expected_filters: errors.append(f"{rule_name} binding filters do not match {expected_name}") cloud_type_filter = ( "RecordType.Name", "in", "literal", [ "TiDB Cloud Services Agreement", "TiDB Cloud Services Agreement Online", ], ) op_type_filter = ("RecordType.Name", "equals", "literal", "PingCAP Master Agreement") account_filter = ("AccountId", "equals", "recordPath", "AccountId") start_filter = ("StartDate", "lessOrEqual", "recordPath", "Governing_Service_Start_Date__c") end_filter = ("EndDate", "greaterOrEqual", "recordPath", "Governing_Service_End_Date__c") effective_filter = ("Status", "equals", "literal", "Effective") cloud_covering_filters = [account_filter, cloud_type_filter, start_filter, end_filter] cloud_effective_filters = [account_filter, cloud_type_filter, effective_filter, start_filter, end_filter] op_covering_filters = [account_filter, op_type_filter, start_filter, end_filter] op_effective_filters = [account_filter, op_type_filter, effective_filter, start_filter, end_filter] assert_record_set_rule( "Order_Create_Requires_Covering_Cloud_CSA", "ISNEW() && !ISBLANK(QuoteId) && Governing_Contract_Family__c == \"Cloud\" && !ISBLANK(Governing_Service_Start_Date__c) && !ISBLANK(Governing_Service_End_Date__c) && NOT(HasCoveringCloudCSAForOrder)", [("HasCoveringCloudCSAForOrder", cloud_covering_filters)], ) assert_record_set_rule( "Order_Create_Requires_Covering_OP_MA", "ISNEW() && !ISBLANK(QuoteId) && Governing_Contract_Family__c == \"OP\" && !ISBLANK(Governing_Service_Start_Date__c) && !ISBLANK(Governing_Service_End_Date__c) && NOT(HasCoveringOPMAForOrder)", [("HasCoveringOPMAForOrder", op_covering_filters)], ) assert_record_set_rule( "Order_Activate_Requires_Covering_Cloud_CSA", "ISCHANGED(Status) && Status == \"Activated\" && !ISBLANK(QuoteId) && Governing_Contract_Family__c == \"Cloud\" && !ISBLANK(Governing_Service_Start_Date__c) && !ISBLANK(Governing_Service_End_Date__c) && NOT(HasCoveringCloudCSAForOrder)", [("HasCoveringCloudCSAForOrder", cloud_covering_filters)], ) assert_record_set_rule( "Order_Activate_Requires_Effective_Cloud_CSA", "ISCHANGED(Status) && Status == \"Activated\" && !ISBLANK(QuoteId) && Governing_Contract_Family__c == \"Cloud\" && HasCoveringCloudCSAForOrder && NOT(HasEffectiveCoveringCloudCSAForOrder)", [ ("HasCoveringCloudCSAForOrder", cloud_covering_filters), ("HasEffectiveCoveringCloudCSAForOrder", cloud_effective_filters), ], ) assert_record_set_rule( "Order_Activate_Requires_Effective_OP_MA", "ISCHANGED(Status) && Status == \"Activated\" && Governing_Contract_Family__c == \"OP\" && HasCoveringOPMAForOrder && NOT(HasEffectiveCoveringOPMAForOrder)", [ ("HasCoveringOPMAForOrder", op_covering_filters), ("HasEffectiveCoveringOPMAForOrder", op_effective_filters), ], ) for legacy_rule_name in [ "Order_Activate_Contract_End", "Order_Activate_Contract_Start", "Order_Activate_Contract_Status", "Order_Activate_Requires_Contract", ]: legacy_rule = validation_rule_by_name(validation_rules_payload, "Order", legacy_rule_name) if legacy_rule is None: errors.append(f"Order validation rules do not include disabled legacy rule {legacy_rule_name}") elif legacy_rule.get("active") is not False: errors.append(f"{legacy_rule_name} must stay disabled because PingCAP validates Account-level governing Contracts") post_activate_rule = validation_rule_by_name(validation_rules_payload, "Order", "Order_Post_Activate_Readonly") expected_post_activate_condition = ( 'PRIORVALUE(Status) == "Activated" && $Profile.Name <> "System Administrator" && ' 'NOT(OR(ISCHANGED(Accumulated_Credit_Amount__c), ISCHANGED(Accumulated_Payable_Amount__c), ' 'ISCHANGED(Accumulated_Revenue_Before_Adjust__c), ISCHANGED(Cash_Collected_Amount_USD__c), ' 'ISCHANGED(Cash_Collected_Amount__c), ISCHANGED(Cash_Collected_USD_exc_tax__c), ' 'ISCHANGED(Cash_Collected_exc_tax_Amount__c), ISCHANGED(Latest_Payable_Date__c), ' 'ISCHANGED(Payable_Amount_USD_exc_tax__c), ISCHANGED(Recognized_Revenue_Amount_USD__c), ' 'ISCHANGED(Recognized_Revenue_Amount__c), ISCHANGED(TotalAmount), ' 'ISCHANGED(Count_Product_Lines__c), ISCHANGED(Net_Fees__c), ' 'ISCHANGED(Overall_Start_Date__c), ISCHANGED(Overall_End_Date__c)))' ) # The rule is wrapped in the derived-recompute marker (#4852): a write to any # child row makes the platform recompute this Order's rollups, and that derived # save is evaluated against these rules with no profile identity, so the # $Profile carve-out below cannot exempt anyone on that path. The wrapper keeps # the header lock off child-row writes entirely. The exclusion list is checked # unchanged inside the wrapper so it still cannot drift away from the set of # rollup-maintained fields. expected_post_activate_wrapped = ( "NOT($SystemContext.IsDerivedRecompute) && (\n" + "\n".join( " " + line if line.strip() else line for line in expected_post_activate_condition.split("\n") ) + "\n)" ) if post_activate_rule is None: errors.append("Order validation rules do not include Order_Post_Activate_Readonly") elif post_activate_rule.get("condition") != expected_post_activate_wrapped: condition = post_activate_rule.get("condition") or "" if "$SystemContext.IsDerivedRecompute" not in condition: errors.append("Order_Post_Activate_Readonly must keep the NOT($SystemContext.IsDerivedRecompute) wrapper so child-row writes are not blocked by the header lock") else: errors.append("Order_Post_Activate_Readonly must allow exactly the rollup-maintained fields (finance rollups + OrderItem product rollups) and nothing else") account_country_token_ref = lambda object_name, fk_chains: { "directField": "Account_Country_Code__c", "fkChains": fk_chains, "readStrategy": "caller_txn", "targetField": "Account_Country_Code__c", "targetObject": object_name, "tokenLiteral": f"{{{object_name}.Account_Country_Code__c}}", } direct_account_country_chain = [ [ { "fk": "AccountId", "targetField": "BillingCountryCode", "targetObject": "Account", } ] ] quote_country_chains = direct_account_country_chain + [ [ { "fk": "OpportunityId", "targetObject": "Opportunity", }, { "fk": "AccountId", "targetField": "BillingCountryCode", "targetObject": "Account", }, ], ] record_type_code_token_ref = { "fkChains": [ [ { "fk": "RecordTypeId", "targetField": "RecordTypeCode", "targetObject": "RecordType", } ] ], "readStrategy": "caller_txn", "targetField": "RecordTypeCode", "targetObject": "RecordType", "tokenLiteral": "{RecordType.RecordTypeCode}", } expected_autonumber_contracts = [ ( "Contract", "ContractNumber", "{Contract.Account_Country_Code__c}-{RecordType.RecordTypeCode}-{YY}{MM}{DD}{00}", [ account_country_token_ref("Contract", direct_account_country_chain), record_type_code_token_ref, ], ), ( "Order", "OrderNumber", "{Order.Account_Country_Code__c}-OF-{YY}{MM}{DD}{00}", [account_country_token_ref("Order", direct_account_country_chain)], ), ( "Quote", "QuoteNumber", "{Quote.Account_Country_Code__c}-QT-{YY}{MM}{DD}{00}", [account_country_token_ref("Quote", quote_country_chains)], ), ] for source_name, seeds in [ ("autonumber/formats.json", autonumber_formats), ("autonumber/counters.json", autonumber_counters), ]: for object_name, field_name, expected_format, expected_token_refs in expected_autonumber_contracts: seed = seeds.get((object_name, field_name)) seed_name = f"{object_name}.{field_name}" if not seed: errors.append(f"{source_name} is missing {seed_name}") continue if seed.get("format") != expected_format: errors.append( f"{source_name} {seed_name} format is not {expected_format}" ) if seed.get("scope") != "per_prefix": errors.append(f"{source_name} {seed_name} scope is not per_prefix") if seed.get("start") != 1: errors.append(f"{source_name} {seed_name} start is not 1") try: token_refs = parsed_token_refs(seed) except json.JSONDecodeError as exc: errors.append(f"{source_name} {seed_name} tokenRefsJson is invalid JSON: {exc}") continue if token_refs != expected_token_refs: errors.append(f"{source_name} {seed_name} tokenRefsJson does not match the PingCAP country-code/record-type contract") expected_contract_record_type_codes = { "Amendment": "AMD", "Customer Reference Agreement": "CRA", "TiDB Cloud Services Agreement": "CSA", "PingCAP Master Agreement": "MA", "PingCAP Non-disclosure Agreement": "NDA", "Payment Agreement": "PA", "PingCAP Partner Network Agreement": "PNA", "PingCAP Reseller Distribtion Addendum": "RDA", "TiDB Cloud PoC Agreement": "PoC", "TiDB Cloud Services Agreement Online": "CSAO", "TiDB Enterprise PoC Agreement": "EPOC", } contract_record_type_names = set(record_types.get("Contract", [])) for record_type_name, expected_code in expected_contract_record_type_codes.items(): if record_type_name not in contract_record_type_names: errors.append(f"record_types.json is missing Contract record type {record_type_name}") actual_code = record_type_codes.get(("Contract", record_type_name)) if actual_code != expected_code: errors.append(f"record_types.json Contract/{record_type_name} code is {actual_code!r}, want {expected_code!r}") for object_name, record_type_name in sorted(record_type_codes): if object_name == "Contract" and record_type_name not in expected_contract_record_type_codes: errors.append(f"record_types.json has unexpected Contract record type code for {record_type_name}") for object_name, names in sorted(record_types.items()): if "Master" in names: errors.append(f"record_types.json must not seed synthetic-only Master for {object_name}") if "Type" not in task_fields: errors.append("Task is missing Type field") elif [item.get("value") for item in task_fields["Type"].get("options", [])] != [ "Call", "Email", "Meeting", "Other", "Weekly Update", "Initial Engagement", "Technical Validation", "Commercial Negotiation", ]: errors.append("Task.Type picklist does not include the PingCAP Weekly Update values in order") disabled_kinds = {"list_view", "web_link", "field_set", "business_process"} if any(component.get("kind") in disabled_kinds for component in pack_components): errors.append("pack.json still includes disabled object subresource component kinds") if errors: fail(errors) print("final pack metadata audit PASSED") if __name__ == "__main__": main()