feat(market-research): add history.py non-repeat memory with offline test
This commit is contained in:
parent
5beb04b43d
commit
ae840beaa3
|
|
@ -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()
|
||||||
|
|
@ -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" }
|
||||||
|
]
|
||||||
|
|
@ -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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
Loading…
Reference in New Issue