Files
claude-skills-reference/c-level-advisor/cpo-advisor/scripts/portfolio_analyzer.py
Alireza Rezvani 466aa13a7b feat: C-Suite expansion — 8 new executive advisory roles (2→10) (#264)
* feat: C-Suite expansion — 8 new executive advisory roles

Add COO, CPO, CMO, CFO, CRO, CISO, CHRO advisors and Executive Mentor.
Expands C-level advisory from 2 to 10 roles with 74 total files.

Each role includes:
- SKILL.md (lean, <5KB, ~1200 tokens for context efficiency)
- Reference docs (loaded on demand, not at startup)
- Python analysis scripts (stdlib only, runnable CLI)

Executive Mentor features /em: slash commands (challenge, board-prep,
hard-call, stress-test, postmortem) with devil's advocate agent.

21 Python tools, 24 reference frameworks, 28,379 total lines.
All SKILL.md files combined: ~17K tokens (8.5% of 200K context window).

Badge: 88 → 116 skills

* feat: C-Suite orchestration layer + 18 complementary skills

ORCHESTRATION (new):
- cs-onboard: Founder interview → company-context.md
- chief-of-staff: Routing, synthesis, inter-agent orchestration
- board-meeting: 6-phase multi-agent deliberation protocol
- decision-logger: Two-layer memory (raw transcripts + approved decisions)
- agent-protocol: Inter-agent invocation with loop prevention
- context-engine: Company context loading + anonymization

CROSS-CUTTING CAPABILITIES (new):
- board-deck-builder: Board/investor update assembly
- scenario-war-room: Cascading multi-variable what-if modeling
- competitive-intel: Systematic competitor tracking + battlecards
- org-health-diagnostic: Cross-functional health scoring (8 dimensions)
- ma-playbook: M&A strategy (acquiring + being acquired)
- intl-expansion: International market entry frameworks

CULTURE & COLLABORATION (new):
- culture-architect: Values → behaviors, culture code, health assessment
- company-os: EOS/Scaling Up operating system selection + implementation
- founder-coach: Founder development, delegation, blind spots
- strategic-alignment: Strategy cascade, silo detection, alignment scoring
- change-management: ADKAR-based change rollout framework
- internal-narrative: One story across employees/investors/customers

UPGRADES TO EXISTING ROLES:
- All 10 roles get reasoning technique directives
- All 10 roles get company-context.md integration
- All 10 roles get board meeting isolation rules
- CEO gets stage-adaptive temporal horizons (seed→C)

Key design decisions:
- Two-layer memory prevents hallucinated consensus from rejected ideas
- Phase 2 isolation: agents think independently before cross-examination
- Executive Mentor (The Critic) sees all perspectives, others don't
- 25 Python tools total (stdlib only, no dependencies)

52 new files, 10 modified, 10,862 new lines.
Total C-suite ecosystem: 134 files, 39,131 lines.

* fix: connect all dots — Chief of Staff routes to all 28 skills

- Added complementary skills registry to routing-matrix.md
- Chief of Staff SKILL.md now lists all 28 skills in ecosystem
- Added integration tables to scenario-war-room and competitive-intel
- Badge: 116 → 134 skills
- README: C-Level Advisory count 10 → 28

Quality audit passed:
 All 10 roles: company-context, reasoning, isolation, invocation
 All 6 phases in board meeting
 Two-layer memory with DO_NOT_RESURFACE
 Loop prevention (no self-invoke, max depth 2, no circular)
 All /em: commands present
 All complementary skills cross-reference roles
 Chief of Staff routes to every skill in ecosystem

* refactor: CEO + CTO advisors upgraded to C-suite parity

Both roles now match the structural standard of all new roles:
- CEO: 11.7KB → 6.8KB SKILL.md (heavy content stays in references)
- CTO: 10KB → 7.2KB SKILL.md (heavy content stays in references)

Added to both:
- Integration table (who they work with and when)
- Key diagnostic questions
- Structured metrics dashboard table
- Consistent section ordering (Keywords → Quick Start → Responsibilities → Questions → Metrics → Red Flags → Integration → Reasoning → Context)

CEO additions:
- Stage-adaptive temporal horizons (seed=3m/6m/12m → B+=1y/3y/5y)
- Cross-references to culture-architect and board-deck-builder

CTO additions:
- Key Questions section (7 diagnostic questions)
- Structured metrics table (DORA + debt + team + architecture + cost)
- Cross-references to all peer roles

All 10 roles now pass structural parity:  Keywords  QuickStart  Questions  Metrics  RedFlags  Integration

* feat: add proactive triggers + output artifacts to all 10 roles

Every C-suite role now specifies:
- Proactive Triggers: 'surface these without being asked' — context-driven
  early warnings that make advisors proactive, not reactive
- Output Artifacts: concrete deliverables per request type (what you ask →
  what you get)

CEO: runway alerts, board prep triggers, strategy review nudges
CTO: deploy frequency monitoring, tech debt thresholds, bus factor flags
COO: blocker detection, scaling threshold warnings, cadence gaps
CPO: retention curve monitoring, portfolio dog detection, research gaps
CMO: CAC trend monitoring, positioning gaps, budget staleness
CFO: runway forecasting, burn multiple alerts, scenario planning gaps
CRO: NRR monitoring, pipeline coverage, pricing review triggers
CISO: audit overdue alerts, compliance gaps, vendor risk
CHRO: retention risk, comp band gaps, org scaling thresholds
Executive Mentor: board prep triggers, groupthink detection, hard call surfacing

This transforms the C-suite from reactive advisors into proactive partners.

* feat: User Communication Standard — structured output for all roles

Defines 3 output formats in agent-protocol/SKILL.md:

1. Standard Output: Bottom Line → What → Why → How to Act → Risks → Your Decision
2. Proactive Alert: What I Noticed → Why It Matters → Action → Urgency (🔴🟡)
3. Board Meeting: Decision Required → Perspectives → Agree/Disagree → Critic → Action Items

10 non-negotiable rules:
- Bottom line first, always
- Results and decisions only (no process narration)
- What + Why + How for every finding
- Actions have owners and deadlines ('we should consider' is banned)
- Decisions framed as options with trade-offs
- Founder is the highest authority — roles recommend, founder decides
- Risks are concrete (if X → Y, costs $Z)
- Max 5 bullets per section
- No jargon without explanation
- Silence over fabricated updates

All 10 roles reference this standard.
Chief of Staff enforces it as a quality gate.
Board meeting Phase 4 uses the Board Meeting Output format.

* feat: Internal Quality Loop — verification before delivery

No role presents to the founder without passing verification:

Step 1: Self-Verification (every role, every time)
  - Source attribution: where did each data point come from?
  - Assumption audit: [VERIFIED] vs [ASSUMED] tags on every finding
  - Confidence scoring: 🟢 high / 🟡 medium / 🔴 low per finding
  - Contradiction check against company-context + decision log
  - 'So what?' test: every finding needs a business consequence

Step 2: Peer Verification (cross-functional)
  - Financial claims → CFO validates math
  - Revenue projections → CRO validates pipeline backing
  - Technical feasibility → CTO validates
  - People/hiring impact → CHRO validates
  - Skip for single-domain, low-stakes questions

Step 3: Critic Pre-Screen (high-stakes only)
  - Irreversible decisions, >20% runway impact, strategy changes
  - Executive Mentor finds weakest point before founder sees it
  - Suspicious consensus triggers mandatory pre-screen

Step 4: Course Correction (after founder feedback)
  - Approve → log + assign actions
  - Modify → re-verify changed parts
  - Reject → DO_NOT_RESURFACE + learn why
  - 30/60/90 day post-decision review

Board meeting contributions now require self-verified format with
confidence tags and source attribution on every finding.

* fix: resolve PR review issues 1, 4, and minor observation

Issue 1: c-level-advisor/CLAUDE.md — completely rewritten
  - Was: 2 skills (CEO, CTO only), dated Nov 2025
  - Now: full 28-skill ecosystem map with architecture diagram,
    all roles/orchestration/cross-cutting/culture skills listed,
    design decisions, integration with other domains

Issue 4: Root CLAUDE.md — updated all stale counts
  - 87 → 134 skills across all 3 references
  - C-Level: 2 → 33 (10 roles + 5 mentor commands + 18 complementary)
  - Tool count: 160+ → 185+
  - Reference count: 200+ → 250+

Minor observation: Documented plugin.json convention
  - Explained in c-level-advisor/CLAUDE.md that only executive-mentor
    has plugin.json because only it has slash commands (/em: namespace)
  - Other skills are invoked by name through Chief of Staff or directly

Also fixed: README.md 88+ → 134 in two places (first line + skills section)

* fix: update all plugin/index registrations for 28-skill C-suite

1. c-level-advisor/.claude-plugin/plugin.json — v2.0.0
   - Was: 2 skills, generic description
   - Now: all 28 skills listed with descriptions, all 25 scripts,
     namespace 'cs', full ecosystem description

2. .codex/skills-index.json — added 18 complementary skills
   - Was: 10 roles only
   - Now: 28 total c-level entries (10 roles + 6 orchestration +
     6 cross-cutting + 6 culture)
   - Each with full description for skill discovery

3. .claude-plugin/marketplace.json — updated c-level-skills entry
   - Was: generic 2-skill description
   - Now: v2.0.0, full 28-skill ecosystem description,
     skills_count: 28, scripts_count: 25

* feat: add root SKILL.md for c-level-advisor ClawHub package

---------

Co-authored-by: Leo <leo@openclaw.ai>
2026-03-06 01:35:08 +01:00

548 lines
21 KiB
Python

#!/usr/bin/env python3
"""
Portfolio Analyzer — Product portfolio BCG matrix classification and investment analysis.
For each product, classifies into BCG quadrant (Star, Cash Cow, Question Mark, Dog)
and generates investment recommendations (Invest / Maintain / Kill).
Usage:
python portfolio_analyzer.py # Run with built-in sample data
python portfolio_analyzer.py --input data.json # Run with your data
python portfolio_analyzer.py --json # Output raw JSON
JSON input format: see sample_data() function below.
"""
import json
import sys
import argparse
from typing import Optional
# ---------------------------------------------------------------------------
# Sample data
# ---------------------------------------------------------------------------
def sample_data() -> dict:
"""
Sample portfolio. Replace with real product data.
Fields:
name Product name
revenue_quarterly Current quarter revenue (any consistent currency)
revenue_prev_q Revenue last quarter (for QoQ calculation)
market_growth_pct Annual market growth rate (percent, e.g. 12.5 for 12.5%)
your_market_share Your estimated market share (percent, e.g. 8.0 for 8%)
largest_competitor_share Largest competitor's share (percent)
eng_capacity_pct % of total engineering capacity allocated (0-100)
d30_retention Optional D30 retention rate (decimal, e.g. 0.45)
nps Optional NPS score (-100 to 100)
notes Optional free text notes for the report
"""
return {
"company": "Acme Corp",
"total_engineering_headcount": 45,
"products": [
{
"name": "CorePlatform",
"revenue_quarterly": 480000,
"revenue_prev_q": 430000,
"market_growth_pct": 22.0,
"your_market_share": 18.0,
"largest_competitor_share": 12.0,
"eng_capacity_pct": 35,
"d30_retention": 0.61,
"nps": 52,
"notes": "Our flagship. Leading market share in fast-growing segment.",
},
{
"name": "ReportingModule",
"revenue_quarterly": 290000,
"revenue_prev_q": 285000,
"market_growth_pct": 5.0,
"your_market_share": 22.0,
"largest_competitor_share": 18.0,
"eng_capacity_pct": 25,
"d30_retention": 0.58,
"nps": 38,
"notes": "Mature product, strong margins, slow market.",
},
{
"name": "MobileApp",
"revenue_quarterly": 95000,
"revenue_prev_q": 78000,
"market_growth_pct": 35.0,
"your_market_share": 3.5,
"largest_competitor_share": 24.0,
"eng_capacity_pct": 28,
"d30_retention": 0.31,
"nps": 22,
"notes": "High growth market. We're far behind on share. Bet or exit.",
},
{
"name": "LegacyConnector",
"revenue_quarterly": 62000,
"revenue_prev_q": 68000,
"market_growth_pct": -3.0,
"your_market_share": 8.0,
"largest_competitor_share": 35.0,
"eng_capacity_pct": 12,
"d30_retention": 0.42,
"nps": 14,
"notes": "Declining market. Customers are on long-term contracts.",
},
],
}
# ---------------------------------------------------------------------------
# BCG Classification
# ---------------------------------------------------------------------------
# Growth rate threshold: markets growing faster than this are "high growth"
GROWTH_THRESHOLD_PCT = 10.0
# Market share ratio threshold: ratio > 1.0 means you lead the market
SHARE_RATIO_THRESHOLD = 1.0
def bcg_quadrant(market_growth_pct: float, share_ratio: float) -> str:
high_growth = market_growth_pct >= GROWTH_THRESHOLD_PCT
leading_share = share_ratio >= SHARE_RATIO_THRESHOLD
if high_growth and leading_share:
return "Star"
elif not high_growth and leading_share:
return "Cash Cow"
elif high_growth and not leading_share:
return "Question Mark"
else:
return "Dog"
def quadrant_emoji(quadrant: str) -> str:
return {
"Star": "",
"Cash Cow": "🐄",
"Question Mark": "",
"Dog": "🐕",
}.get(quadrant, "?")
def investment_posture(quadrant: str, qoq_growth: float, retention: Optional[float]) -> str:
"""
Invest / Maintain / Kill recommendation with nuance.
"""
if quadrant == "Star":
return "Invest"
elif quadrant == "Cash Cow":
# If cash cow is declining fast or retention is poor, consider killing
if qoq_growth < -0.10 or (retention is not None and retention < 0.30):
return "Kill"
return "Maintain"
elif quadrant == "Question Mark":
# Fast QoQ growth signals the bet might pay off → Invest
# Flat or slow QoQ with weak retention → Kill
if qoq_growth >= 0.15 and (retention is None or retention >= 0.25):
return "Invest"
elif qoq_growth < 0.05 or (retention is not None and retention < 0.20):
return "Kill"
return "Evaluate" # Needs explicit strategic decision
else: # Dog
if qoq_growth > 0.10 and (retention is None or retention >= 0.35):
return "Evaluate" # Surprising momentum — verify before killing
return "Kill"
def posture_color(posture: str) -> str:
return {
"Invest": "",
"Maintain": "",
"Kill": "",
"Evaluate": "",
}.get(posture, "?")
# ---------------------------------------------------------------------------
# Product analysis
# ---------------------------------------------------------------------------
def analyze_product(p: dict) -> dict:
revenue_q = p.get("revenue_quarterly", 0)
revenue_prev = p.get("revenue_prev_q", revenue_q)
qoq_growth = (revenue_q - revenue_prev) / revenue_prev if revenue_prev else 0.0
your_share = p.get("your_market_share", 0)
competitor_share = p.get("largest_competitor_share", 1)
share_ratio = your_share / competitor_share if competitor_share else 0.0
market_growth = p.get("market_growth_pct", 0)
retention = p.get("d30_retention")
nps = p.get("nps")
eng_pct = p.get("eng_capacity_pct", 0)
quadrant = bcg_quadrant(market_growth, share_ratio)
posture = investment_posture(quadrant, qoq_growth, retention)
# Alignment score: how well does engineering investment match the recommended posture?
# Invest products should have high eng allocation; Kill products should have low.
alignment_score = _compute_alignment(posture, eng_pct)
return {
"name": p.get("name", "Unknown"),
"revenue_quarterly": revenue_q,
"revenue_prev_q": revenue_prev,
"qoq_growth": qoq_growth,
"market_growth_pct": market_growth,
"your_market_share": your_share,
"largest_competitor_share": competitor_share,
"share_ratio": share_ratio,
"eng_capacity_pct": eng_pct,
"d30_retention": retention,
"nps": nps,
"quadrant": quadrant,
"posture": posture,
"alignment_score": alignment_score,
"notes": p.get("notes", ""),
"findings": _product_findings(quadrant, posture, qoq_growth, share_ratio,
market_growth, retention, nps, eng_pct),
}
def _compute_alignment(posture: str, eng_pct: float) -> float:
"""
Returns 0.0-1.0 score. High = engineering allocation matches strategic posture.
"""
targets = {"Invest": 0.35, "Maintain": 0.15, "Kill": 0.05, "Evaluate": 0.20}
target = targets.get(posture, 0.20)
deviation = abs(eng_pct / 100 - target)
return max(0.0, 1.0 - (deviation / 0.35))
def _product_findings(
quadrant: str, posture: str,
qoq_growth: float, share_ratio: float, market_growth: float,
retention: Optional[float], nps: Optional[int], eng_pct: float
) -> list:
findings = []
if quadrant == "Star":
if eng_pct < 30:
findings.append(f"⚠ Star product getting only {eng_pct}% of eng capacity — likely underinvested. Stars need fuel.")
else:
findings.append(f"✓ Star product with {eng_pct}% eng allocation — appropriate investment.")
if share_ratio < 1.5:
findings.append(f"◑ Share ratio {share_ratio:.1f}x — leading but not dominant. Accelerate to widen the gap.")
else:
findings.append(f"✓ Share ratio {share_ratio:.1f}x — strong lead. Defend aggressively.")
elif quadrant == "Cash Cow":
if eng_pct > 25:
findings.append(f"⚠ Cash Cow getting {eng_pct}% of eng — overinvested. Reduce to 10-15% max. Redeploy to Stars.")
else:
findings.append(f"✓ Cash Cow with {eng_pct}% eng — appropriate. Don't innovate, just maintain.")
if qoq_growth < -0.05:
findings.append(f"⚠ Revenue declining {abs(qoq_growth):.0%} QoQ — monitor for transition to Dog.")
else:
findings.append(f"✓ Revenue stable (QoQ: {qoq_growth:+.0%}) — milk this.")
elif quadrant == "Question Mark":
findings.append(f"⚠ Fast market ({market_growth:.0f}% growth) but only {share_ratio:.1f}x relative share.")
findings.append(f" Decision required: Invest to capture share or exit. 'Maintain' loses share every quarter.")
if qoq_growth >= 0.15:
findings.append(f"✓ QoQ growth {qoq_growth:+.0%} — momentum building. Investment may be justified.")
elif qoq_growth < 0.05:
findings.append(f"✗ QoQ growth {qoq_growth:+.0%} — stalled despite hot market. Strong exit signal.")
elif quadrant == "Dog":
findings.append(f"✗ Low share ({share_ratio:.1f}x) in slow/declining market ({market_growth:.0f}% growth).")
if eng_pct > 10:
findings.append(f"✗ Dog consuming {eng_pct}% of eng capacity. Set a sunset date. Migrate customers.")
if qoq_growth > 0:
findings.append(f"◑ Slight QoQ growth ({qoq_growth:+.0%}) — verify whether this is genuine or contract timing.")
if retention is not None:
if retention < 0.30:
findings.append(f"✗ D30 retention {retention:.0%} — users not finding value. Weak unit economics for any posture.")
elif retention >= 0.50:
findings.append(f"✓ D30 retention {retention:.0%} — users find value. Supports investment or stable maintenance.")
if nps is not None:
if nps < 0:
findings.append(f"✗ NPS {nps} — net detractors. Word of mouth is negative. Fix before scaling.")
elif nps >= 40:
findings.append(f"✓ NPS {nps} — strong promoter base. Harness for referrals.")
return findings
# ---------------------------------------------------------------------------
# Portfolio-level analysis
# ---------------------------------------------------------------------------
def analyze_portfolio(data: dict) -> dict:
products = [analyze_product(p) for p in data.get("products", [])]
total_revenue = sum(p["revenue_quarterly"] for p in products)
total_eng = sum(p["eng_capacity_pct"] for p in products)
# Revenue by quadrant
quadrant_revenue = {}
quadrant_eng = {}
for p in products:
q = p["quadrant"]
quadrant_revenue[q] = quadrant_revenue.get(q, 0) + p["revenue_quarterly"]
quadrant_eng[q] = quadrant_eng.get(q, 0) + p["eng_capacity_pct"]
# Portfolio health score
health = _portfolio_health(products, total_revenue, total_eng)
# Portfolio-level findings
portfolio_findings = _portfolio_findings(products, total_revenue, quadrant_revenue, quadrant_eng)
return {
"company": data.get("company", "Unknown"),
"total_engineering_headcount": data.get("total_engineering_headcount"),
"products": products,
"total_revenue_quarterly": total_revenue,
"quadrant_summary": {
q: {
"count": sum(1 for p in products if p["quadrant"] == q),
"revenue": quadrant_revenue.get(q, 0),
"revenue_pct": quadrant_revenue.get(q, 0) / total_revenue if total_revenue else 0,
"eng_pct": quadrant_eng.get(q, 0),
}
for q in ["Star", "Cash Cow", "Question Mark", "Dog"]
},
"portfolio_health_score": health,
"portfolio_findings": portfolio_findings,
}
def _portfolio_health(products: list, total_revenue: float, total_eng: float) -> float:
"""
Portfolio health 0-1. Penalizes:
- No Stars (no growth engine)
- Dogs consuming > 20% of eng
- Poor alignment scores
- Revenue concentrated in Dogs/Question Marks
"""
score = 1.0
quadrants = [p["quadrant"] for p in products]
has_star = "Star" in quadrants
has_cash_cow = "Cash Cow" in quadrants
if not has_star:
score -= 0.25 # No growth engine is a serious problem
if not has_cash_cow:
score -= 0.10 # No cash generator means funding stars from burn
# Dog eng allocation penalty
dog_eng = sum(p["eng_capacity_pct"] for p in products if p["quadrant"] == "Dog")
if dog_eng > 20:
score -= 0.20
elif dog_eng > 10:
score -= 0.10
# Revenue in dogs penalty
if total_revenue > 0:
dog_rev_pct = sum(p["revenue_quarterly"] for p in products if p["quadrant"] == "Dog") / total_revenue
if dog_rev_pct > 0.30:
score -= 0.15
# Average alignment score
avg_alignment = sum(p["alignment_score"] for p in products) / len(products) if products else 0
score -= (1 - avg_alignment) * 0.20
return max(0.0, min(1.0, score))
def _portfolio_findings(
products: list, total_revenue: float,
quadrant_revenue: dict, quadrant_eng: dict
) -> list:
findings = []
stars = [p for p in products if p["quadrant"] == "Star"]
cows = [p for p in products if p["quadrant"] == "Cash Cow"]
questions = [p for p in products if p["quadrant"] == "Question Mark"]
dogs = [p for p in products if p["quadrant"] == "Dog"]
if not stars:
findings.append("✗ CRITICAL: No Star products. You have no growth engine. Identify a Question Mark to invest in or revisit your market positioning.")
elif len(stars) == 1:
findings.append(f"◑ Single Star ({stars[0]['name']}). Portfolio is fragile — one product drives all growth. Diversify.")
else:
findings.append(f"{len(stars)} Star products — healthy growth engine.")
if not cows:
findings.append("⚠ No Cash Cow products. Stars are consuming capital without a self-funding mechanism. Watch burn rate.")
else:
cow_rev = quadrant_revenue.get("Cash Cow", 0)
cow_pct = cow_rev / total_revenue if total_revenue else 0
findings.append(f"✓ Cash Cow revenue: {cow_pct:.0%} of total — funds Star investment.")
if questions:
findings.append(f"{len(questions)} Question Mark(s): {', '.join(p['name'] for p in questions)}.")
findings.append(" Each needs a binary decision: invest to win share, or exit. Set a 2-quarter deadline.")
if dogs:
dog_eng_total = sum(p["eng_capacity_pct"] for p in dogs)
findings.append(f"{len(dogs)} Dog product(s): {', '.join(p['name'] for p in dogs)} consuming {dog_eng_total}% of eng capacity.")
findings.append(f" That's {dog_eng_total}% of your engineers on declining products. Set sunset dates.")
# Alignment check
misaligned = [p for p in products if p["alignment_score"] < 0.50]
if misaligned:
findings.append(f"⚠ Engineering allocation misaligned on: {', '.join(p['name'] for p in misaligned)}.")
findings.append(" Rebalance: move capacity from Dogs/Cows to Stars.")
return findings
# ---------------------------------------------------------------------------
# Report rendering
# ---------------------------------------------------------------------------
def fmt_currency(n: float) -> str:
if n >= 1_000_000:
return f"${n/1_000_000:.1f}M"
elif n >= 1_000:
return f"${n/1_000:.0f}K"
return f"${n:.0f}"
def render_report(result: dict) -> str:
lines = []
lines.append("=" * 65)
lines.append(f" PORTFOLIO ANALYZER — {result['company']}")
lines.append(f" Total Quarterly Revenue: {fmt_currency(result['total_revenue_quarterly'])}")
if result.get("total_engineering_headcount"):
lines.append(f" Engineering Headcount: {result['total_engineering_headcount']}")
lines.append("=" * 65)
lines.append("")
# Portfolio health
health = result["portfolio_health_score"]
bar_len = 40
filled = round(health * bar_len)
bar = "" * filled + "" * (bar_len - filled)
lines.append(f" Portfolio Health: {health:.0%}")
lines.append(f" [{bar}]")
lines.append("")
# Quadrant summary
lines.append(" QUADRANT SUMMARY")
lines.append(" " + "-" * 55)
header = f" {'Quadrant':<15} {'Count':>5} {'Revenue':>10} {'Rev%':>6} {'Eng%':>6}"
lines.append(header)
lines.append(" " + "-" * 55)
total_rev = result["total_revenue_quarterly"]
for q in ["Star", "Cash Cow", "Question Mark", "Dog"]:
qs = result["quadrant_summary"][q]
emoji = quadrant_emoji(q)
label = f"{emoji} {q}"
rev_pct = f"{qs['revenue_pct']:.0%}" if qs["count"] else "-"
eng = f"{qs['eng_pct']}%" if qs["count"] else "-"
rev = fmt_currency(qs["revenue"]) if qs["count"] else "-"
lines.append(f" {label:<15} {qs['count']:>5} {rev:>10} {rev_pct:>6} {eng:>6}")
lines.append("")
# Per-product breakdown
lines.append(" PRODUCT BREAKDOWN")
lines.append(" " + "-" * 65)
for p in result["products"]:
emoji = quadrant_emoji(p["quadrant"])
pc = posture_color(p["posture"])
lines.append(
f" {emoji} {p['name']}{p['quadrant']}{pc} {p['posture']}"
)
lines.append(
f" Revenue: {fmt_currency(p['revenue_quarterly'])}/qtr "
f"QoQ: {p['qoq_growth']:+.0%} "
f"Mkt growth: {p['market_growth_pct']:+.0f}%"
)
lines.append(
f" Share ratio: {p['share_ratio']:.1f}x "
f"Eng: {p['eng_capacity_pct']}% "
f"Alignment: {p['alignment_score']:.0%}"
)
if p.get("d30_retention") is not None:
lines.append(
f" D30 retention: {p['d30_retention']:.0%} "
f"NPS: {p['nps'] if p['nps'] is not None else 'N/A'}"
)
if p.get("notes"):
lines.append(f" Note: {p['notes']}")
for f in p.get("findings", []):
lines.append(f" {f}")
lines.append("")
# Portfolio-level findings
lines.append(" PORTFOLIO FINDINGS")
lines.append(" " + "-" * 65)
for f in result.get("portfolio_findings", []):
lines.append(f" {f}")
lines.append("")
lines.append("=" * 65)
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description="Portfolio Analyzer — BCG matrix classification and investment recommendations",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--input", "-i",
metavar="FILE",
help="JSON file with portfolio data (default: built-in sample data)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output raw JSON result",
)
args = parser.parse_args()
if args.input:
try:
with open(args.input) as f:
data = json.load(f)
except FileNotFoundError:
print(f"Error: file not found: {args.input}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Error: invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
else:
print("No input file provided — running with sample data.\n")
data = sample_data()
result = analyze_portfolio(data)
if args.json:
# Make result JSON-serializable
def clean(obj):
if isinstance(obj, dict):
return {k: clean(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [clean(v) for v in obj]
elif isinstance(obj, float):
return round(obj, 4)
return obj
print(json.dumps(clean(result), indent=2))
else:
print(render_report(result))
if __name__ == "__main__":
main()