Skip to content

Change Report Schema

ankorio edited this page Jul 15, 2026 · 2 revisions

The stable, machine-consumable output of twinflame … -f json -o OUT (JSON is the default format). Downstream tooling (decompiler alignment, diff viewers) should consume this, not the human stdout summary. The shape is versioned by the top-level schema_version (twinflame.changes.CHANGES_SCHEMA_VERSION); a breaking change bumps it. -f csv / -f xml carry the same per-class fields in a flat form.

v2 adds lhs_super / rhs_super, lhs_interfaces / rhs_interfaces, and components to every entry (type-graph context for malware triage). v3 adds library — the dictionary-evidence provenance label (<coord>@<version-range>) when the class matched the known-library signature dictionary (--libsigs).

Top level

{
  "schema_version": 3,
  "summary": { "modified": 384, "added": 282, "removed": 135, "cosmetic": 414, "unchanged": 10436 },
  "changes": [ /* one object per class, ranked most-review-worthy first */ ]
}
Field Type Meaning
schema_version int Format version. Consumers should check this.
summary object Count per kind (keys absent when zero).
changes array One entry per class, pre-sorted (see Ordering).

A changes[] entry

{
  "kind": "modified",
  "lhs": "Lorg/telegram/ui/ArticleViewer;",
  "rhs": "Lorg/telegram/ui/ArticleViewer;",
  "source_file": "SourceFile",
  "origin": "app",
  "confidence": "high",
  "magnitude": 95,
  "match_distance": 0.87,
  "anchored": true,
  "lhs_super": "Landroid/app/Service;",
  "rhs_super": "Landroid/app/Service;",
  "lhs_interfaces": [],
  "rhs_interfaces": [],
  "components": ["BAS"],
  "library": null,
  "delta": { /* only for kind=modified; see below */ },
  "methods": [ /* only for kind=modified when localizable; see below */ ]
}
Field Type Meaning
kind enum modified | added | removed | cosmetic | unchanged (see Kinds).
lhs string | null Old-build class descriptor (L…;). null when added.
rhs string | null New-build class descriptor. null when removed.
source_file string | null SourceFile attribute if retained (R8 often rewrites it to the literal "SourceFile" or strips it).
origin enum app | library | unknown — provenance (see Origin).
confidence enum high | low (see Confidence). Non-modified is always high.
magnitude int Review-worthiness. For modified: feature-delta count + methods added/deleted. For added/removed: class size in instructions. Else 0.
match_distance float Structural similarity of the underlying match in [0,1] (1.0 = identical). 0.0 for added/removed.
anchored bool Match rests on a rename-invariant string / framework-call anchor.
lhs_super / rhs_super string | null Superclass descriptor per side. Framework supers (Landroid/…;) survive R8, so they carry across the rename.
lhs_interfaces / rhs_interfaces array Declared interface descriptors per side.
components array Sensitive-component short-codes the class implements (transitive superclass/interface resolution): BNL BindNotificationListener, BAS BindAccessibilityService, BDA BindDeviceAdmin, BIM BindInputMethod, BAF BindAutofill, plus generic SVC/RCV/PRV/APP. Empty for ordinary classes.
library string | null (v3) Dictionary-evidence provenance: <coord>@<version-range> (e.g. com.squareup.okhttp3:okhttp@[4.9.0,4.12.0]) when the class matched the known-library signature dictionary (--libsigs). Set even for renamed classes that name/prefix heuristics can't reach; null when no dictionary was loaded or the class didn't match.
delta object | absent Present only for modified.
methods array | absent Per-method localization; present for modified when method-level changes are localizable.

delta (modified only)

All arrays are sorted; framework calls/strings/types are full descriptors. app_types_* are in new-build naming (translated through the class-match map).

"delta": {
  "calls_added":    ["Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;"],
  "calls_removed":  [],
  "strings_added":  ["v2/pay"],
  "strings_removed":["v1/pay"],
  "types_added":    ["Landroidx/appcompat/app/AppCompatActivity;"],
  "types_removed":  [],
  "app_types_added":   ["Lorg/telegram/ui/Foo;"],
  "app_types_removed": []
}

calls_* = framework/library call targets; strings_* = string constants; types_* = framework type references (super/interface/field/param); app_types_* = app type dependencies (only populated when a class-match map was available).

methods[] (modified only)

"methods": [
  { "status": "modified", "name": "onCreate", "descriptor": "(Landroid/os/Bundle;)V",
    "instr_count": 214, "calls_added": ["…Uri;->parse…"], "calls_removed": ["…Log;->d…"] },
  { "status": "added",   "name": "helper", "descriptor": "()V", "instr_count": 12,
    "calls_added": [""], "calls_removed": [] }
]

statusmodified | added | deleted. name/descriptor are obfuscated when the build renames them — resolve via the --deobfuscation-map. modified methods appear only when their framework-call set changed (a body-only re-optimization isn't localized).

Kinds

Kind Meaning delta/methods
modified Matched; semantic features or method structure changed — a real edit. yes
added Only in the new build. no
removed Only in the old build. no
cosmetic Matched, structurally different, but no semantic-feature change → re-obfuscation/re-optimization noise. Emitted, demoted. no
unchanged Matched and structurally identical. no

Origin

  • app — under the developer package (--app-package/--package, else the manifest). Highest priority.
  • library — bundled third party: known prefix, a maven-coordinate SourceFile, or (v3) signature-dictionary evidence (the library field is set). Dictionary evidence demotes a class to library unless the dev prefix claims it first — a library vendored under the app's own package still reviews as app code. Demoted.
  • unknown — can't tell (e.g. R8 repackaged it to a short name). Ranked between.

Confidence

  • high — trustworthy change.
  • low — a modified resting only on framework-call churn in non-app code. Different R8 versions relocate calls between classes via inlining, so this is the cross-toolchain-noise signature (measured 95% of false-modified on a same-source cross-R8 rebuild). Filter these out with --min-confidence high.

Ordering

changes[] is pre-sorted most-review-worthy first: kind (modifiedaddedremovedcosmeticunchanged), then origin (appunknownlibrary), then confidence (highlow), then descending magnitude. Consumers may re-sort; the origin/confidence/ magnitude fields carry everything needed to filter or re-rank.

Design contract

"Demote, never drop": by default every matched/added/removed class is emitted with a score and category — the report is complete and the consumer thresholds. --min-confidence high is the one built-in filter (drops low-confidence modified).

Clone this wiki locally