From ae840beaa325a790edcd5be340697f85d44e9957 Mon Sep 17 00:00:00 2001 From: "fatih.bulut" Date: Mon, 22 Jun 2026 00:52:45 +0300 Subject: [PATCH] feat(market-research): add history.py non-repeat memory with offline test --- .../skills/market-research/scripts/history.py | 83 +++++++++++++++++++ .../tests/fixtures/candidates-sample.json | 5 ++ .../tests/fixtures/history-seed.json | 6 ++ plugins/market-research/tests/test-history.py | 51 ++++++++++++ 4 files changed, 145 insertions(+) create mode 100755 plugins/market-research/skills/market-research/scripts/history.py create mode 100644 plugins/market-research/tests/fixtures/candidates-sample.json create mode 100644 plugins/market-research/tests/fixtures/history-seed.json create mode 100644 plugins/market-research/tests/test-history.py diff --git a/plugins/market-research/skills/market-research/scripts/history.py b/plugins/market-research/skills/market-research/scripts/history.py new file mode 100755 index 0000000..16ba9b9 --- /dev/null +++ b/plugins/market-research/skills/market-research/scripts/history.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Non-repeat memory for market-research suggestions. + +history.json holds every candidate ever suggested so a run can avoid repeating +itself. Identity key = package if present, else the lowercased/stripped name. + +Subcommands (candidates read as a JSON array on stdin): + filter --history H.json -> print the candidates minus already-seen ones + add --history H.json -> append candidates to H.json, print {added,total} + +Date/run_id for added entries come from --date / --run-id (the skill passes the +run date); both default to empty strings so the script is deterministic and +needs no clock. Stdlib-only, no pip. +""" +import sys, json, argparse, os + +def cand_key(entry): + """Canonical identity for a candidate. Package wins; else lowercased name.""" + pkg = entry.get("package") + if pkg: + return str(pkg).strip() + return str(entry.get("name", "")).strip().lower() + +def load_history(path): + if not os.path.exists(path): + return {"suggestions": []} + with open(path, encoding="utf-8") as f: + data = json.load(f) + data.setdefault("suggestions", []) + return data + +def seen_keys(history): + return {s.get("key") for s in history["suggestions"]} + +def read_candidates(): + data = json.load(sys.stdin) + if not isinstance(data, list): + raise ValueError("candidates stdin must be a JSON array") + return data + +def cmd_filter(args): + history = load_history(args.history) + seen = seen_keys(history) + cands = read_candidates() + survivors = [c for c in cands if cand_key(c) not in seen] + print(json.dumps(survivors, indent=2)) + +def cmd_add(args): + history = load_history(args.history) + seen = seen_keys(history) + cands = read_candidates() + added = 0 + for c in cands: + k = cand_key(c) + if k in seen: + continue + history["suggestions"].append({ + "key": k, + "name": c.get("name"), + "package": c.get("package"), + "date": args.date, + "run_id": args.run_id, + }) + seen.add(k) + added += 1 + os.makedirs(os.path.dirname(os.path.abspath(args.history)), exist_ok=True) + with open(args.history, "w", encoding="utf-8") as f: + json.dump(history, f, indent=2) + print(json.dumps({"added": added, "total": len(history["suggestions"])}, indent=2)) + +def main(): + ap = argparse.ArgumentParser() + sub = ap.add_subparsers(dest="cmd", required=True) + for name in ("filter", "add"): + p = sub.add_parser(name) + p.add_argument("--history", required=True) + p.add_argument("--date", default="") + p.add_argument("--run-id", default="") + args = ap.parse_args() + {"filter": cmd_filter, "add": cmd_add}[args.cmd](args) + +if __name__ == "__main__": + main() diff --git a/plugins/market-research/tests/fixtures/candidates-sample.json b/plugins/market-research/tests/fixtures/candidates-sample.json new file mode 100644 index 0000000..9e2af50 --- /dev/null +++ b/plugins/market-research/tests/fixtures/candidates-sample.json @@ -0,0 +1,5 @@ +[ + { "name": "Puzzle Quest Saga", "package": "com.casual.puzzlequest", "category": "Games" }, + { "name": "habit tracker PRO", "category": "Productivity" }, + { "name": "Budget Buddy", "package": "com.fintech.budgetbuddy", "category": "Finance" } +] diff --git a/plugins/market-research/tests/fixtures/history-seed.json b/plugins/market-research/tests/fixtures/history-seed.json new file mode 100644 index 0000000..13db63f --- /dev/null +++ b/plugins/market-research/tests/fixtures/history-seed.json @@ -0,0 +1,6 @@ +{ + "suggestions": [ + { "key": "com.casual.puzzlequest", "name": "Puzzle Quest Saga", "package": "com.casual.puzzlequest", "date": "2026-06-01", "run_id": "seed" }, + { "key": "habit tracker pro", "name": "Habit Tracker Pro", "package": null, "date": "2026-06-01", "run_id": "seed" } + ] +} diff --git a/plugins/market-research/tests/test-history.py b/plugins/market-research/tests/test-history.py new file mode 100644 index 0000000..63ad544 --- /dev/null +++ b/plugins/market-research/tests/test-history.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +import json, subprocess, sys, os, tempfile, shutil + +HERE = os.path.dirname(os.path.abspath(__file__)) +SCRIPT = os.path.join(HERE, "..", "skills", "market-research", "scripts", "history.py") +FIX = os.path.join(HERE, "fixtures") + +def run(args, stdin_path): + with open(stdin_path, "rb") as f: + return subprocess.run([sys.executable, SCRIPT, *args], + stdin=f, capture_output=True) + +def main(): + fails = [] + def check(name, cond): + print(f"{'PASS' if cond else 'FAIL'}: {name}") + if not cond: fails.append(name) + + cands = os.path.join(FIX, "candidates-sample.json") + + # filter: against the seed, only "Budget Buddy" survives + r = run(["filter", "--history", os.path.join(FIX, "history-seed.json")], cands) + check("filter exit 0", r.returncode == 0) + survivors = json.loads(r.stdout) + names = sorted(c["name"] for c in survivors) + check("filter drops package collision + name collision", names == ["Budget Buddy"]) + + # filter against a missing history file = nothing seen, all 3 survive + tmp = tempfile.mkdtemp() + try: + missing = os.path.join(tmp, "nope.json") + r = run(["filter", "--history", missing], cands) + check("filter missing-history exit 0", r.returncode == 0) + check("filter missing-history keeps all", len(json.loads(r.stdout)) == 3) + + # add: appends all 3 to a fresh history, reports counts + h = os.path.join(tmp, "h.json") + r = run(["add", "--history", h], cands) + check("add exit 0", r.returncode == 0) + rep = json.loads(r.stdout) + check("add reports added 3", rep["added"] == 3) + check("add reports total 3", rep["total"] == 3) + saved = json.load(open(h)) + check("history file has 3 suggestions", len(saved["suggestions"]) == 3) + check("history entries carry key", all("key" in s for s in saved["suggestions"])) + finally: + shutil.rmtree(tmp) + + sys.exit(1 if fails else 0) + +main()