Crosswake.RuntimeLine.RebuildPolicy (crosswake v0.1.2)

View Source

Policy contract deriving OTA-safe vs. rebuild-required classification from the existing native_runtime_version compatibility axis.

Derivation principle

This module does NOT maintain a full static registry of rebuild truth — it derives classification from the data that already owns that truth:

  • Capability-axis change classes (capability_family_add, bridge_schema_change, permission_add, entitlement_add, push_capability_change, url_scheme_change) derive their verdict from Capability.rebuild, which is the authoritative field on the capability record. Never classify by change-class label alone; always key off Capability.rebuild. This is the footgun the Expo EAS / CodePush "it's-just-JS" OTA break demonstrates: label-only classification silently misses native-incompatible changes.

  • System-level change classes (sdk_floor_bump, privacy_manifest_entry) have no capability owner; they map via the locked @system_rebuild_classes closed set. New system classes go through a phase, not a config key.

companion_required is never OTA-safe

A change that requires a companion rebuild still requires a binary rebuild — it is never safe to push OTA. classify/2 with :companion_required always returns {:rebuild_required, :companion_shell}.

diff/2 is tooling input, NOT a release-gate oracle

diff/2 classifies changes between two Root.t() manifests and returns a list of {change_class(), verdict()} pairs. It does NOT know whether a binary with the new native_runtime_version has already shipped to the App Store or Google Play. Using diff/2 output as a release gate without consulting the published version history is incorrect — it is a doctor/tooling input only.

Summary

Functions

Classifies a change_class() as OTA-safe or rebuild-required.

Detects change classes between two Root.t() manifests and classifies each.

Public predicate over Capability.rebuild().

Types

change_class()

@type change_class() ::
  :bridge_schema_change
  | :capability_family_add
  | :permission_add
  | :entitlement_add
  | :sdk_floor_bump
  | :privacy_manifest_entry
  | :push_capability_change
  | :url_scheme_change

verdict()

@type verdict() :: :ota_safe | {:rebuild_required, :native_shell | :companion_shell}

Functions

classify(change_class, capability)

@spec classify(change_class(), Crosswake.Manifest.Types.Capability.t() | nil) ::
  verdict()

Classifies a change_class() as OTA-safe or rebuild-required.

For capability-axis change classes, the capability argument must be a Capability.t() struct — nil is not accepted because the source of truth (Capability.rebuild) is required. Passing nil raises ArgumentError.

For system-level classes (sdk_floor_bump, privacy_manifest_entry), the capability argument is ignored and may be nil.

Examples

iex> cap = %Capability{id: "haptics", version: "1.0.0", rebuild: :native_required}
iex> RebuildPolicy.classify(:capability_family_add, cap)
{:rebuild_required, :native_shell}

iex> cap = %Capability{id: "haptics", version: "1.0.0", rebuild: :none}
iex> RebuildPolicy.classify(:capability_family_add, cap)
:ota_safe

iex> RebuildPolicy.classify(:sdk_floor_bump, nil)
{:rebuild_required, :native_shell}

diff(root_a, root_b)

@spec diff(Crosswake.Manifest.Types.Root.t(), Crosswake.Manifest.Types.Root.t()) :: [
  {change_class(), verdict()}
]

Detects change classes between two Root.t() manifests and classifies each.

Returns a list of {change_class(), verdict()} pairs by comparing root_a and root_b and feeding each detected change through classify/2.

This function is tooling/doctor input, NOT a release-gate oracle. It classifies changes present between two manifest snapshots; it does not know whether a binary with the new native_runtime_version has already shipped. Do not use the output of diff/2 alone as a production release gate.

rebuild_required?(atom)

@spec rebuild_required?(Crosswake.Manifest.Types.Capability.rebuild()) :: boolean()

Public predicate over Capability.rebuild().

Returns true when a rebuild is required for the given rebuild value, false when OTA-safe.

Examples

iex> RebuildPolicy.rebuild_required?(:none)
false

iex> RebuildPolicy.rebuild_required?(:native_required)
true

iex> RebuildPolicy.rebuild_required?(:companion_required)
true