#!/usr/bin/env python3 """growth.py — Vòng tăng trưởng (4 phase SEARCH→EXECUTION) + lớp Vận hành (ONGOING). Hợp nhất: định nghĩa CHUNG (content/growth_cycle/phases.json, read-only) + tiến độ thực từ startup_kit (mỗi phase gom các framework của nó) + active_phase PER-CÔNG-TY (company_kb/business/_phase.json, mặc định = default_active). Read-only cho dashboard; bot có thể đọc cùng file để biết công ty đang ở giai đoạn nào. Stdlib only. python3 lib/growth.py phases -> {active, cycle:[{...,progress,active}], operate:{tools}} """ import os import sys import json try: import startup_kit as _sk # sibling lib — nguồn tiến độ framework except Exception: _sk = None def _bundle_root(): return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ROOT = _bundle_root() DEFS = os.path.join(ROOT, "content", "growth_cycle", "phases.json") PHASE_STATE = os.path.join(ROOT, "company_kb", "business", "_phase.json") def _defs(): with open(DEFS, encoding="utf-8") as f: return json.load(f) def _active(defs): try: with open(PHASE_STATE, encoding="utf-8") as f: return json.load(f).get("active_phase") or defs.get("default_active") except Exception: return defs.get("default_active") def _fw_progress(): """{fw_id: {done,total}} từ startup_kit.status(). Ép path về ROOT của growth (cùng bundle) để tránh lệch company_kb khi lib là symlink.""" out = {} if not _sk: return out try: _sk.ROOT = ROOT _sk.MANIFEST = os.path.join(ROOT, "content", "startup_kit", "manifest.json") _sk.KB = os.path.join(ROOT, "company_kb") _sk.BUSINESS = os.path.join(_sk.KB, "business") _sk.STATE = os.path.join(_sk.BUSINESS, "_state.json") for f in _sk.status().get("frameworks", []): out[f["id"]] = {"done": f.get("blocks_done", 0), "total": f.get("blocks_total", 0)} except Exception: pass return out def _phase_progress(phase, fwp): done = total = 0 has = False for t in phase.get("tools", []): if t.get("type") == "framework" and t.get("fw") in fwp: has = True done += fwp[t["fw"]]["done"] total += fwp[t["fw"]]["total"] pct = int(round(100 * done / total)) if total else 0 return {"has": has, "done": done, "total": total, "pct": pct} def phases(): defs = _defs() fwp = _fw_progress() active = _active(defs) keep = ("id", "order", "name_vi", "name_en", "mode", "goal", "exit_criteria", "kpis", "tools", "is_gate") cycle = [] for p in sorted(defs.get("cycle", []), key=lambda x: x.get("order", 99)): item = {k: p[k] for k in keep if k in p} item["active"] = (p["id"] == active) item["progress"] = _phase_progress(p, fwp) cycle.append(item) op = defs.get("operate", {}) operate = {k: op[k] for k in ("id", "name_vi", "mode", "goal", "tools") if k in op} return {"ok": True, "active": active, "cycle": cycle, "operate": operate} def main(): cmd = sys.argv[1] if len(sys.argv) > 1 else "phases" out = phases() if cmd == "phases" else {"ok": False, "message": "unknown cmd: %s" % cmd} print(json.dumps(out, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": sys.exit(main())