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