feat(market-research): add history.py non-repeat memory with offline test

This commit is contained in:
fatih.bulut 2026-06-22 00:52:45 +03:00
parent 5beb04b43d
commit ae840beaa3
4 changed files with 145 additions and 0 deletions

View File

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

View File

@ -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" }
]

View File

@ -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" }
]
}

View File

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