feat(clone-app): extract-logic.py — in-app logic signals inventory
This commit is contained in:
parent
1bd0ba99a6
commit
4104f0ccac
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.example
|
||||
|
||||
class LoginViewModel : ViewModel() {
|
||||
fun validate(email: String): Boolean {
|
||||
return email.matches(Regex("^[^@]+@[^@]+$"))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.example
|
||||
|
||||
enum class Status { IDLE, LOADING, ERROR }
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.example
|
||||
|
||||
class SyncUseCase(private val repo: UserDao) {
|
||||
suspend operator fun invoke() = repo.all()
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.example
|
||||
|
||||
@Dao
|
||||
interface UserDao {
|
||||
@Query("SELECT * FROM users")
|
||||
fun all(): List<UserEntity>
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package com.example
|
||||
|
||||
@Entity(tableName = "users")
|
||||
data class UserEntity(val id: Long, val email: String)
|
||||
|
|
@ -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()
|
||||
Loading…
Reference in New Issue