"""The object-lifecycle declaration contract, in one place (issue #4881). Two callers validate it: a preflight that runs before the build destroys the previous final pack, and the derivation inside the assembly. They MUST agree, because anything the preflight lets through and the derivation then rejects is a rejected input that destroyed a good artifact on its way out — the build refusing to produce a bad pack and deleting the working one instead. They did not agree. The preflight tested `str(spec.get("label") or "").strip()` while the derivation tested `isinstance(value, str)`, so `label: 123` passed the first and failed the second. Two hand-written copies of one rule, drifting — which is the failure this whole epic exists to remove, committed inside the fix for it. Hence one module, imported by both. """ LIFECYCLE_KEYS = ("lifecycleStatus", "supersededBy", "retiredOn") # Ordered by how much each state restricts, so a promotion can be told from a demotion. LIFECYCLE_SUFFIX = {"deprecated": "(Legacy)", "retired": "(Legacy — Retired)"} LIFECYCLE_INSTRUCTION = { "deprecated": "Historical lookups belong here; do not create or update.", "retired": "Read from {superseded_by} instead; do not create or update here.", } # `label` is required for a declared object: it is the marker a human sees in every list and lookup, # so an object without it reads as live to everyone who never opens it. `pluralLabel` is genuinely # optional — 17 of 79 L2 objects omit it. _REQUIRED_STRINGS = ("supersededBy", "retiredOn", "label") def _nonempty_string(value): return isinstance(value, str) and bool(value.strip()) def declaration_problems(spec): """Everything wrong with one object spec's lifecycle declaration, as human-readable lines. Returns [] for a spec that declares nothing — objects without lifecycle keys are the norm. """ if not isinstance(spec, dict): return [] supplied = [key for key in LIFECYCLE_KEYS if key in spec] if not supplied: return [] name = spec.get("apiName") name = name.strip() if isinstance(name, str) and name.strip() else "" status = spec.get("lifecycleStatus") if not _nonempty_string(status): return [f"{name}: lifecycle keys {sorted(supplied)} supplied without a usable lifecycleStatus"] if status not in LIFECYCLE_SUFFIX: return [f"{name}: unknown lifecycleStatus {status!r} (expected deprecated or retired)"] problems = [] for key in _REQUIRED_STRINGS: if not _nonempty_string(spec.get(key)): problems.append( f"{name}: lifecycleStatus {status!r} requires a non-empty string {key}, " f"got {spec.get(key)!r}" ) return problems