From 4104f0ccac58a1e13ae6a8a4de54a826dad2c70e Mon Sep 17 00:00:00 2001 From: "fatih.bulut" Date: Thu, 25 Jun 2026 23:47:00 +0300 Subject: [PATCH] =?UTF-8?q?feat(clone-app):=20extract-logic.py=20=E2=80=94?= =?UTF-8?q?=20in-app=20logic=20signals=20inventory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../skills/clone-app/scripts/extract-logic.py | 67 +++++++++++++++++++ .../com/example/LoginViewModel.kt | 7 ++ .../logic-sample/com/example/Status.kt | 3 + .../logic-sample/com/example/SyncUseCase.kt | 5 ++ .../logic-sample/com/example/UserDao.kt | 7 ++ .../logic-sample/com/example/UserEntity.kt | 4 ++ plugins/clone-app/tests/test-extract-logic.py | 30 +++++++++ 7 files changed, 123 insertions(+) create mode 100755 plugins/clone-app/skills/clone-app/scripts/extract-logic.py create mode 100644 plugins/clone-app/tests/fixtures/logic-sample/com/example/LoginViewModel.kt create mode 100644 plugins/clone-app/tests/fixtures/logic-sample/com/example/Status.kt create mode 100644 plugins/clone-app/tests/fixtures/logic-sample/com/example/SyncUseCase.kt create mode 100644 plugins/clone-app/tests/fixtures/logic-sample/com/example/UserDao.kt create mode 100644 plugins/clone-app/tests/fixtures/logic-sample/com/example/UserEntity.kt create mode 100644 plugins/clone-app/tests/test-extract-logic.py diff --git a/plugins/clone-app/skills/clone-app/scripts/extract-logic.py b/plugins/clone-app/skills/clone-app/scripts/extract-logic.py new file mode 100755 index 0000000..484a2b4 --- /dev/null +++ b/plugins/clone-app/skills/clone-app/scripts/extract-logic.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Surface in-app logic signals from a decompiled APK source tree. + +Walks .java/.kt sources and flags ViewModel/use-case classes, input-validation +calls, state-machine enums/sealed classes, and Room @Entity/@Dao declarations. +Stdlib-only. Emits a JSON signals inventory on stdout (or --out FILE); the +Phase 8 fidelity subagent distills it (plus the sources) into logic-digest.md. +""" +import os, re, json, argparse + +SRC_EXT = (".java", ".kt") +VALIDATION_PAT = re.compile(r'(Pattern\.compile|\.matches\(|isValid|require\(|Validators?\.)') + +def _iter_sources(root): + for dp, _, files in os.walk(root): + for fn in files: + if fn.endswith(SRC_EXT): + yield os.path.join(dp, fn) + +def extract(root): + vms, ucs, vals, enums, entities, daos = [], [], [], [], [], [] + for path in _iter_sources(root): + name = os.path.splitext(os.path.basename(path))[0] + rel = os.path.relpath(path, root) + try: + text = open(path, encoding="utf-8", errors="replace").read() + except OSError: + continue + if name.endswith("ViewModel"): + vms.append({"file": rel, "name": name}) + if name.endswith(("UseCase", "Interactor")): + ucs.append({"file": rel, "name": name}) + for i, line in enumerate(text.splitlines(), 1): + if VALIDATION_PAT.search(line): + vals.append({"file": rel, "line": i, "snippet": line.strip()[:160]}) + for m in re.finditer(r'\b(?:enum|sealed)\s+class\s+(\w+)', text): + enums.append({"file": rel, "name": m.group(1)}) + for m in re.finditer(r'\benum\s+(\w+)\s*\{', text): + enums.append({"file": rel, "name": m.group(1)}) + if re.search(r'@Entity\b', text): + entities.append({"file": rel, "name": name}) + if re.search(r'@Dao\b', text): + daos.append({"file": rel, "name": name}) + return { + "root": root, + "viewmodels": vms, + "usecases": ucs, + "validation": vals, + "state_enums": enums, + "room_entities": entities, + "room_daos": daos, + } + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("root", help="decompile output root (sources)") + ap.add_argument("--out") + args = ap.parse_args() + text = json.dumps(extract(args.root), indent=2) + if args.out: + with open(args.out, "w") as f: + f.write(text) + else: + print(text) + +if __name__ == "__main__": + main() diff --git a/plugins/clone-app/tests/fixtures/logic-sample/com/example/LoginViewModel.kt b/plugins/clone-app/tests/fixtures/logic-sample/com/example/LoginViewModel.kt new file mode 100644 index 0000000..d1e9ac4 --- /dev/null +++ b/plugins/clone-app/tests/fixtures/logic-sample/com/example/LoginViewModel.kt @@ -0,0 +1,7 @@ +package com.example + +class LoginViewModel : ViewModel() { + fun validate(email: String): Boolean { + return email.matches(Regex("^[^@]+@[^@]+$")) + } +} diff --git a/plugins/clone-app/tests/fixtures/logic-sample/com/example/Status.kt b/plugins/clone-app/tests/fixtures/logic-sample/com/example/Status.kt new file mode 100644 index 0000000..8667f8f --- /dev/null +++ b/plugins/clone-app/tests/fixtures/logic-sample/com/example/Status.kt @@ -0,0 +1,3 @@ +package com.example + +enum class Status { IDLE, LOADING, ERROR } diff --git a/plugins/clone-app/tests/fixtures/logic-sample/com/example/SyncUseCase.kt b/plugins/clone-app/tests/fixtures/logic-sample/com/example/SyncUseCase.kt new file mode 100644 index 0000000..542e3d5 --- /dev/null +++ b/plugins/clone-app/tests/fixtures/logic-sample/com/example/SyncUseCase.kt @@ -0,0 +1,5 @@ +package com.example + +class SyncUseCase(private val repo: UserDao) { + suspend operator fun invoke() = repo.all() +} diff --git a/plugins/clone-app/tests/fixtures/logic-sample/com/example/UserDao.kt b/plugins/clone-app/tests/fixtures/logic-sample/com/example/UserDao.kt new file mode 100644 index 0000000..a2fa443 --- /dev/null +++ b/plugins/clone-app/tests/fixtures/logic-sample/com/example/UserDao.kt @@ -0,0 +1,7 @@ +package com.example + +@Dao +interface UserDao { + @Query("SELECT * FROM users") + fun all(): List +} diff --git a/plugins/clone-app/tests/fixtures/logic-sample/com/example/UserEntity.kt b/plugins/clone-app/tests/fixtures/logic-sample/com/example/UserEntity.kt new file mode 100644 index 0000000..951cbb1 --- /dev/null +++ b/plugins/clone-app/tests/fixtures/logic-sample/com/example/UserEntity.kt @@ -0,0 +1,4 @@ +package com.example + +@Entity(tableName = "users") +data class UserEntity(val id: Long, val email: String) diff --git a/plugins/clone-app/tests/test-extract-logic.py b/plugins/clone-app/tests/test-extract-logic.py new file mode 100644 index 0000000..9df256e --- /dev/null +++ b/plugins/clone-app/tests/test-extract-logic.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +import json, subprocess, sys, os + +HERE = os.path.dirname(os.path.abspath(__file__)) +SCRIPT = os.path.join(HERE, "..", "skills", "clone-app", "scripts", "extract-logic.py") +ROOT = os.path.join(HERE, "fixtures", "logic-sample") + +def run(): + out = subprocess.check_output([sys.executable, SCRIPT, ROOT]) + return json.loads(out) + +def main(): + d = run() + fails = [] + def check(name, cond): + print(f"{'PASS' if cond else 'FAIL'}: {name}") + if not cond: fails.append(name) + names = lambda lst: {x["name"] for x in lst} + check("viewmodel detected", "LoginViewModel" in names(d["viewmodels"])) + check("usecase detected", "SyncUseCase" in names(d["usecases"])) + check("room entity detected", "UserEntity" in names(d["room_entities"])) + check("room dao detected", "UserDao" in names(d["room_daos"])) + check("state enum detected", "Status" in names(d["state_enums"])) + check("validation flagged", any("matches" in v["snippet"] for v in d["validation"])) + check("validation has file+line", all({"file","line","snippet"} <= set(v) for v in d["validation"])) + for k in ["root","viewmodels","usecases","validation","state_enums","room_entities","room_daos"]: + check(f"key present: {k}", k in d) + sys.exit(1 if fails else 0) + +main()