feat(clone-app): extract-logic.py — in-app logic signals inventory

This commit is contained in:
fatih.bulut 2026-06-25 23:47:00 +03:00
parent 1bd0ba99a6
commit 4104f0ccac
7 changed files with 123 additions and 0 deletions

View File

@ -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()

View File

@ -0,0 +1,7 @@
package com.example
class LoginViewModel : ViewModel() {
fun validate(email: String): Boolean {
return email.matches(Regex("^[^@]+@[^@]+$"))
}
}

View File

@ -0,0 +1,3 @@
package com.example
enum class Status { IDLE, LOADING, ERROR }

View File

@ -0,0 +1,5 @@
package com.example
class SyncUseCase(private val repo: UserDao) {
suspend operator fun invoke() = repo.all()
}

View File

@ -0,0 +1,7 @@
package com.example
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun all(): List<UserEntity>
}

View File

@ -0,0 +1,4 @@
package com.example
@Entity(tableName = "users")
data class UserEntity(val id: Long, val email: String)

View File

@ -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()