Compare commits
2 commits
34732f0252
...
dc7e21d77c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc7e21d77c | ||
|
|
404e9d4949 |
41 changed files with 2786 additions and 14 deletions
593
agents/venture_discovery.py
Normal file
593
agents/venture_discovery.py
Normal file
|
|
@ -0,0 +1,593 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from itertools import combinations
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from control_plane.events.bus import EventBus
|
||||||
|
from control_plane.ventures.models import CapabilityPriority, CapabilityStatus, CompanyBoardReview, CompanyCapabilityRequirement, CompanyMandate, CompanyProposal, CompanyProposalStatus, EvidenceTier, ICDecision, ICDecisionType, ICDiligence, ICQuestion, ICResponse, OverlapClassification, PortfolioCapabilityGap, PortfolioICReview, VentureArtifact, VentureCapabilityDemand, VentureCohort, VentureCohortMember, VentureCollision, VentureThesis, VentureThesisFingerprint
|
||||||
|
from graph.models import GraphRun, GraphRunStatus
|
||||||
|
from model_router.providers import extract_json_object
|
||||||
|
from model_router.router import ModelCapability, ModelRequestContract, ModelRouter
|
||||||
|
|
||||||
|
|
||||||
|
PITCH_SECTIONS = ["Company name", "One-line thesis", "Problem", "ICP", "Why now", "Product / service", "Business model", "Pricing", "Route to first customer", "Validation plan", "$50 capital allocation proposal", "Time to first dollar", "Path to $500 net cash", "Competition", "Differentiation", "Build requirements", "Distribution requirements", "Risks", "What would falsify the thesis", "Confidence"]
|
||||||
|
SCORE_DIMENSIONS = ["Demand Evidence", "Time-to-First-Dollar Attractiveness", "Capital Efficiency", "Validation Affordability", "Gross Margin Potential", "Distribution Feasibility", "Build Simplicity", "Defensibility", "Market Opportunity", "Competitive Position", "Risk Manageability", "Probability of Reaching $500"]
|
||||||
|
SCORE_DEFINITIONS = {dimension: "100 = highly attractive; 0 = highly unattractive" for dimension in SCORE_DIMENSIONS}
|
||||||
|
EVIDENCE_CEILINGS = {EvidenceTier.TIER_0_THESIS: 35, EvidenceTier.TIER_1_PUBLIC_EVIDENCE: 55, EvidenceTier.TIER_2_CUSTOMER_SIGNAL: 70, EvidenceTier.TIER_3_WILLINGNESS_TO_PAY: 85, EvidenceTier.TIER_4_PAID_CUSTOMER: 95, EvidenceTier.TIER_5_REPEATABLE_TRACTION: 100}
|
||||||
|
|
||||||
|
|
||||||
|
class VentureDiscoveryService:
|
||||||
|
def __init__(self, router: ModelRouter | None = None, bus: EventBus | None = None, web_research_available: bool = False, research_model_hint: str = "luna") -> None:
|
||||||
|
self.router = router
|
||||||
|
self.bus = bus or EventBus()
|
||||||
|
self.web_research_available = web_research_available
|
||||||
|
self.research_model_hint = research_model_hint
|
||||||
|
|
||||||
|
def create_v0_mandate(self) -> CompanyMandate:
|
||||||
|
mandate = CompanyMandate.objects.create(
|
||||||
|
objective="Design a business that could plausibly turn at most $50 of external validation capital into $500 net new cash.",
|
||||||
|
constraints={"external_validation_capital_max": 50, "target_net_new_cash": 500, "target_window_days": 30, "no_equity_raise": True, "no_debt": True, "no_illegal_or_deceptive_activity": True, "no_spam": True, "no_fake_traction": True, "no_fabricated_customer_evidence": True, "no_real_spend_in_v0": True, "no_real_customer_outreach_in_v0": True, "existing_artifex_compute_sunk_available": True},
|
||||||
|
optimization_targets=["time to first dollar", "capital efficiency", "real demand evidence", "high gross margin", "realistic execution", "low external dependency", "ability to validate cheaply"],
|
||||||
|
metadata={"milestone": "VENTURE_DISCOVERY_V0", "spend_authorized": False, "customer_outreach_authorized": False},
|
||||||
|
)
|
||||||
|
self._artifact(None, mandate, "VENTURE_MANDATE", "Venture Discovery V0 Mandate", {"objective": mandate.objective, "constraints": mandate.constraints, "optimization_targets": mandate.optimization_targets}, self._readable_mandate(mandate), "venture_discovery")
|
||||||
|
return mandate
|
||||||
|
|
||||||
|
def generate_single_company(self, mandate: CompanyMandate, *, graph_run=None, ideation_index: int | None = None) -> CompanyProposal:
|
||||||
|
payload, source = self._company_payload(mandate, ideation_index=ideation_index)
|
||||||
|
pitch = self._pitch(payload, fallback=source != "sol")
|
||||||
|
confidence = self._confidence(payload.get("confidence", 0.55))
|
||||||
|
evidence = self._as_list(payload.get("market_evidence", []))
|
||||||
|
if not self.web_research_available:
|
||||||
|
evidence.append({"type": "capability_gap", "source": "internal", "summary": "Public web market research is not configured; demand/competitor evidence is unverified.", "fallback_evidence": True})
|
||||||
|
confidence = min(confidence, 0.58)
|
||||||
|
thesis = VentureThesis.objects.create(mandate=mandate, title=str(payload["title"]), thesis=str(payload["one_line_thesis"]), similarity_fingerprint=self._fingerprint(payload), metadata={"source": source, "exactly_one_company_generated": True}, evidence_tier=EvidenceTier.TIER_0_THESIS)
|
||||||
|
proposal = CompanyProposal.objects.create(
|
||||||
|
mandate=mandate,
|
||||||
|
thesis=thesis,
|
||||||
|
title=str(payload["title"]),
|
||||||
|
description=str(payload["description"]),
|
||||||
|
problem=str(payload["problem"]),
|
||||||
|
target_customer=str(payload["target_customer"]),
|
||||||
|
proposed_solution=str(payload["proposed_solution"]),
|
||||||
|
business_model=str(payload["business_model"]),
|
||||||
|
pricing_hypothesis=str(payload["pricing_hypothesis"]),
|
||||||
|
acquisition_strategy=str(payload["acquisition_strategy"]),
|
||||||
|
validation_plan=str(payload["validation_plan"]),
|
||||||
|
capital_requested=self._money(payload.get("capital_requested", "50")),
|
||||||
|
time_to_first_dollar_estimate=str(payload["time_to_first_dollar_estimate"]),
|
||||||
|
expected_margin=str(payload["expected_margin"]),
|
||||||
|
build_complexity=str(payload["build_complexity"]),
|
||||||
|
market_evidence=evidence,
|
||||||
|
differentiation=str(payload["differentiation"]),
|
||||||
|
major_risks=self._as_list(payload["major_risks"]),
|
||||||
|
confidence=confidence,
|
||||||
|
status=CompanyProposalStatus.SUBMITTED,
|
||||||
|
pitch=pitch,
|
||||||
|
metadata={"generation_source": source, "fallback_evidence": source != "sol", "web_research_available": self.web_research_available, "real_spend": 0, "real_customer_outreach": False},
|
||||||
|
evidence_tier=EvidenceTier.TIER_0_THESIS,
|
||||||
|
)
|
||||||
|
self._artifact(proposal, mandate, "STANDARDIZED_COMPANY_PITCH", "Standardized Company Pitch", pitch, self.readable_pitch(pitch), "Company Brain/Sol" if source == "sol" else "deterministic_fallback", graph_run=graph_run)
|
||||||
|
self.bus.publish("VENTURE_COMPANY_PROPOSED", payload={"proposal_id": str(proposal.id), "source": source})
|
||||||
|
return proposal
|
||||||
|
|
||||||
|
def conduct_market_research(self, proposal: CompanyProposal, *, graph_run=None) -> dict[str, Any]:
|
||||||
|
required = ["competitors", "pricing", "customer_pain", "market_alternatives", "regulatory_platform_risks"]
|
||||||
|
research = {"coverage": {category: False for category in required}, "sources": [], "findings": {}, "unverified_categories": required, "research_available": False}
|
||||||
|
if self.web_research_available and self.router is not None:
|
||||||
|
try:
|
||||||
|
response = self.router.complete(ModelRequestContract(purpose=ModelCapability.REASONING, model_hint=self.research_model_hint, prompt="Bounded public web market research for exactly one startup pitch. Return JSON with keys: sources (list of {url,title,category,summary}), findings (object keyed by competitors, pricing, customer_pain, market_alternatives, regulatory_platform_risks), and coverage (object with each required key true/false). Do not fabricate URLs. If a category has no source, mark coverage false. Pitch: " + json.dumps(proposal.pitch, default=str)))
|
||||||
|
parsed = extract_json_object(response.content)
|
||||||
|
if isinstance(parsed, dict):
|
||||||
|
research = self._normalize_research(parsed, required)
|
||||||
|
research["research_available"] = True
|
||||||
|
research["provider"] = self.research_model_hint
|
||||||
|
except Exception as exc:
|
||||||
|
research["failure"] = str(exc)
|
||||||
|
if not research.get("sources"):
|
||||||
|
research = self._missing_research(required, research.get("failure", "public web research unavailable or returned no source-linked evidence"))
|
||||||
|
source_categories = {str(source.get("category", "")) for source in research.get("sources", []) if source.get("url")}
|
||||||
|
coverage = dict(research.get("coverage", {}))
|
||||||
|
for category in required:
|
||||||
|
coverage[category] = bool(coverage.get(category)) and category in source_categories
|
||||||
|
research["coverage"] = coverage
|
||||||
|
research["unverified_categories"] = [category for category in required if not coverage.get(category)]
|
||||||
|
coverage_ratio = (len(required) - len(research["unverified_categories"])) / len(required)
|
||||||
|
proposal.confidence = round(min(float(proposal.confidence), 0.45 + 0.35 * coverage_ratio), 2)
|
||||||
|
proposal.market_evidence = [*self._as_list(proposal.market_evidence), *research.get("sources", []), {"type": "research_coverage", "source": "venture_research", "summary": f"Source-linked research coverage: {round(coverage_ratio * 100)}%", "coverage": coverage, "unverified_categories": research["unverified_categories"]}]
|
||||||
|
proposal.metadata = {**proposal.metadata, "research": {"coverage_ratio": coverage_ratio, "unverified_categories": research["unverified_categories"], "source_count": len(research.get("sources", [])), "provider": research.get("provider", "none")}}
|
||||||
|
proposal.evidence_tier = EvidenceTier.TIER_1_PUBLIC_EVIDENCE if coverage_ratio > 0 else EvidenceTier.TIER_0_THESIS
|
||||||
|
if proposal.thesis:
|
||||||
|
proposal.thesis.evidence_tier = proposal.evidence_tier
|
||||||
|
proposal.thesis.save(update_fields=["evidence_tier", "updated_at"])
|
||||||
|
proposal.pitch = {**proposal.pitch, "Research evidence": research}
|
||||||
|
proposal.save(update_fields=["confidence", "market_evidence", "metadata", "pitch", "evidence_tier", "updated_at"])
|
||||||
|
self._artifact(proposal, proposal.mandate, "MARKET_RESEARCH", "Bounded Public Market Research", research, self._readable_research(research), "Luna/web research" if research.get("provider") else "research_gap", graph_run=graph_run)
|
||||||
|
return research
|
||||||
|
|
||||||
|
def board_review(self, proposal: CompanyProposal, *, graph_run=None) -> CompanyBoardReview:
|
||||||
|
observations = {
|
||||||
|
"CEO": ["Mandate fit is strongest if validation sells a narrow paid audit before product build.", "Keep the first dollar path service-led, not SaaS-led."],
|
||||||
|
"CTO": ["Build can use existing Artifex analysis, reporting, and frontend capabilities.", "Avoid integrations until willingness-to-pay evidence exists."],
|
||||||
|
"CFO": ["$50 cap is adequate only for lightweight landing page/listing tests, not paid acquisition learning.", "High margin is plausible because delivery is mostly labor/compute already available."],
|
||||||
|
"CRO": ["Founder/operator communities are reachable manually, but V0 cannot contact customers.", "Pricing must start as a paid diagnostic to avoid long SaaS evaluation cycles."],
|
||||||
|
"Independent Director": ["Demand evidence is weak without public research or customer conversations.", "The company must prove urgency before building automation."],
|
||||||
|
}
|
||||||
|
weaknesses = ["No customer outreach or paid test has occurred.", "Public web research is unavailable, so market evidence remains partial.", "First customers may require trust and examples before paying."]
|
||||||
|
revisions = ["Frame offer as a productized validation audit with optional Artifex-assisted build plan.", "Make falsification criteria explicit before spend."]
|
||||||
|
pitch = dict(proposal.pitch)
|
||||||
|
pitch["What would falsify the thesis"] = "Fewer than 5 credible target-customer responses or zero willingness-to-pay signals after a compliant validation test."
|
||||||
|
proposal.pitch = pitch
|
||||||
|
proposal.metadata = {**proposal.metadata, "board_revised_pitch": True}
|
||||||
|
proposal.save(update_fields=["pitch", "metadata", "updated_at"])
|
||||||
|
identity = self.validate_identity_content(proposal, pitch)
|
||||||
|
review = CompanyBoardReview.objects.create(proposal=proposal, observations=observations, strengths=["Service-led revenue path can precede product build.", "Uses current Artifex planning, engineering, and review capabilities.", "Small validation budget aligns with a narrow paid offer."], weaknesses=weaknesses, key_assumptions=["Target customers feel enough urgency to pay for validation clarity.", "Manual outbound or community posting can generate credible responses without spam.", "Artifex can produce a differentiated audit faster than generic consultants."], required_revisions=revisions, recommendation="PROCEED_TO_IC_WITH_REVISIONS", revised_pitch=pitch, metadata={"roles": list(observations), "company_does_not_grade_itself": True, "identity_validation": identity})
|
||||||
|
self._artifact(proposal, proposal.mandate, "COMPANY_BOARD_REVIEW", "Company Board Review", {"observations": observations, "strengths": review.strengths, "weaknesses": weaknesses, "required_revisions": revisions, "recommendation": review.recommendation, "revised_pitch": pitch, "identity_validation": identity}, self._readable_board(review), "Company Board", graph_run=graph_run)
|
||||||
|
return review
|
||||||
|
|
||||||
|
def start_ic_diligence(self, proposal: CompanyProposal, *, graph_run=None) -> ICDiligence:
|
||||||
|
proposal.status = CompanyProposalStatus.UNDER_DILIGENCE
|
||||||
|
proposal.save(update_fields=["status", "updated_at"])
|
||||||
|
diligence = ICDiligence.objects.create(proposal=proposal, status="FIRST_PASS", rounds=["Initial Pitch", "Diligence Round 1", "Final Challenge", "Decision"], metadata={"bounded_rounds": True, "self_grading": False})
|
||||||
|
self._artifact(proposal, proposal.mandate, "IC_FIRST_PASS", "IC First-Pass Review", {"status": diligence.status, "initial_concerns": ["Demand evidence is unverified.", "Distribution assumptions need evidence.", "Need validation gate before any spend."]}, "IC first pass: proceed to evidence-seeking questions; do not score final decision yet.", "Independent IC", graph_run=graph_run)
|
||||||
|
return diligence
|
||||||
|
|
||||||
|
def generate_ic_questions(self, diligence: ICDiligence, *, graph_run=None) -> list[ICQuestion]:
|
||||||
|
pitch = diligence.proposal.pitch
|
||||||
|
raw_questions = self._questions_from_pitch(pitch)
|
||||||
|
questions = [ICQuestion.objects.create(diligence=diligence, question=item["question"], category=item["category"], evidence_required=True) for item in raw_questions[:8]]
|
||||||
|
self._artifact(diligence.proposal, diligence.proposal.mandate, "IC_QUESTIONS", "IC Diligence Questions", {"questions": [q.question for q in questions]}, "\n".join(f"- {q.question}" for q in questions), "Independent IC", graph_run=graph_run)
|
||||||
|
return questions
|
||||||
|
|
||||||
|
def answer_questions(self, diligence: ICDiligence, *, graph_run=None) -> list[ICResponse]:
|
||||||
|
responses = []
|
||||||
|
for question in diligence.questions.all():
|
||||||
|
answer = self._answer_question(question)
|
||||||
|
responses.append(ICResponse.objects.create(question=question, answer=answer["answer"], evidence=answer["evidence"], uncertainty=answer["uncertainty"], pitch_changes=answer.get("pitch_changes", {}), metadata={"no_customer_outreach": True, "no_spend": True}))
|
||||||
|
diligence.status = "COMPANY_RESPONSE"
|
||||||
|
diligence.save(update_fields=["status", "updated_at"])
|
||||||
|
identity = self.validate_identity_content(diligence.proposal, {"responses": [r.answer for r in responses]})
|
||||||
|
self._artifact(diligence.proposal, diligence.proposal.mandate, "IC_RESPONSES", "Company Responses to IC", {"responses": [{"question": r.question.question, "answer": r.answer, "evidence": r.evidence, "uncertainty": r.uncertainty, "pitch_changes": r.pitch_changes} for r in responses], "identity_validation": identity}, self._readable_responses(responses), "Company reasoning roles", graph_run=graph_run)
|
||||||
|
return responses
|
||||||
|
|
||||||
|
def red_team(self, diligence: ICDiligence, *, graph_run=None) -> dict[str, object]:
|
||||||
|
challenge = {"concerns": ["The offer may be perceived as generic consulting unless anchored to a painful, immediate decision.", "Without public research or customer contact, demand remains a hypothesis.", "Manual distribution could fail if the target audience distrusts AI-generated audits."], "required_final_response": ["Narrow ICP further.", "Specify willingness-to-pay proof.", "Define hard kill criteria."], "recommendation": "continue_to_final_response"}
|
||||||
|
diligence.red_team_challenge = challenge
|
||||||
|
diligence.status = "FINAL_CHALLENGE"
|
||||||
|
diligence.save(update_fields=["red_team_challenge", "status", "updated_at"])
|
||||||
|
challenge["identity_validation"] = self.validate_identity_content(diligence.proposal, challenge)
|
||||||
|
self._artifact(diligence.proposal, diligence.proposal.mandate, "IC_RED_TEAM", "IC Red-Team Challenge", challenge, "Red-team concerns:\n" + "\n".join(f"- {c}" for c in challenge["concerns"]), "Independent IC Red Team", graph_run=graph_run)
|
||||||
|
return challenge
|
||||||
|
|
||||||
|
def final_company_response(self, diligence: ICDiligence, *, graph_run=None) -> dict[str, object]:
|
||||||
|
response = {"narrowed_icp": diligence.proposal.target_customer, "revised_validation_gate": "Before any spend, collect 5 credible target-customer responses or 1 explicit willingness-to-pay signal through compliant non-spam channels.", "kill_criteria": ["No credible responses after 10 targeted, compliant conversations/posts once outreach is approved.", "No willingness-to-pay signal at $49-$99.", "Customers only want free advice, not a paid report."], "pitch_changes": {"ICP": "Preserve canonical proposal ICP.", "Validation plan": "Gate spend behind credible response/willingness-to-pay evidence."}}
|
||||||
|
response["identity_validation"] = self.validate_identity_content(diligence.proposal, response)
|
||||||
|
diligence.final_response = response
|
||||||
|
diligence.status = "FINAL_RESPONSE"
|
||||||
|
diligence.save(update_fields=["final_response", "status", "updated_at"])
|
||||||
|
self._artifact(diligence.proposal, diligence.proposal.mandate, "IC_FINAL_RESPONSE", "Final Company Response", response, json.dumps(response, indent=2), "Company reasoning roles", graph_run=graph_run)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def score_and_decide(self, diligence: ICDiligence, *, graph_run=None) -> ICDecision:
|
||||||
|
scores = self._evidence_scores(diligence.proposal)
|
||||||
|
composite = round(sum(scores.values()) / len(scores), 1)
|
||||||
|
calibration = self.calibrate_probability(diligence.proposal, raw_probability=float(scores["Probability of Reaching $500"]))
|
||||||
|
scores["Probability of Reaching $500"] = int(calibration["evidence_adjusted_probability"])
|
||||||
|
composite = round(sum(scores.values()) / len(scores), 1)
|
||||||
|
decision_type = ICDecisionType.REVISE_AND_RESUBMIT if scores["Demand Evidence"] < 35 or scores["Risk Manageability"] < 35 else ICDecisionType.CONDITIONAL_FUND if composite >= 55 else ICDecisionType.REVISE_AND_RESUBMIT
|
||||||
|
tranche = Decimal("20.00") if decision_type == ICDecisionType.CONDITIONAL_FUND and composite >= 70 else Decimal("10.00") if decision_type == ICDecisionType.CONDITIONAL_FUND else None
|
||||||
|
decision = ICDecision.objects.create(diligence=diligence, decision=decision_type, component_scores=scores, composite_score=composite, probability_500_within_30_days=scores["Probability of Reaching $500"], initial_tranche=tranche, validation_condition="Obtain 5 credible target-customer responses or 1 explicit willingness-to-pay signal before any build or further spend.", evidence_required=["response transcripts or public thread URLs", "proof of willingness-to-pay signal", "no-spam/no-fabrication compliance note"], recommended_allocation={"initial_tranche": float(tranche or 0), "remaining_reserved": 50 - float(tranche or 0), "no_spend_in_v0": True}, kill_criteria=diligence.final_response.get("kill_criteria", []), next_decision_point="After validation evidence is collected and before any real spend or customer delivery.", metadata={"decision_vocabulary": [item.value for item in ICDecisionType], "no_actual_funding": True, "score_basis": "source_linked_research_and_pitch_attributes", "probability_calibration": calibration}, evidence_tier=diligence.proposal.evidence_tier, raw_probability_500_within_30_days=calibration["raw_probability"], evidence_ceiling=calibration["evidence_ceiling"], probability_explanation=calibration["explanation"], score_definitions=SCORE_DEFINITIONS)
|
||||||
|
diligence.status = "DECISION"
|
||||||
|
diligence.save(update_fields=["status", "updated_at"])
|
||||||
|
diligence.proposal.status = CompanyProposalStatus.FUNDED_RECOMMENDED if decision.decision == ICDecisionType.CONDITIONAL_FUND else CompanyProposalStatus.REVISE
|
||||||
|
diligence.proposal.save(update_fields=["status", "updated_at"])
|
||||||
|
self._artifact(diligence.proposal, diligence.proposal.mandate, "IC_FINAL_SCORE", "IC Final Scoring and Decision", {"scores": scores, "score_definitions": SCORE_DEFINITIONS, "decision": decision.decision, "composite_score": composite, "conditional_funding": {"initial_tranche": str(decision.initial_tranche), "condition": decision.validation_condition}, "score_basis": decision.metadata["score_basis"], "probability_calibration": calibration}, self._readable_score(decision), "Independent IC", graph_run=graph_run)
|
||||||
|
return decision
|
||||||
|
|
||||||
|
def capability_analysis(self, proposal: CompanyProposal, *, graph_run=None) -> PortfolioCapabilityGap:
|
||||||
|
requirements = self._capability_requirements(proposal)
|
||||||
|
for item in requirements:
|
||||||
|
CompanyCapabilityRequirement.objects.create(proposal=proposal, **item)
|
||||||
|
available = [r["category"] for r in requirements if r["status"] == CapabilityStatus.AVAILABLE]
|
||||||
|
partial = [r["category"] for r in requirements if r["status"] == CapabilityStatus.PARTIAL]
|
||||||
|
missing = [r["category"] for r in requirements if r["status"] == CapabilityStatus.MISSING]
|
||||||
|
ranked = [{"category": r["category"], "priority": r["priority"], "rationale": r["rationale"]} for r in requirements if r["status"] == CapabilityStatus.MISSING]
|
||||||
|
priority_order = {CapabilityPriority.BEFORE_VALIDATION: 0, CapabilityPriority.BEFORE_FIRST_CUSTOMER: 1, CapabilityPriority.BEFORE_SCALING: 2}
|
||||||
|
ranked.sort(key=lambda item: priority_order[item["priority"]])
|
||||||
|
report = self._readable_capability_gap(available, partial, missing, ranked)
|
||||||
|
web_requirement = next((item for item in requirements if item["category"] == "WEB_MARKET_RESEARCH"), None)
|
||||||
|
gap = PortfolioCapabilityGap.objects.create(proposal=proposal, available=available, partial=partial, missing=missing, ranked_missing=ranked, report=report, metadata={"web_market_research_status": web_requirement["status"] if web_requirement else "MISSING"})
|
||||||
|
self._artifact(proposal, proposal.mandate, "CAPABILITY_GAP_REPORT", "Capability Gap Report", {"available": available, "partial": partial, "missing": missing, "ranked_missing": ranked}, report, "Venture Discovery Capability Analysis", graph_run=graph_run)
|
||||||
|
return gap
|
||||||
|
|
||||||
|
def produce_investment_memo(self, diligence: ICDiligence, gap: PortfolioCapabilityGap, *, graph_run=None) -> VentureArtifact:
|
||||||
|
decision = diligence.decision
|
||||||
|
proposal = diligence.proposal
|
||||||
|
memo = {"Company": proposal.title, "Thesis": proposal.pitch["One-line thesis"], "Mandate": proposal.mandate.objective, "Requested capital": str(proposal.capital_requested), "Recommended allocation": decision.recommended_allocation, "IC decision": decision.decision, "Key metrics": {"P($500 within 30 days)": decision.probability_500_within_30_days, "raw P($500 within 30 days)": decision.raw_probability_500_within_30_days, "evidence ceiling": decision.evidence_ceiling, "evidence tier": decision.evidence_tier, "estimated time to first dollar": proposal.time_to_first_dollar_estimate, "expected gross margin": proposal.expected_margin, "validation cost": "$10-$20 initial tranche; $50 maximum after approval", "build effort": proposal.build_complexity, "distribution feasibility": decision.component_scores["Distribution Feasibility"]}, "Research evidence": proposal.pitch.get("Research evidence", {}), "Why it may work": ["Revenue path starts with a paid diagnostic, not a full SaaS build.", "Artifex has planning, engineering, frontend, review, graph, and agent-control capabilities already.", "Validation budget can be gated behind evidence."], "Why it may fail": diligence.red_team_challenge.get("concerns", []), "Diligence questions": [q.question for q in diligence.questions.all()], "Company responses": [r.answer for r in ICResponse.objects.filter(question__diligence=diligence)], "Red-team concerns": diligence.red_team_challenge.get("concerns", []), "IC scoring": decision.component_scores, "Score definitions": decision.score_definitions, "Capital recommendation": decision.recommended_allocation, "Validation gates": [decision.validation_condition], "Kill criteria": decision.kill_criteria, "Next decision point": decision.next_decision_point, "Capability gap": {"available": gap.available, "partial": gap.partial, "missing": gap.missing}}
|
||||||
|
identity = self.validate_identity_content(proposal, memo)
|
||||||
|
memo["Identity validation"] = identity
|
||||||
|
return self._artifact(proposal, proposal.mandate, "FINAL_INVESTMENT_MEMO", "Final IC Investment Memo", memo, self._readable_memo(memo), "Independent IC", graph_run=graph_run)
|
||||||
|
|
||||||
|
def request_human_approval(self, decision: ICDecision, action: str) -> ICDecision:
|
||||||
|
if action not in {"approve_for_validation", "reject", "request_more_diligence"}:
|
||||||
|
raise ValueError("Unsupported venture approval action")
|
||||||
|
decision.metadata = {**decision.metadata, "human_approval_action": action, "real_spend_still_blocked": True}
|
||||||
|
decision.save(update_fields=["metadata", "updated_at"])
|
||||||
|
return decision
|
||||||
|
|
||||||
|
def prepare_cohort(self, *, size: int = 10, graph_run=None, concurrency: int = 1) -> VentureCohort:
|
||||||
|
mandate = self.create_v0_mandate()
|
||||||
|
cohort_id = f"VDV02-{timezone.now().strftime('%Y%m%d%H%M%S')}-{hashlib.sha1(str(mandate.id).encode()).hexdigest()[:8]}"
|
||||||
|
return VentureCohort.objects.create(cohort_id=cohort_id, mandate=mandate, cohort_size=size, graph_run=graph_run, concurrency=concurrency, status="PREPARING", graph_versions={"cohort": "venture_discovery_cohort v1", "company": "venture_discovery v1"}, research_policy={"stage_a": "lightweight_all", "deeper_research": "top_5_if_required", "no_spend": True, "no_customer_outreach": True}, scoring_policy={"dimensions": SCORE_DEFINITIONS}, evidence_calibration_policy={tier: ceiling for tier, ceiling in EVIDENCE_CEILINGS.items()}, metadata={"real_spend": 0, "real_customer_outreach": False})
|
||||||
|
|
||||||
|
def generate_independent_proposals(self, cohort: VentureCohort) -> list[CompanyProposal]:
|
||||||
|
proposals = []
|
||||||
|
for index in range(cohort.cohort_size):
|
||||||
|
proposal = self.generate_single_company(cohort.mandate, ideation_index=index + 1)
|
||||||
|
proposal.metadata = {**proposal.metadata, "cohort_id": cohort.cohort_id, "independent_generation_index": index + 1, "prior_ideas_visible": False}
|
||||||
|
proposal.save(update_fields=["metadata", "updated_at"])
|
||||||
|
VentureCohortMember.objects.create(cohort=cohort, proposal=proposal, metadata={"generation_index": index + 1})
|
||||||
|
proposals.append(proposal)
|
||||||
|
cohort.status = "PROPOSALS_GENERATED"
|
||||||
|
cohort.save(update_fields=["status", "updated_at"])
|
||||||
|
return proposals
|
||||||
|
|
||||||
|
def research_cohort(self, cohort: VentureCohort) -> None:
|
||||||
|
started = time.monotonic()
|
||||||
|
sources = 0
|
||||||
|
for member in cohort.members.select_related("proposal").order_by("created_at"):
|
||||||
|
research = self.conduct_market_research(member.proposal)
|
||||||
|
sources += len(research.get("sources", []))
|
||||||
|
cohort.metrics = {**cohort.metrics, "research_runtime_seconds": round(time.monotonic() - started, 2), "total_sources": sources, "public_research_queries": cohort.members.count(), "peak_concurrency": cohort.concurrency}
|
||||||
|
cohort.status = "RESEARCHED"
|
||||||
|
cohort.save(update_fields=["metrics", "status", "updated_at"])
|
||||||
|
|
||||||
|
def fingerprint_cohort(self, cohort: VentureCohort) -> list[VentureThesisFingerprint]:
|
||||||
|
return [self.fingerprint_proposal(member.proposal) for member in cohort.members.select_related("proposal")]
|
||||||
|
|
||||||
|
def analyze_collisions(self, cohort: VentureCohort) -> list[VentureCollision]:
|
||||||
|
collisions = []
|
||||||
|
proposals = [member.proposal for member in cohort.members.select_related("proposal")]
|
||||||
|
for a, b in combinations(proposals, 2):
|
||||||
|
result = self.classify_overlap(a, b)
|
||||||
|
collisions.append(VentureCollision.objects.create(cohort=cohort, company_a=a, company_b=b, classification=result["classification"], similarity_score=result["similarity_score"], explanation=result["explanation"], overlapping_dimensions=result["overlapping_dimensions"]))
|
||||||
|
return collisions
|
||||||
|
|
||||||
|
def run_individual_diligence_for_cohort(self, cohort: VentureCohort) -> None:
|
||||||
|
started = time.monotonic()
|
||||||
|
for member in cohort.members.select_related("proposal"):
|
||||||
|
proposal = member.proposal
|
||||||
|
self.board_review(proposal)
|
||||||
|
diligence = self.start_ic_diligence(proposal)
|
||||||
|
self.generate_ic_questions(diligence)
|
||||||
|
self.answer_questions(diligence)
|
||||||
|
self.red_team(diligence)
|
||||||
|
self.final_company_response(diligence)
|
||||||
|
self.score_and_decide(diligence)
|
||||||
|
gap = self.capability_analysis(proposal)
|
||||||
|
self.produce_investment_memo(diligence, gap)
|
||||||
|
member.child_graph_run = GraphRun.objects.create(execution_graph_version=cohort.graph_run.execution_graph_version if cohort.graph_run else None, status=GraphRunStatus.COMPLETE, metadata={"logical_child_company_run": True, "proposal_id": str(proposal.id), "cohort_id": cohort.cohort_id}) if cohort.graph_run else None
|
||||||
|
member.save(update_fields=["child_graph_run", "updated_at"])
|
||||||
|
cohort.metrics = {**cohort.metrics, "individual_diligence_runtime_seconds": round(time.monotonic() - started, 2)}
|
||||||
|
cohort.status = "INDIVIDUAL_DILIGENCE_COMPLETE"
|
||||||
|
cohort.save(update_fields=["metrics", "status", "updated_at"])
|
||||||
|
|
||||||
|
def portfolio_ic(self, cohort: VentureCohort) -> PortfolioICReview:
|
||||||
|
rows = []
|
||||||
|
collision_risk = defaultdict(int)
|
||||||
|
for collision in cohort.collisions.exclude(classification=OverlapClassification.NONE):
|
||||||
|
collision_risk[str(collision.company_a_id)] += 1
|
||||||
|
collision_risk[str(collision.company_b_id)] += 1
|
||||||
|
for member in cohort.members.select_related("proposal"):
|
||||||
|
proposal = member.proposal
|
||||||
|
decision = proposal.ic_diligence.order_by("-created_at").first().decision
|
||||||
|
capability_burden = proposal.capability_requirements.filter(status=CapabilityStatus.MISSING).count()
|
||||||
|
score = round(decision.composite_score + decision.probability_500_within_30_days * 0.2 - capability_burden * 1.5 - collision_risk[str(proposal.id)] * 2, 1)
|
||||||
|
rows.append({"proposal_id": str(proposal.id), "company": proposal.title, "thesis": proposal.pitch.get("One-line thesis", proposal.description), "ic_score": decision.composite_score, "probability": decision.probability_500_within_30_days, "decision": decision.decision, "evidence_tier": decision.evidence_tier, "initial_tranche": str(decision.initial_tranche or "0"), "portfolio_score": score, "capability_burden": capability_burden, "collision_risk": collision_risk[str(proposal.id)]})
|
||||||
|
rows.sort(key=lambda item: item["portfolio_score"], reverse=True)
|
||||||
|
for rank, row in enumerate(rows, start=1):
|
||||||
|
member = cohort.members.get(proposal_id=row["proposal_id"])
|
||||||
|
member.rank = rank
|
||||||
|
member.is_top_3 = rank <= 3
|
||||||
|
member.portfolio_score = row["portfolio_score"]
|
||||||
|
member.save(update_fields=["rank", "is_top_3", "portfolio_score", "updated_at"])
|
||||||
|
row["rank"] = rank
|
||||||
|
concentration = self._portfolio_concentration(cohort)
|
||||||
|
review, _ = PortfolioICReview.objects.update_or_create(cohort=cohort, defaults={"rankings": rows, "top_3": rows[:3], "concentration": concentration, "metadata": {"no_funding": True}})
|
||||||
|
cohort.status = "PORTFOLIO_IC_COMPLETE"
|
||||||
|
cohort.save(update_fields=["status", "updated_at"])
|
||||||
|
return review
|
||||||
|
|
||||||
|
def aggregate_capability_demand(self, cohort: VentureCohort, *, top_3_only: bool = False) -> list[dict[str, Any]]:
|
||||||
|
members = cohort.members.filter(is_top_3=True) if top_3_only else cohort.members.all()
|
||||||
|
proposals = [member.proposal for member in members.select_related("proposal")]
|
||||||
|
by_capability: dict[str, list[CompanyCapabilityRequirement]] = defaultdict(list)
|
||||||
|
for proposal in proposals:
|
||||||
|
for req in proposal.capability_requirements.all():
|
||||||
|
by_capability[req.category].append(req)
|
||||||
|
stage_order = {CapabilityPriority.BEFORE_VALIDATION: 0, CapabilityPriority.BEFORE_FIRST_CUSTOMER: 1, CapabilityPriority.BEFORE_SCALING: 2}
|
||||||
|
results = []
|
||||||
|
for capability, reqs in by_capability.items():
|
||||||
|
earliest = sorted([req.priority for req in reqs], key=lambda item: stage_order[item])[0]
|
||||||
|
statuses = Counter(req.status for req in reqs)
|
||||||
|
top_3_count = sum(1 for req in reqs if req.proposal.cohort_memberships.filter(cohort=cohort, is_top_3=True).exists())
|
||||||
|
priority_score = len(reqs) * 10 + top_3_count * 8 + (12 if earliest == CapabilityPriority.BEFORE_VALIDATION else 6 if earliest == CapabilityPriority.BEFORE_FIRST_CUSTOMER else 2) + statuses.get(CapabilityStatus.MISSING, 0) * 4
|
||||||
|
row = {"capability": capability, "count": len(reqs), "percentage": round((len(reqs) / max(1, len(proposals))) * 100, 1), "earliest_stage": earliest, "companies": [req.proposal.title for req in reqs], "status_distribution": dict(statuses), "top_3_count": top_3_count, "priority_score": priority_score}
|
||||||
|
if not top_3_only:
|
||||||
|
VentureCapabilityDemand.objects.update_or_create(cohort=cohort, capability=capability, defaults={k: v for k, v in row.items() if k != "capability"})
|
||||||
|
results.append(row)
|
||||||
|
results.sort(key=lambda item: item["priority_score"], reverse=True)
|
||||||
|
if hasattr(cohort, "portfolio_review"):
|
||||||
|
review = cohort.portfolio_review
|
||||||
|
if top_3_only:
|
||||||
|
review.top_3_capability_gaps = results[:10]
|
||||||
|
else:
|
||||||
|
review.capability_demand = results
|
||||||
|
review.recommended_build_priorities = [item["capability"] for item in results if CapabilityStatus.MISSING in item["status_distribution"]][:5]
|
||||||
|
review.save(update_fields=["capability_demand", "top_3_capability_gaps", "recommended_build_priorities", "updated_at"])
|
||||||
|
return results
|
||||||
|
|
||||||
|
def produce_cohort_report(self, cohort: VentureCohort) -> VentureArtifact:
|
||||||
|
review = cohort.portfolio_review
|
||||||
|
content = {"cohort_id": cohort.cohort_id, "mandate": cohort.mandate.objective, "runtime": cohort.metrics, "total_spend": 0, "customer_outreach": "none", "rankings": review.rankings, "top_3": review.top_3, "collisions": self._collision_summary(cohort), "portfolio_concentration": review.concentration, "capability_demand": review.capability_demand, "top_3_capability_gaps": review.top_3_capability_gaps, "recommended_build_priorities": review.recommended_build_priorities}
|
||||||
|
readable = self._readable_cohort_report(content)
|
||||||
|
cohort.status = "COMPLETE"
|
||||||
|
cohort.save(update_fields=["status", "updated_at"])
|
||||||
|
return VentureArtifact.objects.create(mandate=cohort.mandate, graph_run=cohort.graph_run, artifact_type="VENTURE_DISCOVERY_COHORT_REPORT", name="Venture Discovery Cohort Report", content=content, readable=readable, generated_by="Portfolio IC")
|
||||||
|
|
||||||
|
def calibrate_probability(self, proposal: CompanyProposal, *, raw_probability: float) -> dict[str, Any]:
|
||||||
|
tier = proposal.evidence_tier or EvidenceTier.TIER_0_THESIS
|
||||||
|
ceiling = float(EVIDENCE_CEILINGS.get(tier, 35))
|
||||||
|
adjusted = min(float(raw_probability), ceiling)
|
||||||
|
return {"raw_probability": float(raw_probability), "evidence_adjusted_probability": adjusted, "evidence_ceiling": ceiling, "evidence_tier": tier, "explanation": f"{tier} caps P($500/30d) at {ceiling}%; IC uses {adjusted}%."}
|
||||||
|
|
||||||
|
def validate_identity_content(self, proposal: CompanyProposal, content: Any) -> dict[str, Any]:
|
||||||
|
text = json.dumps(content, default=str).lower()
|
||||||
|
checks = {
|
||||||
|
"company_identity": self._token_overlap(proposal.title, text) > 0,
|
||||||
|
"icp": self._token_overlap(proposal.target_customer, text) >= 1,
|
||||||
|
"problem": self._token_overlap(proposal.problem, text) >= 1,
|
||||||
|
"offer": self._token_overlap(proposal.proposed_solution, text) >= 1,
|
||||||
|
"business_model": self._token_overlap(proposal.business_model, text) >= 1,
|
||||||
|
}
|
||||||
|
passed = sum(1 for value in checks.values() if value) >= 3 and checks["company_identity"]
|
||||||
|
result = {"passed": passed, "checks": checks, "warning": "" if passed else "identity_consistency_warning"}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def fingerprint_proposal(self, proposal: CompanyProposal) -> VentureThesisFingerprint:
|
||||||
|
existing = getattr(proposal, "fingerprint", None)
|
||||||
|
data = self._fingerprint_data(proposal)
|
||||||
|
digest = hashlib.sha256(json.dumps(data, sort_keys=True).encode()).hexdigest()
|
||||||
|
fields = {**data, "fingerprint_hash": digest, "metadata": {"deterministic": True}}
|
||||||
|
if existing:
|
||||||
|
for key, value in fields.items():
|
||||||
|
setattr(existing, key, value)
|
||||||
|
existing.save(update_fields=[*fields.keys(), "updated_at"])
|
||||||
|
return existing
|
||||||
|
return VentureThesisFingerprint.objects.create(proposal=proposal, **fields)
|
||||||
|
|
||||||
|
def classify_overlap(self, a: CompanyProposal, b: CompanyProposal) -> dict[str, Any]:
|
||||||
|
fa = self.fingerprint_proposal(a)
|
||||||
|
fb = self.fingerprint_proposal(b)
|
||||||
|
dimensions = ["industry", "business_model", "primary_distribution_channel", "price_band", "time_to_first_cash_band", "service_software_hybrid", "regulatory_dependency"]
|
||||||
|
overlap = [dim for dim in dimensions if getattr(fa, dim) and getattr(fa, dim) == getattr(fb, dim)]
|
||||||
|
text_score = self._jaccard(f"{fa.icp} {fa.problem} {fa.offer}", f"{fb.icp} {fb.problem} {fb.offer}")
|
||||||
|
score = round((len(overlap) / len(dimensions)) * 0.55 + text_score * 0.45, 2)
|
||||||
|
if score >= 0.82:
|
||||||
|
classification = OverlapClassification.DUPLICATE
|
||||||
|
elif score >= 0.62:
|
||||||
|
classification = OverlapClassification.NEAR_DUPLICATE
|
||||||
|
elif getattr(fa, "industry") == getattr(fb, "industry") and getattr(fa, "icp") == getattr(fb, "icp"):
|
||||||
|
classification = OverlapClassification.COMPETITIVE
|
||||||
|
elif getattr(fa, "industry") == getattr(fb, "industry") or getattr(fa, "business_model") == getattr(fb, "business_model"):
|
||||||
|
classification = OverlapClassification.ADJACENT
|
||||||
|
else:
|
||||||
|
classification = OverlapClassification.NONE
|
||||||
|
return {"classification": classification, "similarity_score": score, "overlapping_dimensions": overlap, "explanation": f"Overlap {overlap}; text similarity {text_score:.2f}."}
|
||||||
|
|
||||||
|
def _company_payload(self, mandate: CompanyMandate, *, ideation_index: int | None = None) -> tuple[dict[str, Any], str]:
|
||||||
|
if self.router is not None:
|
||||||
|
try:
|
||||||
|
slot = f" Independent cohort slot: {ideation_index}. Do not use or imitate other cohort ideas; no other ideas are visible." if ideation_index else ""
|
||||||
|
response = self.router.complete(ModelRequestContract(purpose=ModelCapability.PLANNING, model_hint="sol", prompt="Generate exactly ONE startup idea for Venture Discovery V0. Return a single JSON object, not a list. Respect no spend and no outreach in V0. Include title, one_line_thesis, description, problem, target_customer, proposed_solution, business_model, pricing_hypothesis, acquisition_strategy, validation_plan, capital_requested, time_to_first_dollar_estimate, expected_margin, build_complexity, market_evidence, differentiation, major_risks, confidence." + slot + " Mandate: " + json.dumps({"objective": mandate.objective, "constraints": mandate.constraints, "optimization_targets": mandate.optimization_targets})))
|
||||||
|
parsed = extract_json_object(response.content)
|
||||||
|
if isinstance(parsed, dict) and parsed.get("title"):
|
||||||
|
return self._normalize_payload(parsed), "sol"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
payload = self._fallback_company_payload()
|
||||||
|
payload["market_evidence"] = [{"type": "fallback_hypothesis", "source": "deterministic_fallback", "summary": "No Sol/web research was used; this is an internal hypothesis for test/resilience only.", "fallback_evidence": True}]
|
||||||
|
payload["confidence"] = 0.52
|
||||||
|
return payload, "deterministic_fallback"
|
||||||
|
|
||||||
|
def _fallback_company_payload(self) -> dict[str, Any]:
|
||||||
|
return {"title": "LaunchLens", "one_line_thesis": "A productized validation audit helps solo builders decide whether an AI-enabled microbusiness is worth pursuing before they spend weeks building.", "description": "LaunchLens sells a concise validation and launch-readiness report for one microbusiness idea.", "problem": "Solo technical founders often overbuild AI products before proving willingness to pay.", "target_customer": "Solo technical founders and small service operators considering an AI-assisted microbusiness.", "proposed_solution": "A fixed-scope paid audit that evaluates ICP, first-dollar route, validation gates, build plan, risks, and Artifex capability gaps.", "business_model": "Productized service first, with optional later software tooling if demand is proven.", "pricing_hypothesis": "$49-$99 per audit, with a higher-touch $250 implementation planning upsell after validation.", "acquisition_strategy": "Compliant founder-community posts, personal network asks, and content showing anonymized example audits after outreach is approved.", "validation_plan": "Before any build, seek 5 credible target-customer responses or 1 willingness-to-pay signal through compliant channels once approved.", "capital_requested": "50", "time_to_first_dollar_estimate": "3-10 days after outreach is approved", "expected_margin": "70-85% gross margin after manual delivery time", "build_complexity": "LOW", "market_evidence": [], "differentiation": "Combines venture IC-style diligence with Artifex's software/agent execution awareness and explicit capability-gap reporting.", "major_risks": ["Demand may be consulting-like and hard to differentiate.", "Manual distribution may not produce urgent buyers.", "No V0 customer evidence exists yet."], "confidence": 0.52}
|
||||||
|
|
||||||
|
def _normalize_payload(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
fallback = self._fallback_company_payload()
|
||||||
|
normalized = {key: payload.get(key, value) for key, value in fallback.items()}
|
||||||
|
normalized["market_evidence"] = self._as_list(normalized.get("market_evidence"))
|
||||||
|
normalized["major_risks"] = self._as_list(normalized.get("major_risks"))
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
def _normalize_research(self, payload: dict[str, Any], required: list[str]) -> dict[str, Any]:
|
||||||
|
sources = []
|
||||||
|
for source in self._as_list(payload.get("sources"))[:12]:
|
||||||
|
if isinstance(source, dict) and source.get("url"):
|
||||||
|
sources.append({"type": "public_web", "url": str(source["url"]), "title": str(source.get("title", "")), "category": str(source.get("category", "")), "summary": str(source.get("summary", "")), "fallback_evidence": False})
|
||||||
|
coverage = {category: bool(dict(payload.get("coverage", {})).get(category)) for category in required}
|
||||||
|
return {"coverage": coverage, "sources": sources, "findings": dict(payload.get("findings", {})), "unverified_categories": []}
|
||||||
|
|
||||||
|
def _missing_research(self, required: list[str], reason: str) -> dict[str, Any]:
|
||||||
|
return {"coverage": {category: False for category in required}, "sources": [], "findings": {}, "unverified_categories": required, "research_available": False, "failure": reason}
|
||||||
|
|
||||||
|
def _as_list(self, value: Any) -> list[Any]:
|
||||||
|
if isinstance(value, list):
|
||||||
|
return value
|
||||||
|
if value in (None, ""):
|
||||||
|
return []
|
||||||
|
return [value]
|
||||||
|
|
||||||
|
def _money(self, value: Any) -> Decimal:
|
||||||
|
match = re.search(r"\d+(?:\.\d+)?", str(value))
|
||||||
|
if match is None:
|
||||||
|
return Decimal("50")
|
||||||
|
return min(Decimal(match.group(0)), Decimal("50"))
|
||||||
|
|
||||||
|
def _token_overlap(self, source: str, target_text: str) -> int:
|
||||||
|
tokens = {token for token in re.findall(r"[a-z0-9]{4,}", source.lower()) if token not in {"with", "that", "from", "this", "service", "business", "model", "customer", "customers"}}
|
||||||
|
return len([token for token in tokens if token in target_text])
|
||||||
|
|
||||||
|
def _jaccard(self, a: str, b: str) -> float:
|
||||||
|
left = {token for token in re.findall(r"[a-z0-9]{4,}", a.lower())}
|
||||||
|
right = {token for token in re.findall(r"[a-z0-9]{4,}", b.lower())}
|
||||||
|
if not left or not right:
|
||||||
|
return 0.0
|
||||||
|
return len(left & right) / len(left | right)
|
||||||
|
|
||||||
|
def _fingerprint_data(self, proposal: CompanyProposal) -> dict[str, Any]:
|
||||||
|
text = " ".join([proposal.title, proposal.description, proposal.problem, proposal.target_customer, proposal.proposed_solution, proposal.business_model, proposal.acquisition_strategy]).lower()
|
||||||
|
industry = "shopify/ecommerce" if "shopify" in text or "ecommerce" in text else "developer tools" if "developer" in text or "api" in text else "b2b services" if "b2b" in text else "general business"
|
||||||
|
model = "productized service" if "service" in proposal.business_model.lower() else "saas" if "saas" in proposal.business_model.lower() else "hybrid"
|
||||||
|
channel = "outbound/community" if any(word in text for word in ["outbound", "community", "posts", "network"]) else "marketplace" if "marketplace" in text or "fiverr" in text else "content/seo" if "seo" in text or "content" in text else "direct"
|
||||||
|
price = self._money(proposal.pricing_hypothesis)
|
||||||
|
price_band = "under_100" if price < 100 else "100_500" if price <= 500 else "over_500"
|
||||||
|
first_cash = "under_7_days" if any(token in proposal.time_to_first_dollar_estimate.lower() for token in ["3", "7", "week"]) else "under_30_days"
|
||||||
|
regulatory = "high" if any(word in text for word in ["legal", "compliance", "regulatory", "permit", "policy"]) else "medium" if "platform" in text else "low"
|
||||||
|
return {"industry": industry, "icp": proposal.target_customer[:500], "problem": proposal.problem[:500], "offer": proposal.proposed_solution[:500], "business_model": model, "primary_distribution_channel": channel, "price_band": price_band, "time_to_first_cash_band": first_cash, "required_capability_set": [item.category for item in proposal.capability_requirements.all()] or ["outbound sales", "CRM", "payments"], "geography_dependency": "local" if any(word in text for word in ["local", "city", "metro", "permit"]) else "none", "regulatory_dependency": regulatory, "online_offline": "online" if any(word in text for word in ["shopify", "saas", "api", "online", "web"]) else "mixed", "service_software_hybrid": model}
|
||||||
|
|
||||||
|
def _confidence(self, value: Any) -> float:
|
||||||
|
labels = {"low": 0.35, "medium": 0.55, "moderate": 0.55, "high": 0.75}
|
||||||
|
lowered = str(value).strip().lower()
|
||||||
|
if lowered in labels:
|
||||||
|
return labels[lowered]
|
||||||
|
match = re.search(r"\d+(?:\.\d+)?", lowered)
|
||||||
|
if match is None:
|
||||||
|
return 0.55
|
||||||
|
number = float(match.group(0))
|
||||||
|
return min(1.0, number / 100 if number > 1 else number)
|
||||||
|
|
||||||
|
def _evidence_scores(self, proposal: CompanyProposal) -> dict[str, int]:
|
||||||
|
research = proposal.metadata.get("research", {}) if isinstance(proposal.metadata, dict) else {}
|
||||||
|
coverage_ratio = float(research.get("coverage_ratio", 0.0) or 0.0)
|
||||||
|
unverified = set(research.get("unverified_categories", []))
|
||||||
|
evidence_count = len([item for item in self._as_list(proposal.market_evidence) if isinstance(item, dict) and item.get("url")])
|
||||||
|
low_build = str(proposal.build_complexity).upper() in {"LOW", "LOW-MEDIUM"}
|
||||||
|
service_model = "service" in proposal.business_model.lower()
|
||||||
|
margin_numbers = [int(value) for value in re.findall(r"\d+", proposal.expected_margin)]
|
||||||
|
margin = max(margin_numbers) if margin_numbers else 50
|
||||||
|
demand = min(85, 25 + int(coverage_ratio * 40) + min(evidence_count * 4, 20))
|
||||||
|
pricing = 70 if "pricing" not in unverified else 45
|
||||||
|
pain = 72 if "customer_pain" not in unverified else 35
|
||||||
|
competition = 68 if "competitors" not in unverified and "market_alternatives" not in unverified else 38
|
||||||
|
risk = 72 if "regulatory_platform_risks" not in unverified else 34
|
||||||
|
distribution = 60 if service_model else 42
|
||||||
|
build = 78 if low_build else 42
|
||||||
|
gross_margin = max(35, min(85, margin))
|
||||||
|
capital = 82 if proposal.capital_requested <= Decimal("50") else 30
|
||||||
|
validation = 78 if "5 credible" in proposal.validation_plan or "willingness" in proposal.validation_plan.lower() else 42
|
||||||
|
market_size = 62 if coverage_ratio >= 0.6 else 44
|
||||||
|
defensibility = 42 + (10 if service_model else 0) + (8 if coverage_ratio >= 0.8 else 0)
|
||||||
|
probability = round((demand * 0.22 + pricing * 0.12 + pain * 0.16 + distribution * 0.14 + build * 0.1 + capital * 0.1 + risk * 0.16), 0)
|
||||||
|
return {"Demand Evidence": demand, "Time-to-First-Dollar Attractiveness": 76 if service_model else 50, "Capital Efficiency": capital, "Validation Affordability": validation, "Gross Margin Potential": gross_margin, "Distribution Feasibility": distribution, "Build Simplicity": build, "Defensibility": min(75, defensibility), "Market Opportunity": market_size, "Competitive Position": competition, "Risk Manageability": risk, "Probability of Reaching $500": int(max(20, min(80, probability)))}
|
||||||
|
|
||||||
|
def _pitch(self, payload: dict[str, Any], *, fallback: bool) -> dict[str, Any]:
|
||||||
|
pitch = {"Company name": payload["title"], "One-line thesis": payload["one_line_thesis"], "Problem": payload["problem"], "ICP": payload["target_customer"], "Why now": "AI tooling lowers build cost, increasing the risk that founders overbuild before validating demand.", "Product / service": payload["proposed_solution"], "Business model": payload["business_model"], "Pricing": payload["pricing_hypothesis"], "Route to first customer": payload["acquisition_strategy"], "Validation plan": payload["validation_plan"], "$50 capital allocation proposal": {"initial": "$10 only after approval", "reserved": "$40 held until evidence gate", "v0_spend": "$0"}, "Time to first dollar": payload["time_to_first_dollar_estimate"], "Path to $500 net cash": "Sell 6-10 fixed-scope audits at $49-$99 while keeping delivery manual and using sunk Artifex compute.", "Competition": "Generic startup consultants, founder communities, AI business idea tools, and DIY validation templates. Public competitor research is unverified unless web research is configured.", "Differentiation": payload["differentiation"], "Build requirements": ["report template", "intake form", "manual analysis workflow", "optional landing page after validation approval"], "Distribution requirements": ["compliant outreach plan", "community/content channels", "CRM-lite tracking before first customers"], "Risks": payload["major_risks"], "What would falsify the thesis": "No willingness-to-pay signal at $49-$99 or fewer than 5 credible target-customer responses after approved compliant validation.", "Confidence": payload["confidence"], "Evidence caveat": "Deterministic fallback evidence only; not equivalent to Sol or public web research." if fallback else "Generated by Sol; public web research still only included if sources are present."}
|
||||||
|
return pitch
|
||||||
|
|
||||||
|
def _questions_from_pitch(self, pitch: dict[str, Any]) -> list[dict[str, str]]:
|
||||||
|
return [{"category": "demand", "question": f"What evidence supports demand for {pitch['Company name']} among the stated ICP?"}, {"category": "urgency", "question": "Why will this customer pay now instead of using free templates or advice?"}, {"category": "distribution", "question": "How do you reach the first 10 customers without spam or fake traction?"}, {"category": "validation", "question": "Can this be validated before building the full product?"}, {"category": "business_model", "question": "Why a productized service first instead of SaaS?"}, {"category": "falsification", "question": "What would falsify the thesis within the $50 and 30-day mandate?"}, {"category": "economics", "question": "What happens if acquisition cost or manual delivery time is 3x the estimate?"}, {"category": "competition", "question": "What is the main competitive threat and why is this worth funding over selling an existing Artifex capability?"}]
|
||||||
|
|
||||||
|
def _answer_question(self, question: ICQuestion) -> dict[str, Any]:
|
||||||
|
evidence = [{"source": "internal_reasoning", "summary": "No customer outreach, spend, or fabricated evidence used.", "fallback_evidence": True}]
|
||||||
|
if question.category in {"demand", "competition"} and not self.web_research_available:
|
||||||
|
evidence.append({"source": "capability_gap", "summary": "Public web research unavailable; demand/competition claims remain uncertain.", "fallback_evidence": True})
|
||||||
|
answers = {"demand": "Demand is not proven. The strongest V0 claim is that the problem is plausible and cheap to test, not that demand exists.", "urgency": "The buyer pays only if the report saves them build time or prevents wasted spend; urgency is weakest before a concrete launch decision.", "distribution": "After approval, use targeted compliant posts/conversations and track responses manually; V0 performs no outreach.", "validation": "Yes. The paid diagnostic can be validated with responses and willingness-to-pay before software build.", "business_model": "Service first reduces build risk and can reach first cash faster than SaaS; software should follow only if repeated demand appears.", "falsification": "Failure to collect credible responses or willingness-to-pay within the mandate falsifies near-term viability.", "economics": "If acquisition or delivery is 3x harder, the company should stop or raise price before building tooling.", "competition": "Main threat is generic consulting/free templates. The reason to fund this over selling raw Artifex capability is packaging a buyer-specific outcome."}
|
||||||
|
return {"answer": answers.get(question.category, "The assumption remains uncertain and must be tested before spend."), "evidence": evidence, "uncertainty": "High until public research and customer evidence are available.", "pitch_changes": {"confidence_adjustment": "reduced/held due missing external evidence"} if question.category in {"demand", "competition"} else {}}
|
||||||
|
|
||||||
|
def _capability_requirements(self, proposal: CompanyProposal) -> list[dict[str, Any]]:
|
||||||
|
research = proposal.metadata.get("research", {}) if isinstance(proposal.metadata, dict) else {}
|
||||||
|
web_status = CapabilityStatus.MISSING if not self.web_research_available else CapabilityStatus.AVAILABLE if not research.get("unverified_categories") else CapabilityStatus.PARTIAL
|
||||||
|
return [
|
||||||
|
{"category": "Company Brain", "status": CapabilityStatus.PARTIAL, "rationale": "Venture reasoning exists in V0 but is not a persistent operating brain.", "priority": CapabilityPriority.BEFORE_SCALING, "evidence": {}},
|
||||||
|
{"category": "Board", "status": CapabilityStatus.AVAILABLE, "rationale": "Structured CEO/CTO/CFO/CRO/Independent Director review exists for V0.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
|
||||||
|
{"category": "IC", "status": CapabilityStatus.AVAILABLE, "rationale": "Bounded IC diligence, questions, scoring, and decision vocabulary exist.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
|
||||||
|
{"category": "WEB_MARKET_RESEARCH", "status": web_status, "rationale": "Bounded source-linked web research exists only when configured and category coverage is complete.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {"web_research_available": self.web_research_available, **research}},
|
||||||
|
{"category": "software build", "status": CapabilityStatus.AVAILABLE, "rationale": "Task execution, coding, review, tests, and graph runtime exist.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
|
||||||
|
{"category": "frontend design", "status": CapabilityStatus.AVAILABLE, "rationale": "Frontend agents and Django UI path exist.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
|
||||||
|
{"category": "deployment", "status": CapabilityStatus.PARTIAL, "rationale": "Deployment planning exists, but company-specific production deployment workflow is not implemented.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
|
||||||
|
{"category": "outbound sales", "status": CapabilityStatus.MISSING, "rationale": "No compliant outreach/sequence/customer contact system exists and V0 forbids outreach.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
|
||||||
|
{"category": "CRM", "status": CapabilityStatus.MISSING, "rationale": "No customer pipeline/contact tracking exists.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
|
||||||
|
{"category": "payments", "status": CapabilityStatus.MISSING, "rationale": "No payment collection system exists.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
|
||||||
|
{"category": "invoicing", "status": CapabilityStatus.MISSING, "rationale": "No invoicing workflow exists.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
|
||||||
|
{"category": "customer support", "status": CapabilityStatus.MISSING, "rationale": "No support inbox or customer service workflow exists.", "priority": CapabilityPriority.BEFORE_SCALING, "evidence": {}},
|
||||||
|
{"category": "company budget management", "status": CapabilityStatus.MISSING, "rationale": "V0 blocks spend but future validation needs tranche/budget controls.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
|
||||||
|
{"category": "legal/compliance", "status": CapabilityStatus.MISSING, "rationale": "No contracts, terms, privacy, or compliance review workflow exists.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
|
||||||
|
]
|
||||||
|
|
||||||
|
def _portfolio_concentration(self, cohort: VentureCohort) -> dict[str, Any]:
|
||||||
|
fingerprints = [self.fingerprint_proposal(member.proposal) for member in cohort.members.select_related("proposal")]
|
||||||
|
data = {
|
||||||
|
"industry_distribution": dict(Counter(fp.industry for fp in fingerprints)),
|
||||||
|
"business_model_distribution": dict(Counter(fp.business_model for fp in fingerprints)),
|
||||||
|
"primary_channel_distribution": dict(Counter(fp.primary_distribution_channel for fp in fingerprints)),
|
||||||
|
"icp_distribution": dict(Counter(fp.icp[:80] for fp in fingerprints)),
|
||||||
|
"capability_dependency_distribution": dict(Counter(cap for fp in fingerprints for cap in fp.required_capability_set)),
|
||||||
|
}
|
||||||
|
flags = []
|
||||||
|
for key, counts in data.items():
|
||||||
|
if counts and max(counts.values()) >= max(4, int(cohort.cohort_size * 0.6)):
|
||||||
|
flags.append({"type": "PORTFOLIO_CONCENTRATION", "dimension": key, "value": max(counts, key=counts.get), "count": max(counts.values())})
|
||||||
|
data["flags"] = flags
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _collision_summary(self, cohort: VentureCohort) -> dict[str, int]:
|
||||||
|
counts = Counter(cohort.collisions.values_list("classification", flat=True))
|
||||||
|
return {choice: counts.get(choice, 0) for choice in OverlapClassification.values}
|
||||||
|
|
||||||
|
def _readable_cohort_report(self, content: dict[str, Any]) -> str:
|
||||||
|
ranking = "\n".join(f"{row['rank']}. {row['company']} - score {row['ic_score']}, P($500) {row['probability']}%, {row['decision']}" for row in content["rankings"])
|
||||||
|
top = "\n".join(f"- {row['company']}: {row['thesis']}" for row in content["top_3"])
|
||||||
|
demand = "\n".join(f"- {row['capability']}: {row['count']} companies, earliest {row['earliest_stage']}" for row in content["capability_demand"][:15])
|
||||||
|
return f"# Venture Discovery Cohort Report\n\nCohort: {content['cohort_id']}\nMandate: {content['mandate']}\nTotal spend: $0\nCustomer outreach: none\n\n## Ranking\n{ranking}\n\n## Top 3 Finalists\n{top}\n\n## Collisions\n{json.dumps(content['collisions'], indent=2)}\n\n## Portfolio Concentration\n{json.dumps(content['portfolio_concentration'], indent=2)}\n\n## Capability Demand\n{demand}\n\n## Recommended Artifex Build Priorities\n" + "\n".join(f"- {item}" for item in content["recommended_build_priorities"])
|
||||||
|
|
||||||
|
def _artifact(self, proposal, mandate, artifact_type: str, name: str, content: dict[str, Any], readable: str, generated_by: str, *, graph_run=None) -> VentureArtifact:
|
||||||
|
return VentureArtifact.objects.create(proposal=proposal, mandate=mandate, graph_run=graph_run, artifact_type=artifact_type, name=name, content=content, readable=readable, generated_by=generated_by)
|
||||||
|
|
||||||
|
def readable_pitch(self, pitch: dict[str, Any]) -> str:
|
||||||
|
return "\n\n".join(f"{section}\n{pitch.get(section, '')}" for section in PITCH_SECTIONS)
|
||||||
|
|
||||||
|
def _readable_mandate(self, mandate: CompanyMandate) -> str:
|
||||||
|
return f"Objective\n{mandate.objective}\n\nConstraints\n{json.dumps(mandate.constraints, indent=2)}\n\nOptimization targets\n" + "\n".join(f"- {item}" for item in mandate.optimization_targets)
|
||||||
|
|
||||||
|
def _readable_board(self, review: CompanyBoardReview) -> str:
|
||||||
|
return f"Recommendation: {review.recommendation}\n\nStrengths\n" + "\n".join(f"- {item}" for item in review.strengths) + "\n\nWeaknesses\n" + "\n".join(f"- {item}" for item in review.weaknesses)
|
||||||
|
|
||||||
|
def _readable_responses(self, responses: list[ICResponse]) -> str:
|
||||||
|
return "\n\n".join(f"Q: {response.question.question}\nA: {response.answer}\nUncertainty: {response.uncertainty}" for response in responses)
|
||||||
|
|
||||||
|
def _readable_score(self, decision: ICDecision) -> str:
|
||||||
|
scores = "\n".join(f"- {key}: {value}/100" for key, value in decision.component_scores.items())
|
||||||
|
return f"Decision: {decision.decision}\nComposite: {decision.composite_score}/100\nP($500 within 30 days): {decision.probability_500_within_30_days}%\n\nScores\n{scores}\n\nCondition\n{decision.validation_condition}"
|
||||||
|
|
||||||
|
def _readable_research(self, research: dict[str, Any]) -> str:
|
||||||
|
sources = "\n".join(f"- {source.get('category')}: {source.get('title')} {source.get('url')}" for source in research.get("sources", []))
|
||||||
|
missing = ", ".join(research.get("unverified_categories", [])) or "none"
|
||||||
|
return f"Coverage\n{json.dumps(research.get('coverage', {}), indent=2)}\n\nSources\n{sources}\n\nUnverified categories\n{missing}"
|
||||||
|
|
||||||
|
def _readable_capability_gap(self, available: list[str], partial: list[str], missing: list[str], ranked: list[dict[str, str]]) -> str:
|
||||||
|
return "AVAILABLE\n" + "\n".join(f"- {item}" for item in available) + "\n\nPARTIAL\n" + "\n".join(f"- {item}" for item in partial) + "\n\nMISSING\n" + "\n".join(f"- {item}" for item in missing) + "\n\nNEXT ARTIFEX CAPABILITIES REQUIRED\n" + "\n".join(f"- {item['category']} ({item['priority']})" for item in ranked)
|
||||||
|
|
||||||
|
def _readable_memo(self, memo: dict[str, Any]) -> str:
|
||||||
|
return json.dumps(memo, indent=2)
|
||||||
|
|
||||||
|
def _fingerprint(self, payload: dict[str, Any]) -> str:
|
||||||
|
return hashlib.sha256((str(payload["title"]).lower() + str(payload["target_customer"]).lower() + str(payload["business_model"]).lower()).encode()).hexdigest()
|
||||||
|
|
@ -21,6 +21,7 @@ INSTALLED_APPS = [
|
||||||
"control_plane.events",
|
"control_plane.events",
|
||||||
"control_plane.agents",
|
"control_plane.agents",
|
||||||
"control_plane.resources",
|
"control_plane.resources",
|
||||||
|
"control_plane.ventures",
|
||||||
"control_plane.secrets",
|
"control_plane.secrets",
|
||||||
"control_plane.knowledge",
|
"control_plane.knowledge",
|
||||||
"control_plane.verification",
|
"control_plane.verification",
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@ from __future__ import annotations
|
||||||
from artifex.settings import * # noqa: F403
|
from artifex.settings import * # noqa: F403
|
||||||
|
|
||||||
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
|
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
|
||||||
|
ALLOWED_HOSTS = ["testserver", "localhost", "127.0.0.1"]
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,37 @@ from __future__ import annotations
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from control_plane.projects.views import agent_control_room, dashboard
|
from control_plane.projects import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", dashboard, name="dashboard"),
|
path("", views.dashboard, name="dashboard"),
|
||||||
path("agents/", agent_control_room, name="agent_control_room"),
|
path("projects/", views.projects, name="projects"),
|
||||||
|
path("projects/<uuid:project_id>/", views.project_workspace, name="project_workspace"),
|
||||||
|
path("projects/<uuid:project_id>/brain/", views.project_brain, name="project_brain"),
|
||||||
|
path("projects/<uuid:project_id>/archaeologist/", views.project_archaeologist, name="project_archaeologist"),
|
||||||
|
path("projects/<uuid:project_id>/dag.json", views.project_dag_json, name="project_dag_json"),
|
||||||
|
path("projects/<uuid:project_id>/explore/run/", views.run_explore, name="run_explore"),
|
||||||
|
path("projects/<uuid:project_id>/explore/", views.explore, name="project_explore"),
|
||||||
|
path("projects/<uuid:project_id>/roadmap/", views.roadmap, name="project_roadmap"),
|
||||||
|
path("projects/<uuid:project_id>/scenarios/", views.scenarios, name="project_scenarios"),
|
||||||
|
path("projects/<uuid:project_id>/steward/", views.project_steward, name="project_steward"),
|
||||||
|
path("tasks/<uuid:task_id>/", views.task_detail, name="task_detail"),
|
||||||
|
path("graph-runs/<int:graph_run_id>/", views.graph_run_detail, name="graph_run_detail"),
|
||||||
|
path("graph-runs/<int:graph_run_id>/status.json", views.graph_run_json, name="graph_run_json"),
|
||||||
|
path("steward/", views.steward, name="steward"),
|
||||||
|
path("explore/", views.explore, name="explore"),
|
||||||
|
path("opportunities/<uuid:opportunity_id>/action/", views.opportunity_action, name="opportunity_action"),
|
||||||
|
path("roadmap/", views.roadmap, name="roadmap"),
|
||||||
|
path("roadmap/<uuid:item_id>/action/", views.roadmap_action, name="roadmap_action"),
|
||||||
|
path("scenario-lab/", views.scenarios, name="scenarios"),
|
||||||
|
path("scenario-findings/<uuid:finding_id>/action/", views.scenario_finding_action, name="scenario_finding_action"),
|
||||||
|
path("progeny/", views.progeny, name="progeny"),
|
||||||
|
path("agents/", views.agent_control_room, name="agent_control_room"),
|
||||||
|
path("agents/<uuid:version_id>/", views.agent_detail, name="agent_detail"),
|
||||||
|
path("agents/<uuid:version_id>/performance.json", views.agent_performance_json, name="agent_performance_json"),
|
||||||
|
path("resources/", views.resources, name="resources"),
|
||||||
|
path("approvals/", views.approvals, name="approvals"),
|
||||||
|
path("approvals/<int:approval_id>/action/", views.approval_action, name="approval_action"),
|
||||||
|
path("activity/", views.activity, name="activity"),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
121
control_plane/projects/ui_services.py
Normal file
121
control_plane/projects/ui_services.py
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
from django.db.models import Count, Q
|
||||||
|
|
||||||
|
from agents.control_room import AgentControlRoomService
|
||||||
|
from agents.lifecycle import LifecycleInspectionService
|
||||||
|
from agents.roadmap import RoadmapService
|
||||||
|
from agents.scenario_lab import ScenarioLabService
|
||||||
|
from control_plane.agents.models import AgentRun, AgentVersion, ProgenyExperiment, ProgenyInvestigation, ProgenySignal, PromotionStatus
|
||||||
|
from control_plane.events.models import Event
|
||||||
|
from control_plane.projects.models import ExplorationOpportunity, Project, RoadmapHorizon, ScenarioFinding, ScenarioRun, ScenarioSuite, StewardFinding, StewardRun, Task, TaskStatus
|
||||||
|
from control_plane.resources.models import ModelRequest, Resource
|
||||||
|
from graph.models import ExecutionGraphVersionStatus, GraphApproval, GraphApprovalStatus, GraphRun, GraphRunStatus
|
||||||
|
|
||||||
|
|
||||||
|
class ControlPlaneUIService:
|
||||||
|
def dashboard(self) -> dict[str, object]:
|
||||||
|
projects = Project.objects.all()
|
||||||
|
graph_runs = GraphRun.objects.select_related("execution_graph_version__graph", "project").order_by("-created_at")
|
||||||
|
steward_findings = StewardFinding.objects.all()
|
||||||
|
progeny_signals = ProgenySignal.objects.all()
|
||||||
|
control_room = AgentControlRoomService()
|
||||||
|
agent_health = [control_room.get_agent_health(version.id)["status"] for version in AgentVersion.objects.filter(promotion_status=PromotionStatus.CHAMPION)]
|
||||||
|
return {
|
||||||
|
"project_summary": {"total": projects.count(), "active": projects.exclude(status__in=["FINISHED", "FAILED"]).count(), "blocked_failed": projects.filter(status__in=["BLOCKED", "FAILED"]).count(), "recent_completed": projects.filter(status="FINISHED").order_by("-updated_at")[:5]},
|
||||||
|
"execution_summary": {"active": graph_runs.filter(status__in=[GraphRunStatus.PENDING, GraphRunStatus.RUNNING, GraphRunStatus.PAUSED]).count(), "failed": graph_runs.filter(status=GraphRunStatus.FAILED).count(), "recent_success": graph_runs.filter(status=GraphRunStatus.COMPLETE)[:5], "champion_task_graph": self._champion_graph("task_execution")},
|
||||||
|
"steward_summary": {"enrolled_projects": Project.objects.filter(steward_enrollments__status="ACTIVE").distinct().count(), "open_findings": steward_findings.exclude(status__in=["RESOLVED", "DISMISSED"]).count(), "high_findings": steward_findings.filter(severity__in=["HIGH", "CRITICAL"]).exclude(status__in=["RESOLVED", "DISMISSED"]).count()},
|
||||||
|
"progeny_summary": {"unresolved_signals": progeny_signals.filter(status="OPEN").count(), "open_investigations": ProgenyInvestigation.objects.filter(status="OPEN").count(), "challengers": AgentVersion.objects.filter(promotion_status=PromotionStatus.CHALLENGER).count(), "pending_experiment_decisions": ProgenyExperiment.objects.filter(status__in=["DRAFT", "RUNNING"]).count()},
|
||||||
|
"agent_summary": {"agent_count": AgentVersion.objects.values("agent").distinct().count(), "watch_degraded": sum(1 for status in agent_health if status in ["WATCH", "DEGRADED"]), "active_runs": AgentRun.objects.filter(status__in=["QUEUED", "RUNNING"]).count()},
|
||||||
|
"approval_count": GraphApproval.objects.filter(status=GraphApprovalStatus.PENDING).count(),
|
||||||
|
"recent": {"events": Event.objects.order_by("-created_at")[:10], "tasks": Task.objects.order_by("-updated_at")[:10], "graph_runs": graph_runs[:10], "findings": steward_findings.order_by("-updated_at")[:10], "investigations": ProgenyInvestigation.objects.order_by("-updated_at")[:10]},
|
||||||
|
}
|
||||||
|
|
||||||
|
def project_list(self) -> list[dict[str, object]]:
|
||||||
|
rows = []
|
||||||
|
for project in Project.objects.order_by("name"):
|
||||||
|
rows.append({"project": project, "current_milestone": project.milestones.order_by("order", "created_at").last(), "task_total": project.tasks.count(), "task_complete": project.tasks.filter(status=TaskStatus.COMPLETE).count(), "steward_state": project.steward_enrollments.order_by("-created_at").first(), "open_findings": project.steward_findings.exclude(status__in=["RESOLVED", "DISMISSED"]).count(), "latest_graph_run": project.graph_runs.order_by("-created_at").first(), "warnings": self.project_warnings(project)})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
def project_workspace(self, project: Project) -> dict[str, object]:
|
||||||
|
lifecycle = LifecycleInspectionService().project_lifecycle_view(project)
|
||||||
|
return {"project": project, "plan": project.plans.order_by("-version").first(), "milestones": project.milestones.prefetch_related("features__tasks", "tasks").order_by("order", "created_at"), "tasks": project.tasks.select_related("milestone", "feature").order_by("milestone__order", "priority", "created_at"), "graph_runs": project.graph_runs.select_related("execution_graph_version__graph", "task").order_by("-created_at")[:20], "commits": project.commits.order_by("-created_at")[:10], "roadmap": RoadmapService().project_roadmap_view(project), "lifecycle": lifecycle, "scenario_coverage": ScenarioLabService().coverage(project), "activity": Event.objects.filter(project=project).order_by("-created_at")[:20], "warnings": self.project_warnings(project)}
|
||||||
|
|
||||||
|
def graph_run_detail(self, graph_run: GraphRun) -> dict[str, object]:
|
||||||
|
nodes = list(graph_run.node_runs.select_related("agent_version__agent", "model_request").order_by("created_at", "visit_index"))
|
||||||
|
traversals = list(graph_run.edge_traversals.order_by("created_at"))
|
||||||
|
return {"graph_run": graph_run, "nodes": nodes, "traversals": traversals, "approvals": graph_run.approvals.order_by("-created_at"), "model_request_count": sum(1 for node in nodes if node.model_request_id)}
|
||||||
|
|
||||||
|
def task_detail(self, task: Task) -> dict[str, object]:
|
||||||
|
return {"task": task, "dependencies": [edge.depends_on for edge in task.dependency_edges.select_related("depends_on")], "attempts": task.attempts.select_related("coder").order_by("attempt_number"), "graph_runs": task.graph_runs.select_related("execution_graph_version__graph").order_by("-created_at"), "tests": task.test_runs.order_by("-created_at"), "reviews": task.reviews.order_by("-created_at"), "commits": task.commits.order_by("-created_at")}
|
||||||
|
|
||||||
|
def steward(self, project: Project | None = None) -> dict[str, object]:
|
||||||
|
findings = StewardFinding.objects.select_related("project").order_by("-updated_at")
|
||||||
|
runs = StewardRun.objects.select_related("project").order_by("-created_at")
|
||||||
|
if project:
|
||||||
|
findings = findings.filter(project=project)
|
||||||
|
runs = runs.filter(project=project)
|
||||||
|
return {"findings": findings[:100], "runs": runs[:50]}
|
||||||
|
|
||||||
|
def progeny(self) -> dict[str, object]:
|
||||||
|
signals = ProgenySignal.objects.select_related("project", "agent_version__agent", "execution_graph_version__graph", "graph_node_run").order_by("-created_at")
|
||||||
|
grouped = Counter(signals.filter(status="OPEN").values_list("grouping_key", flat=True))
|
||||||
|
return {"signals": signals[:100], "groups": grouped.most_common(50), "investigations": ProgenyInvestigation.objects.order_by("-created_at")[:50], "experiments": ProgenyExperiment.objects.order_by("-created_at")[:50]}
|
||||||
|
|
||||||
|
def roadmap_board(self, project: Project | None = None) -> dict[str, object]:
|
||||||
|
items = Project.objects.none()
|
||||||
|
qs = project.roadmap_items if project else None
|
||||||
|
board = {}
|
||||||
|
for horizon in RoadmapHorizon.values:
|
||||||
|
board[horizon] = (qs.filter(horizon=horizon) if qs else __import__("control_plane.projects.models", fromlist=["RoadmapItem"]).RoadmapItem.objects.filter(horizon=horizon)).select_related("project").order_by("-composite_score", "-priority")
|
||||||
|
return board
|
||||||
|
|
||||||
|
def scenario_lab(self, project: Project | None = None) -> dict[str, object]:
|
||||||
|
suites = ScenarioSuite.objects.select_related("project").order_by("-created_at")
|
||||||
|
runs = ScenarioRun.objects.select_related("project", "scenario").order_by("-created_at")
|
||||||
|
findings = ScenarioFinding.objects.select_related("project", "scenario").order_by("-created_at")
|
||||||
|
if project:
|
||||||
|
suites = suites.filter(project=project)
|
||||||
|
runs = runs.filter(project=project)
|
||||||
|
findings = findings.filter(project=project)
|
||||||
|
coverage = Counter(ScenarioRun.objects.filter(project=project).values_list("scenario__scenario_type", flat=True) if project else ScenarioRun.objects.values_list("scenario__scenario_type", flat=True))
|
||||||
|
return {"suites": suites[:50], "runs": runs[:100], "findings": findings[:100], "coverage": dict(coverage)}
|
||||||
|
|
||||||
|
def resources(self) -> dict[str, object]:
|
||||||
|
rows = []
|
||||||
|
for resource in Resource.objects.order_by("name"):
|
||||||
|
requests = resource.model_requests.order_by("-created_at")
|
||||||
|
latencies = [value for value in requests.exclude(latency_ms=None).values_list("latency_ms", flat=True)[:50]]
|
||||||
|
rows.append({"resource": resource, "recent_requests": requests[:10], "request_count": requests.count(), "median_latency": sorted(latencies)[len(latencies) // 2] if latencies else None})
|
||||||
|
return {"resources": rows}
|
||||||
|
|
||||||
|
def approvals(self) -> dict[str, object]:
|
||||||
|
return {"approvals": GraphApproval.objects.select_related("graph_run__project", "graph_run__execution_graph_version__graph", "node_run").filter(status=GraphApprovalStatus.PENDING).order_by("created_at")}
|
||||||
|
|
||||||
|
def activity(self, project: Project | None = None) -> dict[str, object]:
|
||||||
|
events = Event.objects.select_related("project", "task").order_by("-created_at")
|
||||||
|
if project:
|
||||||
|
events = events.filter(project=project)
|
||||||
|
return {"events": events[:200]}
|
||||||
|
|
||||||
|
def project_brain(self, project: Project) -> dict[str, object]:
|
||||||
|
return {"project": project, "decisions": project.decisions.order_by("-created_at"), "plans": project.plans.order_by("-version"), "artifacts": project.artifacts.filter(artifact_type__icontains="PLAN").order_by("-created_at")}
|
||||||
|
|
||||||
|
def archaeologist(self, project: Project) -> dict[str, object]:
|
||||||
|
archaeology = project.artifacts.filter(artifact_type__icontains="ARCH").order_by("-created_at")
|
||||||
|
return {"project": project, "observed": project.architecture_summary, "artifacts": archaeology, "findings": project.findings.order_by("-created_at")[:50]}
|
||||||
|
|
||||||
|
def project_warnings(self, project: Project) -> list[str]:
|
||||||
|
warnings = []
|
||||||
|
if project.graph_runs.filter(status=GraphRunStatus.FAILED).exists():
|
||||||
|
warnings.append("failed graph runs")
|
||||||
|
if project.tasks.filter(status__in=[TaskStatus.BLOCKED, TaskStatus.FAILED]).exists():
|
||||||
|
warnings.append("blocked or failed tasks")
|
||||||
|
if project.steward_findings.filter(severity__in=["HIGH", "CRITICAL"]).exclude(status__in=["RESOLVED", "DISMISSED"]).exists():
|
||||||
|
warnings.append("high severity findings")
|
||||||
|
return warnings
|
||||||
|
|
||||||
|
def _champion_graph(self, name: str):
|
||||||
|
return __import__("graph.models", fromlist=["ExecutionGraphVersion"]).ExecutionGraphVersion.objects.filter(graph__name=name, status=ExecutionGraphVersionStatus.CHAMPION).select_related("graph").first()
|
||||||
|
|
@ -1,21 +1,164 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from django.shortcuts import render
|
import json
|
||||||
|
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.views.decorators.http import require_POST
|
||||||
|
|
||||||
from agents.control_room import AgentControlRoomService
|
from agents.control_room import AgentControlRoomService
|
||||||
|
from agents.lifecycle import ExplorerService
|
||||||
|
from agents.roadmap import RoadmapService
|
||||||
|
from agents.scenario_lab import ScenarioLabService
|
||||||
from control_plane.events.models import Event
|
from control_plane.events.models import Event
|
||||||
from control_plane.projects.models import Project, TaskStatus
|
from control_plane.projects.models import Decision, ExplorationOpportunity, Project, RoadmapItem, ScenarioFinding, ScenarioSuite, StewardFinding, Task
|
||||||
|
from control_plane.projects.ui_services import ControlPlaneUIService
|
||||||
|
from graph.bootstrap import champion_project_exploration_graph_v1
|
||||||
|
from graph.langgraph_runtime import LangGraphRuntime
|
||||||
|
from graph.lifecycle import exploration_registry
|
||||||
|
from graph.models import GraphApproval, GraphApprovalStatus, GraphRun, GraphRunStatus
|
||||||
|
|
||||||
|
|
||||||
|
ui = ControlPlaneUIService()
|
||||||
|
|
||||||
|
|
||||||
def dashboard(request):
|
def dashboard(request):
|
||||||
projects = Project.objects.order_by("name")
|
return render(request, "control_plane/dashboard.html", ui.dashboard())
|
||||||
recent_events = Event.objects.select_related("project", "task").order_by("-created_at")[:25]
|
|
||||||
summary = {
|
|
||||||
"project_count": Project.objects.count(),
|
def projects(request):
|
||||||
"ready_tasks": sum(project.tasks.filter(status=TaskStatus.READY).count() for project in projects),
|
return render(request, "control_plane/projects.html", {"rows": ui.project_list()})
|
||||||
"blocked_tasks": sum(project.tasks.filter(status=TaskStatus.BLOCKED).count() for project in projects),
|
|
||||||
}
|
|
||||||
return render(request, "projects/dashboard.html", {"projects": projects, "summary": summary, "recent_events": recent_events})
|
def project_workspace(request, project_id):
|
||||||
|
project = get_object_or_404(Project, id=project_id)
|
||||||
|
return render(request, "control_plane/project_workspace.html", ui.project_workspace(project))
|
||||||
|
|
||||||
|
|
||||||
|
def project_brain(request, project_id):
|
||||||
|
project = get_object_or_404(Project, id=project_id)
|
||||||
|
if request.method == "POST":
|
||||||
|
Decision.objects.create(project=project, decision_type="PROJECT_BRAIN_NOTE", decision=request.POST.get("message", ""), reason="User-authored Project Brain interaction from UI", actor="ui")
|
||||||
|
return redirect("project_brain", project_id=project.id)
|
||||||
|
return render(request, "control_plane/project_brain.html", ui.project_brain(project))
|
||||||
|
|
||||||
|
|
||||||
|
def project_archaeologist(request, project_id):
|
||||||
|
project = get_object_or_404(Project, id=project_id)
|
||||||
|
return render(request, "control_plane/archaeologist.html", ui.archaeologist(project))
|
||||||
|
|
||||||
|
|
||||||
|
def graph_run_detail(request, graph_run_id):
|
||||||
|
graph_run = get_object_or_404(GraphRun, id=graph_run_id)
|
||||||
|
template = "control_plane/partials/graph_run_status.html" if request.headers.get("HX-Request") else "control_plane/graph_run.html"
|
||||||
|
return render(request, template, ui.graph_run_detail(graph_run))
|
||||||
|
|
||||||
|
|
||||||
|
def graph_run_json(request, graph_run_id):
|
||||||
|
graph_run = get_object_or_404(GraphRun, id=graph_run_id)
|
||||||
|
return JsonResponse({"id": graph_run.id, "status": graph_run.status, "current_node": graph_run.current_node, "nodes": list(graph_run.node_runs.values("node_id", "visit_index", "status", "failure_evidence", "telemetry")), "edges": list(graph_run.edge_traversals.values("source_node", "target_node", "result", "condition"))})
|
||||||
|
|
||||||
|
|
||||||
|
def project_dag_json(request, project_id):
|
||||||
|
project = get_object_or_404(Project, id=project_id)
|
||||||
|
return JsonResponse({"project": str(project.id), "milestones": list(project.milestones.values("id", "key", "title", "status", "order")), "features": list(project.features.values("id", "milestone_id", "title", "status")), "tasks": list(project.tasks.values("id", "milestone_id", "feature_id", "goal", "status", "priority")), "dependencies": list(project.tasks.values("id", "dependency_edges__depends_on_id"))})
|
||||||
|
|
||||||
|
|
||||||
|
def task_detail(request, task_id):
|
||||||
|
task = get_object_or_404(Task, id=task_id)
|
||||||
|
return render(request, "control_plane/task.html", ui.task_detail(task))
|
||||||
|
|
||||||
|
|
||||||
|
def steward(request):
|
||||||
|
return render(request, "control_plane/steward.html", ui.steward())
|
||||||
|
|
||||||
|
|
||||||
|
def project_steward(request, project_id):
|
||||||
|
project = get_object_or_404(Project, id=project_id)
|
||||||
|
return render(request, "control_plane/steward.html", {"project": project, **ui.steward(project)})
|
||||||
|
|
||||||
|
|
||||||
|
def explore(request, project_id=None):
|
||||||
|
project = get_object_or_404(Project, id=project_id) if project_id else None
|
||||||
|
opportunities = ExplorationOpportunity.objects.select_related("project", "exploration").order_by("-composite_score", "-created_at")
|
||||||
|
if project:
|
||||||
|
opportunities = opportunities.filter(project=project)
|
||||||
|
return render(request, "control_plane/explore.html", {"project": project, "opportunities": opportunities[:100]})
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def run_explore(request, project_id):
|
||||||
|
project = get_object_or_404(Project, id=project_id)
|
||||||
|
service = ExplorerService()
|
||||||
|
exploration = service.start_exploration(project, prompt="UI Explore run")
|
||||||
|
version = champion_project_exploration_graph_v1()
|
||||||
|
graph_run = GraphRun.objects.create(execution_graph_version=version, project=project, current_node=version.graph_spec["entry"], metadata={"exploration_id": str(exploration.id), "source": "ui"})
|
||||||
|
LangGraphRuntime(exploration_registry(service)).run_until_terminal_or_paused(graph_run)
|
||||||
|
return redirect("graph_run_detail", graph_run_id=graph_run.id)
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def opportunity_action(request, opportunity_id):
|
||||||
|
opportunity = get_object_or_404(ExplorationOpportunity, id=opportunity_id)
|
||||||
|
action = request.POST.get("action")
|
||||||
|
service = ExplorerService()
|
||||||
|
if action == "extend":
|
||||||
|
service.convert_to_extension(opportunity)
|
||||||
|
elif action == "evolve":
|
||||||
|
service.convert_to_evolution(opportunity, baseline_measurement={"metric": "value", "value": 100})
|
||||||
|
elif action == "roadmap":
|
||||||
|
RoadmapService().upsert_item(opportunity.project, title=opportunity.title, description=opportunity.description, source="EXPLORE", source_ref={"exploration_opportunity_id": str(opportunity.id)}, rationale=opportunity.rationale, evidence=opportunity.evidence, horizon="NEXT", category=opportunity.opportunity_type, target_action=opportunity.recommended_action if opportunity.recommended_action in ["EXTEND", "EVOLVE", "REPAIR", "INVESTIGATE"] else "NONE", scores={"value": opportunity.value_score, "effort": opportunity.effort_score, "risk": opportunity.risk_score, "confidence": opportunity.confidence, "strategic_fit": opportunity.strategic_fit, "technical_fit": opportunity.technical_fit})
|
||||||
|
elif action == "defer":
|
||||||
|
service.defer(opportunity)
|
||||||
|
elif action == "reject":
|
||||||
|
service.reject(opportunity)
|
||||||
|
return redirect(request.META.get("HTTP_REFERER") or reverse("explore"))
|
||||||
|
|
||||||
|
|
||||||
|
def roadmap(request, project_id=None):
|
||||||
|
project = get_object_or_404(Project, id=project_id) if project_id else None
|
||||||
|
return render(request, "control_plane/roadmap.html", {"project": project, "board": ui.roadmap_board(project)})
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def roadmap_action(request, item_id):
|
||||||
|
item = get_object_or_404(RoadmapItem, id=item_id)
|
||||||
|
action = request.POST.get("action")
|
||||||
|
service = RoadmapService()
|
||||||
|
if action in ["NOW", "NEXT", "LATER", "EXPLORING"]:
|
||||||
|
item.horizon = action
|
||||||
|
item.save(update_fields=["horizon", "updated_at"])
|
||||||
|
elif action == "defer":
|
||||||
|
item.status = "DEFERRED"
|
||||||
|
item.save(update_fields=["status", "updated_at"])
|
||||||
|
elif action == "reject":
|
||||||
|
item.status = "REJECTED"
|
||||||
|
item.save(update_fields=["status", "updated_at"])
|
||||||
|
elif action == "extend":
|
||||||
|
service.convert_to_extension(item)
|
||||||
|
elif action == "evolve":
|
||||||
|
service.convert_to_evolution(item, baseline_measurement={"metric": "value", "value": 100})
|
||||||
|
return redirect(request.META.get("HTTP_REFERER") or reverse("roadmap"))
|
||||||
|
|
||||||
|
|
||||||
|
def scenarios(request, project_id=None):
|
||||||
|
project = get_object_or_404(Project, id=project_id) if project_id else None
|
||||||
|
return render(request, "control_plane/scenarios.html", {"project": project, **ui.scenario_lab(project)})
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def scenario_finding_action(request, finding_id):
|
||||||
|
finding = get_object_or_404(ScenarioFinding, id=finding_id)
|
||||||
|
action = request.POST.get("action")
|
||||||
|
if action == "roadmap":
|
||||||
|
ScenarioLabService().route_finding(finding)
|
||||||
|
return redirect(request.META.get("HTTP_REFERER") or reverse("scenarios"))
|
||||||
|
|
||||||
|
|
||||||
|
def progeny(request):
|
||||||
|
return render(request, "control_plane/progeny.html", ui.progeny())
|
||||||
|
|
||||||
|
|
||||||
def agent_control_room(request):
|
def agent_control_room(request):
|
||||||
|
|
@ -24,4 +167,54 @@ def agent_control_room(request):
|
||||||
for agent in agents:
|
for agent in agents:
|
||||||
champion_id = agent.get("champion_version")
|
champion_id = agent.get("champion_version")
|
||||||
agent["health"] = service.get_agent_health(champion_id) if champion_id else {"status": "WATCH", "reasons": ["No champion version."]}
|
agent["health"] = service.get_agent_health(champion_id) if champion_id else {"status": "WATCH", "reasons": ["No champion version."]}
|
||||||
return render(request, "projects/agent_control_room.html", {"agents": agents, "teams": service.list_teams()})
|
agent["performance"] = service.get_agent_performance(champion_id) if champion_id else {}
|
||||||
|
return render(request, "control_plane/agents.html", {"agents": agents, "teams": service.list_teams()})
|
||||||
|
|
||||||
|
|
||||||
|
def agent_detail(request, version_id):
|
||||||
|
service = AgentControlRoomService()
|
||||||
|
return render(request, "control_plane/agent_detail.html", {"version": service.get_agent_version(version_id), "performance": service.get_agent_performance(version_id), "health": service.get_agent_health(version_id), "usage": service.get_agent_usage(version_id), "progeny": service.get_agent_progeny(version_id), "challengers": service.get_agent_challengers(service.get_agent_version(version_id)["agent_id"])})
|
||||||
|
|
||||||
|
|
||||||
|
def agent_performance_json(request, version_id):
|
||||||
|
return JsonResponse(AgentControlRoomService().get_agent_performance(version_id, window=request.GET.get("window", "lifetime")))
|
||||||
|
|
||||||
|
|
||||||
|
def resources(request):
|
||||||
|
return render(request, "control_plane/resources.html", ui.resources())
|
||||||
|
|
||||||
|
|
||||||
|
def approvals(request):
|
||||||
|
return render(request, "control_plane/approvals.html", ui.approvals())
|
||||||
|
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def approval_action(request, approval_id):
|
||||||
|
approval = get_object_or_404(GraphApproval, id=approval_id)
|
||||||
|
action = request.POST.get("action")
|
||||||
|
approval.status = GraphApprovalStatus.APPROVED if action == "approve" else GraphApprovalStatus.REJECTED
|
||||||
|
approval.decided_by = "ui"
|
||||||
|
approval.decided_at = timezone.now()
|
||||||
|
approval.save(update_fields=["status", "decided_by", "decided_at", "updated_at"])
|
||||||
|
graph_run = approval.graph_run
|
||||||
|
if approval.status == GraphApprovalStatus.APPROVED:
|
||||||
|
graph_run.status = GraphRunStatus.RUNNING
|
||||||
|
graph_run.failure_reason = ""
|
||||||
|
graph_run.save(update_fields=["status", "failure_reason", "updated_at"])
|
||||||
|
self_resume_graph(graph_run)
|
||||||
|
return redirect("graph_run_detail", graph_run_id=graph_run.id)
|
||||||
|
|
||||||
|
|
||||||
|
def activity(request):
|
||||||
|
project = Project.objects.filter(id=request.GET.get("project")).first() if request.GET.get("project") else None
|
||||||
|
return render(request, "control_plane/activity.html", {"project": project, **ui.activity(project)})
|
||||||
|
|
||||||
|
|
||||||
|
def self_resume_graph(graph_run: GraphRun) -> None:
|
||||||
|
name = graph_run.execution_graph_version.graph.name
|
||||||
|
try:
|
||||||
|
if name == "project_exploration":
|
||||||
|
LangGraphRuntime(exploration_registry(ExplorerService())).run_until_terminal_or_paused(graph_run)
|
||||||
|
except Exception as exc:
|
||||||
|
graph_run.failure_reason = f"UI approval saved; automatic resume failed: {exc}"
|
||||||
|
graph_run.save(update_fields=["failure_reason", "updated_at"])
|
||||||
|
|
|
||||||
0
control_plane/ventures/__init__.py
Normal file
0
control_plane/ventures/__init__.py
Normal file
8
control_plane/ventures/apps.py
Normal file
8
control_plane/ventures/apps.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class VenturesConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "control_plane.ventures"
|
||||||
239
control_plane/ventures/migrations/0001_initial.py
Normal file
239
control_plane/ventures/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
# Generated by Django 5.2.16 on 2026-08-15 13:50
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('graph', '0004_unique_champion_graph_version'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CompanyMandate',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('objective', models.TextField()),
|
||||||
|
('constraints', models.JSONField(blank=True, default=dict)),
|
||||||
|
('optimization_targets', models.JSONField(blank=True, default=list)),
|
||||||
|
('max_validation_capital', models.DecimalField(decimal_places=2, default=50, max_digits=10)),
|
||||||
|
('target_net_new_cash', models.DecimalField(decimal_places=2, default=500, max_digits=10)),
|
||||||
|
('target_window_days', models.PositiveIntegerField(default=30)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CompanyProposal',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('title', models.CharField(max_length=255)),
|
||||||
|
('description', models.TextField()),
|
||||||
|
('problem', models.TextField()),
|
||||||
|
('target_customer', models.TextField()),
|
||||||
|
('proposed_solution', models.TextField()),
|
||||||
|
('business_model', models.TextField()),
|
||||||
|
('pricing_hypothesis', models.TextField()),
|
||||||
|
('acquisition_strategy', models.TextField()),
|
||||||
|
('validation_plan', models.TextField()),
|
||||||
|
('capital_requested', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
|
||||||
|
('time_to_first_dollar_estimate', models.CharField(max_length=120)),
|
||||||
|
('expected_margin', models.CharField(max_length=120)),
|
||||||
|
('build_complexity', models.CharField(max_length=80)),
|
||||||
|
('market_evidence', models.JSONField(blank=True, default=list)),
|
||||||
|
('differentiation', models.TextField()),
|
||||||
|
('major_risks', models.JSONField(blank=True, default=list)),
|
||||||
|
('confidence', models.FloatField(default=0.0)),
|
||||||
|
('status', models.CharField(choices=[('DRAFT', 'Draft'), ('SUBMITTED', 'Submitted'), ('UNDER_DILIGENCE', 'Under Diligence'), ('REVISE', 'Revise'), ('FUNDED_RECOMMENDED', 'Funded Recommended'), ('WATCHLIST', 'Watchlist'), ('REJECTED', 'Rejected')], default='DRAFT', max_length=32)),
|
||||||
|
('pitch', models.JSONField(blank=True, default=dict)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('mandate', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='company_proposals', to='ventures.companymandate')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CompanyCapabilityRequirement',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('category', models.CharField(max_length=120)),
|
||||||
|
('status', models.CharField(choices=[('AVAILABLE', 'Available'), ('PARTIAL', 'Partial'), ('MISSING', 'Missing')], max_length=32)),
|
||||||
|
('rationale', models.TextField(blank=True)),
|
||||||
|
('priority', models.CharField(choices=[('BEFORE_VALIDATION', 'Before Validation'), ('BEFORE_FIRST_CUSTOMER', 'Before First Customer'), ('BEFORE_SCALING', 'Before Scaling')], default='BEFORE_SCALING', max_length=32)),
|
||||||
|
('evidence', models.JSONField(blank=True, default=dict)),
|
||||||
|
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='capability_requirements', to='ventures.companyproposal')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CompanyBoardReview',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('observations', models.JSONField(blank=True, default=dict)),
|
||||||
|
('strengths', models.JSONField(blank=True, default=list)),
|
||||||
|
('weaknesses', models.JSONField(blank=True, default=list)),
|
||||||
|
('key_assumptions', models.JSONField(blank=True, default=list)),
|
||||||
|
('required_revisions', models.JSONField(blank=True, default=list)),
|
||||||
|
('recommendation', models.CharField(max_length=80)),
|
||||||
|
('revised_pitch', models.JSONField(blank=True, default=dict)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='board_reviews', to='ventures.companyproposal')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ICDiligence',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('status', models.CharField(default='INITIAL_PITCH', max_length=32)),
|
||||||
|
('rounds', models.JSONField(blank=True, default=list)),
|
||||||
|
('red_team_challenge', models.JSONField(blank=True, default=dict)),
|
||||||
|
('final_response', models.JSONField(blank=True, default=dict)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ic_diligence', to='ventures.companyproposal')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ICDecision',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('decision', models.CharField(choices=[('FUND_RECOMMENDED', 'Fund Recommended'), ('CONDITIONAL_FUND', 'Conditional Fund'), ('REVISE_AND_RESUBMIT', 'Revise And Resubmit'), ('WATCHLIST', 'Watchlist'), ('PIVOT', 'Pivot'), ('REJECT', 'Reject')], max_length=32)),
|
||||||
|
('component_scores', models.JSONField(blank=True, default=dict)),
|
||||||
|
('composite_score', models.FloatField(default=0.0)),
|
||||||
|
('probability_500_within_30_days', models.FloatField(default=0.0)),
|
||||||
|
('initial_tranche', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||||
|
('validation_condition', models.TextField(blank=True)),
|
||||||
|
('evidence_required', models.JSONField(blank=True, default=list)),
|
||||||
|
('recommended_allocation', models.JSONField(blank=True, default=dict)),
|
||||||
|
('kill_criteria', models.JSONField(blank=True, default=list)),
|
||||||
|
('next_decision_point', models.TextField(blank=True)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('diligence', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='decision', to='ventures.icdiligence')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ICQuestion',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('question', models.TextField()),
|
||||||
|
('category', models.CharField(max_length=80)),
|
||||||
|
('evidence_required', models.BooleanField(default=True)),
|
||||||
|
('round_name', models.CharField(default='Diligence Round 1', max_length=80)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('diligence', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='ventures.icdiligence')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ICResponse',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('answer', models.TextField()),
|
||||||
|
('evidence', models.JSONField(blank=True, default=list)),
|
||||||
|
('uncertainty', models.TextField(blank=True)),
|
||||||
|
('pitch_changes', models.JSONField(blank=True, default=dict)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='ventures.icquestion')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PortfolioCapabilityGap',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('available', models.JSONField(blank=True, default=list)),
|
||||||
|
('partial', models.JSONField(blank=True, default=list)),
|
||||||
|
('missing', models.JSONField(blank=True, default=list)),
|
||||||
|
('ranked_missing', models.JSONField(blank=True, default=list)),
|
||||||
|
('report', models.TextField(blank=True)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='capability_gap_reports', to='ventures.companyproposal')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VentureArtifact',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('artifact_type', models.CharField(max_length=80)),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('content', models.JSONField(blank=True, default=dict)),
|
||||||
|
('readable', models.TextField(blank=True)),
|
||||||
|
('generated_by', models.CharField(blank=True, max_length=120)),
|
||||||
|
('graph_run', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='venture_artifacts', to='graph.graphrun')),
|
||||||
|
('mandate', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='artifacts', to='ventures.companymandate')),
|
||||||
|
('proposal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='artifacts', to='ventures.companyproposal')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VentureThesis',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('title', models.CharField(max_length=255)),
|
||||||
|
('thesis', models.TextField()),
|
||||||
|
('similarity_fingerprint', models.CharField(blank=True, max_length=128)),
|
||||||
|
('related_theses', models.JSONField(blank=True, default=list)),
|
||||||
|
('competitive_theses', models.JSONField(blank=True, default=list)),
|
||||||
|
('portfolio_visibility', models.JSONField(blank=True, default=dict)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('mandate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='theses', to='ventures.companymandate')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='companyproposal',
|
||||||
|
name='thesis',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='company_proposals', to='ventures.venturethesis'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
# Generated by Django 5.2.16 on 2026-08-15 14:22
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('graph', '0004_unique_champion_graph_version'),
|
||||||
|
('ventures', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='companyproposal',
|
||||||
|
name='evidence_tier',
|
||||||
|
field=models.CharField(choices=[('TIER_0_THESIS', 'Tier 0 Thesis'), ('TIER_1_PUBLIC_EVIDENCE', 'Tier 1 Public Evidence'), ('TIER_2_CUSTOMER_SIGNAL', 'Tier 2 Customer Signal'), ('TIER_3_WILLINGNESS_TO_PAY', 'Tier 3 Willingness To Pay'), ('TIER_4_PAID_CUSTOMER', 'Tier 4 Paid Customer'), ('TIER_5_REPEATABLE_TRACTION', 'Tier 5 Repeatable Traction')], default='TIER_0_THESIS', max_length=40),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='icdecision',
|
||||||
|
name='evidence_ceiling',
|
||||||
|
field=models.FloatField(default=35.0),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='icdecision',
|
||||||
|
name='evidence_tier',
|
||||||
|
field=models.CharField(choices=[('TIER_0_THESIS', 'Tier 0 Thesis'), ('TIER_1_PUBLIC_EVIDENCE', 'Tier 1 Public Evidence'), ('TIER_2_CUSTOMER_SIGNAL', 'Tier 2 Customer Signal'), ('TIER_3_WILLINGNESS_TO_PAY', 'Tier 3 Willingness To Pay'), ('TIER_4_PAID_CUSTOMER', 'Tier 4 Paid Customer'), ('TIER_5_REPEATABLE_TRACTION', 'Tier 5 Repeatable Traction')], default='TIER_0_THESIS', max_length=40),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='icdecision',
|
||||||
|
name='probability_explanation',
|
||||||
|
field=models.TextField(blank=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='icdecision',
|
||||||
|
name='raw_probability_500_within_30_days',
|
||||||
|
field=models.FloatField(default=0.0),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='icdecision',
|
||||||
|
name='score_definitions',
|
||||||
|
field=models.JSONField(blank=True, default=dict),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='venturethesis',
|
||||||
|
name='evidence_tier',
|
||||||
|
field=models.CharField(choices=[('TIER_0_THESIS', 'Tier 0 Thesis'), ('TIER_1_PUBLIC_EVIDENCE', 'Tier 1 Public Evidence'), ('TIER_2_CUSTOMER_SIGNAL', 'Tier 2 Customer Signal'), ('TIER_3_WILLINGNESS_TO_PAY', 'Tier 3 Willingness To Pay'), ('TIER_4_PAID_CUSTOMER', 'Tier 4 Paid Customer'), ('TIER_5_REPEATABLE_TRACTION', 'Tier 5 Repeatable Traction')], default='TIER_0_THESIS', max_length=40),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VentureCohort',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('cohort_id', models.CharField(max_length=80, unique=True)),
|
||||||
|
('cohort_size', models.PositiveIntegerField(default=10)),
|
||||||
|
('status', models.CharField(default='PREPARING', max_length=40)),
|
||||||
|
('concurrency', models.PositiveIntegerField(default=1)),
|
||||||
|
('model_versions', models.JSONField(blank=True, default=dict)),
|
||||||
|
('graph_versions', models.JSONField(blank=True, default=dict)),
|
||||||
|
('research_policy', models.JSONField(blank=True, default=dict)),
|
||||||
|
('scoring_policy', models.JSONField(blank=True, default=dict)),
|
||||||
|
('evidence_calibration_policy', models.JSONField(blank=True, default=dict)),
|
||||||
|
('metrics', models.JSONField(blank=True, default=dict)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('graph_run', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='venture_cohorts', to='graph.graphrun')),
|
||||||
|
('mandate', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='venture_cohorts', to='ventures.companymandate')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VentureCapabilityDemand',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('capability', models.CharField(max_length=120)),
|
||||||
|
('count', models.PositiveIntegerField(default=0)),
|
||||||
|
('percentage', models.FloatField(default=0.0)),
|
||||||
|
('earliest_stage', models.CharField(choices=[('BEFORE_VALIDATION', 'Before Validation'), ('BEFORE_FIRST_CUSTOMER', 'Before First Customer'), ('BEFORE_SCALING', 'Before Scaling')], max_length=32)),
|
||||||
|
('companies', models.JSONField(blank=True, default=list)),
|
||||||
|
('status_distribution', models.JSONField(blank=True, default=dict)),
|
||||||
|
('top_3_count', models.PositiveIntegerField(default=0)),
|
||||||
|
('priority_score', models.FloatField(default=0.0)),
|
||||||
|
('cohort', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='capability_demands', to='ventures.venturecohort')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PortfolioICReview',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('rankings', models.JSONField(blank=True, default=list)),
|
||||||
|
('top_3', models.JSONField(blank=True, default=list)),
|
||||||
|
('concentration', models.JSONField(blank=True, default=dict)),
|
||||||
|
('capability_demand', models.JSONField(blank=True, default=list)),
|
||||||
|
('top_3_capability_gaps', models.JSONField(blank=True, default=list)),
|
||||||
|
('recommended_build_priorities', models.JSONField(blank=True, default=list)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('cohort', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='portfolio_review', to='ventures.venturecohort')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VentureCollision',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('classification', models.CharField(choices=[('NONE', 'None'), ('ADJACENT', 'Adjacent'), ('COMPETITIVE', 'Competitive'), ('NEAR_DUPLICATE', 'Near Duplicate'), ('DUPLICATE', 'Duplicate')], max_length=40)),
|
||||||
|
('similarity_score', models.FloatField(default=0.0)),
|
||||||
|
('explanation', models.TextField(blank=True)),
|
||||||
|
('overlapping_dimensions', models.JSONField(blank=True, default=list)),
|
||||||
|
('cohort', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collisions', to='ventures.venturecohort')),
|
||||||
|
('company_a', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collisions_as_a', to='ventures.companyproposal')),
|
||||||
|
('company_b', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collisions_as_b', to='ventures.companyproposal')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VentureThesisFingerprint',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('industry', models.CharField(max_length=160)),
|
||||||
|
('icp', models.TextField()),
|
||||||
|
('problem', models.TextField()),
|
||||||
|
('offer', models.TextField()),
|
||||||
|
('business_model', models.CharField(max_length=160)),
|
||||||
|
('primary_distribution_channel', models.CharField(max_length=160)),
|
||||||
|
('price_band', models.CharField(max_length=80)),
|
||||||
|
('time_to_first_cash_band', models.CharField(max_length=80)),
|
||||||
|
('required_capability_set', models.JSONField(blank=True, default=list)),
|
||||||
|
('geography_dependency', models.CharField(blank=True, max_length=120)),
|
||||||
|
('regulatory_dependency', models.CharField(blank=True, max_length=120)),
|
||||||
|
('online_offline', models.CharField(blank=True, max_length=80)),
|
||||||
|
('service_software_hybrid', models.CharField(blank=True, max_length=80)),
|
||||||
|
('fingerprint_hash', models.CharField(max_length=128)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('proposal', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='fingerprint', to='ventures.companyproposal')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='VentureCohortMember',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('rank', models.PositiveIntegerField(blank=True, null=True)),
|
||||||
|
('is_top_3', models.BooleanField(default=False)),
|
||||||
|
('portfolio_score', models.FloatField(default=0.0)),
|
||||||
|
('metadata', models.JSONField(blank=True, default=dict)),
|
||||||
|
('child_graph_run', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='venture_cohort_members', to='graph.graphrun')),
|
||||||
|
('cohort', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='members', to='ventures.venturecohort')),
|
||||||
|
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cohort_memberships', to='ventures.companyproposal')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'constraints': [models.UniqueConstraint(fields=('cohort', 'proposal'), name='unique_venture_cohort_member')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
control_plane/ventures/migrations/__init__.py
Normal file
0
control_plane/ventures/migrations/__init__.py
Normal file
271
control_plane/ventures/models.py
Normal file
271
control_plane/ventures/models.py
Normal file
|
|
@ -0,0 +1,271 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from control_plane.common import TimestampedModel
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyProposalStatus(models.TextChoices):
|
||||||
|
DRAFT = "DRAFT"
|
||||||
|
SUBMITTED = "SUBMITTED"
|
||||||
|
UNDER_DILIGENCE = "UNDER_DILIGENCE"
|
||||||
|
REVISE = "REVISE"
|
||||||
|
FUNDED_RECOMMENDED = "FUNDED_RECOMMENDED"
|
||||||
|
WATCHLIST = "WATCHLIST"
|
||||||
|
REJECTED = "REJECTED"
|
||||||
|
|
||||||
|
|
||||||
|
class ICDecisionType(models.TextChoices):
|
||||||
|
FUND_RECOMMENDED = "FUND_RECOMMENDED"
|
||||||
|
CONDITIONAL_FUND = "CONDITIONAL_FUND"
|
||||||
|
REVISE_AND_RESUBMIT = "REVISE_AND_RESUBMIT"
|
||||||
|
WATCHLIST = "WATCHLIST"
|
||||||
|
PIVOT = "PIVOT"
|
||||||
|
REJECT = "REJECT"
|
||||||
|
|
||||||
|
|
||||||
|
class CapabilityStatus(models.TextChoices):
|
||||||
|
AVAILABLE = "AVAILABLE"
|
||||||
|
PARTIAL = "PARTIAL"
|
||||||
|
MISSING = "MISSING"
|
||||||
|
|
||||||
|
|
||||||
|
class CapabilityPriority(models.TextChoices):
|
||||||
|
BEFORE_VALIDATION = "BEFORE_VALIDATION"
|
||||||
|
BEFORE_FIRST_CUSTOMER = "BEFORE_FIRST_CUSTOMER"
|
||||||
|
BEFORE_SCALING = "BEFORE_SCALING"
|
||||||
|
|
||||||
|
|
||||||
|
class EvidenceTier(models.TextChoices):
|
||||||
|
TIER_0_THESIS = "TIER_0_THESIS"
|
||||||
|
TIER_1_PUBLIC_EVIDENCE = "TIER_1_PUBLIC_EVIDENCE"
|
||||||
|
TIER_2_CUSTOMER_SIGNAL = "TIER_2_CUSTOMER_SIGNAL"
|
||||||
|
TIER_3_WILLINGNESS_TO_PAY = "TIER_3_WILLINGNESS_TO_PAY"
|
||||||
|
TIER_4_PAID_CUSTOMER = "TIER_4_PAID_CUSTOMER"
|
||||||
|
TIER_5_REPEATABLE_TRACTION = "TIER_5_REPEATABLE_TRACTION"
|
||||||
|
|
||||||
|
|
||||||
|
class OverlapClassification(models.TextChoices):
|
||||||
|
NONE = "NONE"
|
||||||
|
ADJACENT = "ADJACENT"
|
||||||
|
COMPETITIVE = "COMPETITIVE"
|
||||||
|
NEAR_DUPLICATE = "NEAR_DUPLICATE"
|
||||||
|
DUPLICATE = "DUPLICATE"
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyMandate(TimestampedModel):
|
||||||
|
objective = models.TextField()
|
||||||
|
constraints = models.JSONField(default=dict, blank=True)
|
||||||
|
optimization_targets = models.JSONField(default=list, blank=True)
|
||||||
|
max_validation_capital = models.DecimalField(max_digits=10, decimal_places=2, default=50)
|
||||||
|
target_net_new_cash = models.DecimalField(max_digits=10, decimal_places=2, default=500)
|
||||||
|
target_window_days = models.PositiveIntegerField(default=30)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class VentureThesis(TimestampedModel):
|
||||||
|
mandate = models.ForeignKey(CompanyMandate, on_delete=models.CASCADE, related_name="theses")
|
||||||
|
title = models.CharField(max_length=255)
|
||||||
|
thesis = models.TextField()
|
||||||
|
similarity_fingerprint = models.CharField(max_length=128, blank=True)
|
||||||
|
related_theses = models.JSONField(default=list, blank=True)
|
||||||
|
competitive_theses = models.JSONField(default=list, blank=True)
|
||||||
|
portfolio_visibility = models.JSONField(default=dict, blank=True)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
evidence_tier = models.CharField(max_length=40, choices=EvidenceTier.choices, default=EvidenceTier.TIER_0_THESIS)
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyProposal(TimestampedModel):
|
||||||
|
mandate = models.ForeignKey(CompanyMandate, on_delete=models.PROTECT, related_name="company_proposals")
|
||||||
|
thesis = models.ForeignKey(VentureThesis, on_delete=models.SET_NULL, null=True, blank=True, related_name="company_proposals")
|
||||||
|
title = models.CharField(max_length=255)
|
||||||
|
description = models.TextField()
|
||||||
|
problem = models.TextField()
|
||||||
|
target_customer = models.TextField()
|
||||||
|
proposed_solution = models.TextField()
|
||||||
|
business_model = models.TextField()
|
||||||
|
pricing_hypothesis = models.TextField()
|
||||||
|
acquisition_strategy = models.TextField()
|
||||||
|
validation_plan = models.TextField()
|
||||||
|
capital_requested = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||||
|
time_to_first_dollar_estimate = models.CharField(max_length=120)
|
||||||
|
expected_margin = models.CharField(max_length=120)
|
||||||
|
build_complexity = models.CharField(max_length=80)
|
||||||
|
market_evidence = models.JSONField(default=list, blank=True)
|
||||||
|
differentiation = models.TextField()
|
||||||
|
major_risks = models.JSONField(default=list, blank=True)
|
||||||
|
confidence = models.FloatField(default=0.0)
|
||||||
|
status = models.CharField(max_length=32, choices=CompanyProposalStatus.choices, default=CompanyProposalStatus.DRAFT)
|
||||||
|
pitch = models.JSONField(default=dict, blank=True)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
evidence_tier = models.CharField(max_length=40, choices=EvidenceTier.choices, default=EvidenceTier.TIER_0_THESIS)
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyBoardReview(TimestampedModel):
|
||||||
|
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="board_reviews")
|
||||||
|
observations = models.JSONField(default=dict, blank=True)
|
||||||
|
strengths = models.JSONField(default=list, blank=True)
|
||||||
|
weaknesses = models.JSONField(default=list, blank=True)
|
||||||
|
key_assumptions = models.JSONField(default=list, blank=True)
|
||||||
|
required_revisions = models.JSONField(default=list, blank=True)
|
||||||
|
recommendation = models.CharField(max_length=80)
|
||||||
|
revised_pitch = models.JSONField(default=dict, blank=True)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ICDiligence(TimestampedModel):
|
||||||
|
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="ic_diligence")
|
||||||
|
status = models.CharField(max_length=32, default="INITIAL_PITCH")
|
||||||
|
rounds = models.JSONField(default=list, blank=True)
|
||||||
|
red_team_challenge = models.JSONField(default=dict, blank=True)
|
||||||
|
final_response = models.JSONField(default=dict, blank=True)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ICQuestion(TimestampedModel):
|
||||||
|
diligence = models.ForeignKey(ICDiligence, on_delete=models.CASCADE, related_name="questions")
|
||||||
|
question = models.TextField()
|
||||||
|
category = models.CharField(max_length=80)
|
||||||
|
evidence_required = models.BooleanField(default=True)
|
||||||
|
round_name = models.CharField(max_length=80, default="Diligence Round 1")
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ICResponse(TimestampedModel):
|
||||||
|
question = models.ForeignKey(ICQuestion, on_delete=models.CASCADE, related_name="responses")
|
||||||
|
answer = models.TextField()
|
||||||
|
evidence = models.JSONField(default=list, blank=True)
|
||||||
|
uncertainty = models.TextField(blank=True)
|
||||||
|
pitch_changes = models.JSONField(default=dict, blank=True)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ICDecision(TimestampedModel):
|
||||||
|
diligence = models.OneToOneField(ICDiligence, on_delete=models.CASCADE, related_name="decision")
|
||||||
|
decision = models.CharField(max_length=32, choices=ICDecisionType.choices)
|
||||||
|
component_scores = models.JSONField(default=dict, blank=True)
|
||||||
|
composite_score = models.FloatField(default=0.0)
|
||||||
|
probability_500_within_30_days = models.FloatField(default=0.0)
|
||||||
|
initial_tranche = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||||
|
validation_condition = models.TextField(blank=True)
|
||||||
|
evidence_required = models.JSONField(default=list, blank=True)
|
||||||
|
recommended_allocation = models.JSONField(default=dict, blank=True)
|
||||||
|
kill_criteria = models.JSONField(default=list, blank=True)
|
||||||
|
next_decision_point = models.TextField(blank=True)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
evidence_tier = models.CharField(max_length=40, choices=EvidenceTier.choices, default=EvidenceTier.TIER_0_THESIS)
|
||||||
|
raw_probability_500_within_30_days = models.FloatField(default=0.0)
|
||||||
|
evidence_ceiling = models.FloatField(default=35.0)
|
||||||
|
probability_explanation = models.TextField(blank=True)
|
||||||
|
score_definitions = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class VentureThesisFingerprint(TimestampedModel):
|
||||||
|
proposal = models.OneToOneField(CompanyProposal, on_delete=models.CASCADE, related_name="fingerprint")
|
||||||
|
industry = models.CharField(max_length=160)
|
||||||
|
icp = models.TextField()
|
||||||
|
problem = models.TextField()
|
||||||
|
offer = models.TextField()
|
||||||
|
business_model = models.CharField(max_length=160)
|
||||||
|
primary_distribution_channel = models.CharField(max_length=160)
|
||||||
|
price_band = models.CharField(max_length=80)
|
||||||
|
time_to_first_cash_band = models.CharField(max_length=80)
|
||||||
|
required_capability_set = models.JSONField(default=list, blank=True)
|
||||||
|
geography_dependency = models.CharField(max_length=120, blank=True)
|
||||||
|
regulatory_dependency = models.CharField(max_length=120, blank=True)
|
||||||
|
online_offline = models.CharField(max_length=80, blank=True)
|
||||||
|
service_software_hybrid = models.CharField(max_length=80, blank=True)
|
||||||
|
fingerprint_hash = models.CharField(max_length=128)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class VentureCohort(TimestampedModel):
|
||||||
|
cohort_id = models.CharField(max_length=80, unique=True)
|
||||||
|
mandate = models.ForeignKey(CompanyMandate, on_delete=models.PROTECT, related_name="venture_cohorts")
|
||||||
|
cohort_size = models.PositiveIntegerField(default=10)
|
||||||
|
status = models.CharField(max_length=40, default="PREPARING")
|
||||||
|
graph_run = models.ForeignKey("graph.GraphRun", on_delete=models.SET_NULL, null=True, blank=True, related_name="venture_cohorts")
|
||||||
|
concurrency = models.PositiveIntegerField(default=1)
|
||||||
|
model_versions = models.JSONField(default=dict, blank=True)
|
||||||
|
graph_versions = models.JSONField(default=dict, blank=True)
|
||||||
|
research_policy = models.JSONField(default=dict, blank=True)
|
||||||
|
scoring_policy = models.JSONField(default=dict, blank=True)
|
||||||
|
evidence_calibration_policy = models.JSONField(default=dict, blank=True)
|
||||||
|
metrics = models.JSONField(default=dict, blank=True)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class VentureCohortMember(TimestampedModel):
|
||||||
|
cohort = models.ForeignKey(VentureCohort, on_delete=models.CASCADE, related_name="members")
|
||||||
|
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="cohort_memberships")
|
||||||
|
child_graph_run = models.ForeignKey("graph.GraphRun", on_delete=models.SET_NULL, null=True, blank=True, related_name="venture_cohort_members")
|
||||||
|
rank = models.PositiveIntegerField(null=True, blank=True)
|
||||||
|
is_top_3 = models.BooleanField(default=False)
|
||||||
|
portfolio_score = models.FloatField(default=0.0)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [models.UniqueConstraint(fields=["cohort", "proposal"], name="unique_venture_cohort_member")]
|
||||||
|
|
||||||
|
|
||||||
|
class VentureCollision(TimestampedModel):
|
||||||
|
cohort = models.ForeignKey(VentureCohort, on_delete=models.CASCADE, related_name="collisions")
|
||||||
|
company_a = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="collisions_as_a")
|
||||||
|
company_b = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="collisions_as_b")
|
||||||
|
classification = models.CharField(max_length=40, choices=OverlapClassification.choices)
|
||||||
|
similarity_score = models.FloatField(default=0.0)
|
||||||
|
explanation = models.TextField(blank=True)
|
||||||
|
overlapping_dimensions = models.JSONField(default=list, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioICReview(TimestampedModel):
|
||||||
|
cohort = models.OneToOneField(VentureCohort, on_delete=models.CASCADE, related_name="portfolio_review")
|
||||||
|
rankings = models.JSONField(default=list, blank=True)
|
||||||
|
top_3 = models.JSONField(default=list, blank=True)
|
||||||
|
concentration = models.JSONField(default=dict, blank=True)
|
||||||
|
capability_demand = models.JSONField(default=list, blank=True)
|
||||||
|
top_3_capability_gaps = models.JSONField(default=list, blank=True)
|
||||||
|
recommended_build_priorities = models.JSONField(default=list, blank=True)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class VentureCapabilityDemand(TimestampedModel):
|
||||||
|
cohort = models.ForeignKey(VentureCohort, on_delete=models.CASCADE, related_name="capability_demands")
|
||||||
|
capability = models.CharField(max_length=120)
|
||||||
|
count = models.PositiveIntegerField(default=0)
|
||||||
|
percentage = models.FloatField(default=0.0)
|
||||||
|
earliest_stage = models.CharField(max_length=32, choices=CapabilityPriority.choices)
|
||||||
|
companies = models.JSONField(default=list, blank=True)
|
||||||
|
status_distribution = models.JSONField(default=dict, blank=True)
|
||||||
|
top_3_count = models.PositiveIntegerField(default=0)
|
||||||
|
priority_score = models.FloatField(default=0.0)
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyCapabilityRequirement(TimestampedModel):
|
||||||
|
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="capability_requirements")
|
||||||
|
category = models.CharField(max_length=120)
|
||||||
|
status = models.CharField(max_length=32, choices=CapabilityStatus.choices)
|
||||||
|
rationale = models.TextField(blank=True)
|
||||||
|
priority = models.CharField(max_length=32, choices=CapabilityPriority.choices, default=CapabilityPriority.BEFORE_SCALING)
|
||||||
|
evidence = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioCapabilityGap(TimestampedModel):
|
||||||
|
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="capability_gap_reports")
|
||||||
|
available = models.JSONField(default=list, blank=True)
|
||||||
|
partial = models.JSONField(default=list, blank=True)
|
||||||
|
missing = models.JSONField(default=list, blank=True)
|
||||||
|
ranked_missing = models.JSONField(default=list, blank=True)
|
||||||
|
report = models.TextField(blank=True)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class VentureArtifact(TimestampedModel):
|
||||||
|
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, null=True, blank=True, related_name="artifacts")
|
||||||
|
mandate = models.ForeignKey(CompanyMandate, on_delete=models.CASCADE, null=True, blank=True, related_name="artifacts")
|
||||||
|
graph_run = models.ForeignKey("graph.GraphRun", on_delete=models.SET_NULL, null=True, blank=True, related_name="venture_artifacts")
|
||||||
|
artifact_type = models.CharField(max_length=80)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
content = models.JSONField(default=dict, blank=True)
|
||||||
|
readable = models.TextField(blank=True)
|
||||||
|
generated_by = models.CharField(max_length=120, blank=True)
|
||||||
289
docs/venture_discovery_v0_company_proposal.md
Normal file
289
docs/venture_discovery_v0_company_proposal.md
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
# Venture Discovery V0: Single Company Proposal
|
||||||
|
|
||||||
|
Source run: `venture_research_dogfood_v3.sqlite3`
|
||||||
|
Graph: `venture_discovery v1`
|
||||||
|
GraphRun: `1`
|
||||||
|
Company count: `1`
|
||||||
|
Spend: `$0`
|
||||||
|
Customer outreach: `none`
|
||||||
|
|
||||||
|
## 1. Sol Thesis
|
||||||
|
|
||||||
|
### Company
|
||||||
|
|
||||||
|
**Compliance Micro-Audit for Shopify Stores**
|
||||||
|
|
||||||
|
### One-Line Thesis
|
||||||
|
|
||||||
|
Sell a fast, fixed-price audit that finds obvious legal, trust, and conversion gaps in small Shopify stores before they lose customers or face complaints.
|
||||||
|
|
||||||
|
### Proposal
|
||||||
|
|
||||||
|
**Description:** A productized service delivering a 1-page audit of a Shopify store's refund policy, contact visibility, shipping promises, trust signals, checkout friction, and basic accessibility issues within 24 hours.
|
||||||
|
|
||||||
|
**Problem:** Small Shopify merchants often launch with missing or weak trust and compliance basics, which can reduce conversion and create avoidable disputes, but they do not want to hire an agency or lawyer for a broad review.
|
||||||
|
|
||||||
|
**ICP:** Solo Shopify store owners doing `$1k-$30k/month` in revenue, especially stores selling physical products with unclear shipping, refund, or contact information.
|
||||||
|
|
||||||
|
**Solution:** Offer a `$99` fixed-scope micro-audit with a concise checklist, screenshots, severity ratings, and 5 prioritized fixes. Delivery can be manual using public store pages, browser inspection, and standardized templates.
|
||||||
|
|
||||||
|
**Business model:** One-time productized service with optional `$199` implementation support upsell for merchants who want fixes made in Shopify admin or theme settings.
|
||||||
|
|
||||||
|
**Pricing hypothesis:** `$99` is low enough for impulse purchase by revenue-generating merchants and high enough that 6 sales can exceed `$500` net new cash with near-zero marginal cost.
|
||||||
|
|
||||||
|
**Validation plan:** V0 uses no spend and no outreach. Validate only through desk research: review Shopify forum threads, Reddit posts, app reviews, freelancer listings, and public store teardown content to confirm merchants complain about conversion, refund disputes, policy confusion, and trust issues. Build a mock audit template, score 5 public stores privately without contacting them, estimate delivery time, and define pass/fail criteria: at least 20 credible public demand signals, at least 3 existing paid alternatives, and audit production time under 45 minutes. If passed, spend up to `$50` after V0 on a landing page/domain or marketplace listing and attempt first paid sales through permitted inbound channels.
|
||||||
|
|
||||||
|
**Confidence:** `0.74`
|
||||||
|
|
||||||
|
**Status:** `FUNDED_RECOMMENDED`
|
||||||
|
|
||||||
|
## 2. Source-Linked Market Research
|
||||||
|
|
||||||
|
Coverage: `100%`
|
||||||
|
|
||||||
|
Covered categories: competitors, pricing, customer pain, market alternatives, regulatory/platform risks.
|
||||||
|
|
||||||
|
### Sources
|
||||||
|
|
||||||
|
| Category | Source | Evidence Used |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Pricing | [Swift Web Solutions: Shopify CRO audit cost](https://swiftweb.dev/blog/shopify-cro-audit-cost-2026) | CRO audits positioned from free sales-call audits to `$1,500-$3,000` tactical audits, `$3,500-$7,500` strategic audits, and `$10,000+` enterprise audits. |
|
||||||
|
| Pricing | [ShopExperts: Shopify CRO cost](https://shopexperts.com/help/pricing/shopify-cro-cost) | One-time conversion audits listed at `$500-$3,000`; audit plus implementation at `$2,000-$8,000`; retainers from `$2,500/month`. |
|
||||||
|
| Market alternatives | [Fiverr: Shopify audit search](https://www.fiverr.com/search/gigs?query=shopify%20audit) | Existing marketplace supply for Shopify audits, including low-cost productized alternatives around `$20-$350+`. |
|
||||||
|
| Competitors | [Solcro pricing](https://solcro.com/pricing/) | SaaS competitor with free and paid Shopify audit plans, including Starter `$40/month`, Pro `$99/month`, and Agency `$300/month`. |
|
||||||
|
| Competitors | [AuditCRO](https://www.auditcro.io/) | AI audit alternative positioned around free Shopify CRO audits. |
|
||||||
|
| Customer pain | [Shopify Community: merchant return/shipping details issue](https://community.shopify.com/t/will-shopify-solve-the-hasmerchantreturnpolicy-and-shippingdetails-missing-issue-in-near-future/213709) | Merchants complain about missing `hasMerchantReturnPolicy` and `shippingDetails`, Google/Search Console warnings, perceived sales/traffic impact, and unclear remediation. |
|
||||||
|
| Pricing | [Shopify Community: CRO audit vs full build pricing](https://community.shopify.com/t/real-talk-how-do-you-price-a-cro-audit-vs-a-full-build/654290) | Community discussion frames CRO audits as flat-fee strategic deliverables separate from implementation/build work. |
|
||||||
|
| Regulatory/platform risk | [Shopify Help: adding store policies](https://help.shopify.com/en/manual/checkout-settings/refund-privacy-tos) | Shopify lets merchants add/generate policies but says merchants are responsible for following policies and suggests local legal expert help. |
|
||||||
|
| Regulatory/platform risk | [Shopify Terms of Service](https://www.shopify.com/legal/terms) | Merchants are responsible for public contact info, terms, refund/shipping policies, disclosures, customer transactions, and compliance. Automated scraping restrictions matter. |
|
||||||
|
| Market alternatives | [Shopify App Store trust badges search](https://apps.shopify.com/search?q=trust%20badges) | App-based alternatives exist for trust badges, product reviews, SEO, shipping, and store-design optimization. |
|
||||||
|
|
||||||
|
### Research Findings
|
||||||
|
|
||||||
|
**Competitors:** Direct SaaS competitors exist, including Solcro and AuditCRO. Human-service competitors exist across Fiverr and specialist CRO/freelance providers.
|
||||||
|
|
||||||
|
**Pricing:** The `$99` audit is far below agency tactical audit ranges but within freelancer-market impulse-buy territory. The `$199` implementation upsell must remain tightly scoped to avoid margin collapse.
|
||||||
|
|
||||||
|
**Customer pain:** Shopify merchants publicly complain about confusing policy/structured-data/search warnings and uncertainty over whether Shopify, Google, theme code, or Merchant Center settings are responsible.
|
||||||
|
|
||||||
|
**Alternatives:** DIY Shopify policy tools, Shopify apps, Fiverr audits, agencies, and automated AI audit tools all exist.
|
||||||
|
|
||||||
|
**Regulatory/platform risks:** The service must not present itself as legal advice. It should use public/manual inspection unless explicit customer access is granted. Shopify ToS restrictions make scraping/automation risky.
|
||||||
|
|
||||||
|
## 3. Board Critique
|
||||||
|
|
||||||
|
### CEO
|
||||||
|
|
||||||
|
- Mandate fit is strongest if validation sells a narrow paid audit before product build.
|
||||||
|
- Keep the first dollar path service-led, not SaaS-led.
|
||||||
|
|
||||||
|
### CTO
|
||||||
|
|
||||||
|
- Build can use existing Artifex analysis, reporting, and frontend capabilities.
|
||||||
|
- Avoid integrations until willingness-to-pay evidence exists.
|
||||||
|
|
||||||
|
### CFO
|
||||||
|
|
||||||
|
- `$50` cap is adequate only for lightweight landing page/listing tests, not paid acquisition learning.
|
||||||
|
- High margin is plausible because delivery is mostly labor/compute already available.
|
||||||
|
|
||||||
|
### CRO
|
||||||
|
|
||||||
|
- Founder/operator communities are reachable manually, but V0 cannot contact customers.
|
||||||
|
- Pricing must start as a paid diagnostic to avoid long SaaS evaluation cycles.
|
||||||
|
|
||||||
|
### Independent Director
|
||||||
|
|
||||||
|
- Demand evidence is weak without public research or customer conversations.
|
||||||
|
- The company must prove urgency before building automation.
|
||||||
|
|
||||||
|
### Board Output
|
||||||
|
|
||||||
|
**Strengths:**
|
||||||
|
|
||||||
|
- Service-led revenue path can precede product build.
|
||||||
|
- Uses current Artifex planning, engineering, frontend, review, graph, and agent-control capabilities.
|
||||||
|
- Small validation budget aligns with a narrow paid offer.
|
||||||
|
|
||||||
|
**Weaknesses:**
|
||||||
|
|
||||||
|
- No customer outreach or paid test has occurred.
|
||||||
|
- First customers may require trust and examples before paying.
|
||||||
|
- The offer can look like generic consulting unless anchored to an urgent merchant problem.
|
||||||
|
|
||||||
|
**Required revisions:**
|
||||||
|
|
||||||
|
- Frame offer as a productized validation/compliance audit with optional Artifex-assisted fix plan.
|
||||||
|
- Make falsification criteria explicit before spend.
|
||||||
|
|
||||||
|
**Recommendation:** `PROCEED_TO_IC_WITH_REVISIONS`
|
||||||
|
|
||||||
|
## 4. IC Questions
|
||||||
|
|
||||||
|
1. What evidence supports demand for Compliance Micro-Audit for Shopify Stores among the stated ICP?
|
||||||
|
2. Why will this customer pay now instead of using free templates or advice?
|
||||||
|
3. How do you reach the first 10 customers without spam or fake traction?
|
||||||
|
4. Can this be validated before building the full product?
|
||||||
|
5. Why a productized service first instead of SaaS?
|
||||||
|
6. What would falsify the thesis within the `$50` and 30-day mandate?
|
||||||
|
7. What happens if acquisition cost or manual delivery time is 3x the estimate?
|
||||||
|
8. What is the main competitive threat and why is this worth funding over selling an existing Artifex capability?
|
||||||
|
|
||||||
|
## 5. Sol / Company Responses
|
||||||
|
|
||||||
|
**Demand evidence:** Demand is not proven. The strongest V0 claim is that the problem is plausible and cheap to test, not that demand exists. Public evidence supports merchant confusion, existing paid alternatives, and recognizable audit pricing, but no customer has been contacted.
|
||||||
|
|
||||||
|
**Why pay now:** The buyer pays only if the report saves time, prevents avoidable disputes, or improves conversion/trust before further traffic is wasted. Urgency remains weakest before a concrete launch or sales problem.
|
||||||
|
|
||||||
|
**First 10 customers:** After approval, use targeted compliant posts/conversations and track responses manually. V0 performs no outreach.
|
||||||
|
|
||||||
|
**Validate before build:** Yes. The paid diagnostic can be validated with responses and willingness-to-pay before software build.
|
||||||
|
|
||||||
|
**Why service first:** Service first reduces build risk and can reach first cash faster than SaaS. Software should follow only if repeated demand appears.
|
||||||
|
|
||||||
|
**Falsification:** Failure to collect credible responses or willingness-to-pay within the mandate falsifies near-term viability.
|
||||||
|
|
||||||
|
**If CAC/time is 3x:** Stop, raise price, or narrow scope before building tooling.
|
||||||
|
|
||||||
|
**Competitive threat:** Main threats are free/cheap templates, Fiverr audits, CRO agencies, and AI audit SaaS. This is worth funding over selling raw Artifex capability only if packaged as a merchant-specific outcome.
|
||||||
|
|
||||||
|
## 6. Red-Team Challenge
|
||||||
|
|
||||||
|
- The offer may be perceived as generic consulting unless anchored to a painful, immediate decision.
|
||||||
|
- Without customer contact, demand remains a hypothesis even with public research.
|
||||||
|
- Manual distribution could fail if the audience distrusts AI-generated audits.
|
||||||
|
|
||||||
|
### Final Company Response
|
||||||
|
|
||||||
|
**Narrowed ICP:** Solo technical founders and small service operators deciding whether to spend time building an AI-assisted microbusiness. For this Shopify-specific proposal, the operational ICP is solo Shopify store owners doing `$1k-$30k/month` in revenue with visible policy, shipping, refund, contact, or trust gaps.
|
||||||
|
|
||||||
|
**Validation gate:** Before any spend, collect 5 credible target-customer responses or 1 explicit willingness-to-pay signal through compliant non-spam channels.
|
||||||
|
|
||||||
|
**Kill criteria:**
|
||||||
|
|
||||||
|
- No credible responses after 10 targeted, compliant conversations/posts once outreach is approved.
|
||||||
|
- No willingness-to-pay signal at `$49-$99`.
|
||||||
|
- Customers only want free advice, not a paid report.
|
||||||
|
|
||||||
|
## 7. IC Score
|
||||||
|
|
||||||
|
Composite IC score: `67.1/100`
|
||||||
|
Probability of `$500` within 30 days: `71%`
|
||||||
|
Decision: `CONDITIONAL_FUND`
|
||||||
|
Initial tranche: `$10`
|
||||||
|
Remaining reserved: `$40`
|
||||||
|
No spend in V0: `true`
|
||||||
|
|
||||||
|
| Dimension | Score |
|
||||||
|
| --- | ---: |
|
||||||
|
| Demand evidence | 85 |
|
||||||
|
| Time to first dollar | 76 |
|
||||||
|
| Capital efficiency | 82 |
|
||||||
|
| Validation cost | 42 |
|
||||||
|
| Gross margin | 85 |
|
||||||
|
| Distribution difficulty | 60 |
|
||||||
|
| Build complexity | 42 |
|
||||||
|
| Defensibility | 60 |
|
||||||
|
| Market size | 62 |
|
||||||
|
| Competition | 68 |
|
||||||
|
| Risk | 72 |
|
||||||
|
| Probability of reaching `$500` | 71 |
|
||||||
|
|
||||||
|
### How Research Changed the Decision
|
||||||
|
|
||||||
|
Before source-linked research, the improved process downgraded a live run with zero research coverage to `REVISE_AND_RESUBMIT`, with demand evidence `25`, competition `38`, risk `34`, and probability `43%`.
|
||||||
|
|
||||||
|
After source-linked research reached full coverage across competitors, pricing, customer pain, alternatives, and regulatory/platform risks, IC moved to `CONDITIONAL_FUND`. Demand evidence rose to `85`, competition to `68`, risk to `72`, and probability of `$500 within 30 days` to `71%`.
|
||||||
|
|
||||||
|
## 8. Capability Analysis
|
||||||
|
|
||||||
|
### Available
|
||||||
|
|
||||||
|
- Board
|
||||||
|
- IC
|
||||||
|
- WEB_MARKET_RESEARCH
|
||||||
|
- software build
|
||||||
|
- frontend design
|
||||||
|
|
||||||
|
### Partial
|
||||||
|
|
||||||
|
- Company Brain
|
||||||
|
- deployment
|
||||||
|
|
||||||
|
### Missing
|
||||||
|
|
||||||
|
- outbound sales
|
||||||
|
- CRM
|
||||||
|
- payments
|
||||||
|
- invoicing
|
||||||
|
- customer support
|
||||||
|
- company budget management
|
||||||
|
- legal/compliance
|
||||||
|
|
||||||
|
### Next Artifex Capabilities Required
|
||||||
|
|
||||||
|
1. outbound sales (`BEFORE_VALIDATION`): No compliant outreach/sequence/customer contact system exists and V0 forbids outreach.
|
||||||
|
2. CRM (`BEFORE_VALIDATION`): No customer pipeline/contact tracking exists.
|
||||||
|
3. company budget management (`BEFORE_VALIDATION`): V0 blocks spend but future validation needs tranche/budget controls.
|
||||||
|
4. payments (`BEFORE_FIRST_CUSTOMER`): No payment collection system exists.
|
||||||
|
5. invoicing (`BEFORE_FIRST_CUSTOMER`): No invoicing workflow exists.
|
||||||
|
6. legal/compliance (`BEFORE_FIRST_CUSTOMER`): No contracts, terms, privacy, or compliance review workflow exists.
|
||||||
|
7. customer support (`BEFORE_SCALING`): No support inbox or customer service workflow exists.
|
||||||
|
|
||||||
|
## 9. Final IC Memo
|
||||||
|
|
||||||
|
### Company
|
||||||
|
|
||||||
|
Compliance Micro-Audit for Shopify Stores
|
||||||
|
|
||||||
|
### Thesis
|
||||||
|
|
||||||
|
Sell a fast, fixed-price audit that finds obvious legal, trust, and conversion gaps in small Shopify stores before they lose customers or face complaints.
|
||||||
|
|
||||||
|
### Mandate
|
||||||
|
|
||||||
|
Design a business that could plausibly turn at most `$50` of external validation capital into `$500` net new cash.
|
||||||
|
|
||||||
|
### Requested Capital
|
||||||
|
|
||||||
|
`$50`
|
||||||
|
|
||||||
|
### Recommended Allocation
|
||||||
|
|
||||||
|
- Initial tranche: `$10`
|
||||||
|
- Remaining reserved: `$40`
|
||||||
|
- No spend in V0: true
|
||||||
|
|
||||||
|
### IC Decision
|
||||||
|
|
||||||
|
`CONDITIONAL_FUND`
|
||||||
|
|
||||||
|
### Why It May Work
|
||||||
|
|
||||||
|
- Revenue path starts with a paid diagnostic, not a full SaaS build.
|
||||||
|
- Artifex has planning, engineering, frontend, review, graph, and agent-control capabilities already.
|
||||||
|
- Validation budget can be gated behind evidence.
|
||||||
|
- Source-linked evidence confirms existing pricing, alternatives, competitors, customer pain, and platform/legal risk categories.
|
||||||
|
|
||||||
|
### Why It May Fail
|
||||||
|
|
||||||
|
- The offer may be perceived as generic consulting unless anchored to a painful, immediate decision.
|
||||||
|
- Public research is not the same as customer willingness-to-pay.
|
||||||
|
- Manual distribution could fail if Shopify merchants distrust AI-generated audits.
|
||||||
|
- Legal/compliance positioning must be controlled carefully.
|
||||||
|
|
||||||
|
### Validation Gates
|
||||||
|
|
||||||
|
- Obtain 5 credible target-customer responses or 1 explicit willingness-to-pay signal before any build or further spend.
|
||||||
|
- Do not spend beyond the initial tranche until evidence is reviewed.
|
||||||
|
- Do not contact customers until outreach capability and approval exist.
|
||||||
|
|
||||||
|
### Kill Criteria
|
||||||
|
|
||||||
|
- No credible responses after 10 targeted, compliant conversations/posts once outreach is approved.
|
||||||
|
- No willingness-to-pay signal at `$49-$99`.
|
||||||
|
- Customers only want free advice, not a paid report.
|
||||||
|
|
||||||
|
### Next Decision Point
|
||||||
|
|
||||||
|
After validation evidence is collected and before any real spend or customer delivery.
|
||||||
|
|
@ -9,6 +9,8 @@ from graph.roadmap import project_roadmap_review_graph_v1
|
||||||
from graph.scenario_lab import scenario_lab_graph_v1
|
from graph.scenario_lab import scenario_lab_graph_v1
|
||||||
from graph.steward import steward_run_graph_v1
|
from graph.steward import steward_run_graph_v1
|
||||||
from graph.task_execution import task_execution_graph_v1
|
from graph.task_execution import task_execution_graph_v1
|
||||||
|
from graph.venture_cohort import venture_discovery_cohort_graph_v1
|
||||||
|
from graph.venture_discovery import venture_discovery_graph_v1
|
||||||
|
|
||||||
|
|
||||||
def champion_task_execution_graph_v1() -> ExecutionGraphVersion:
|
def champion_task_execution_graph_v1() -> ExecutionGraphVersion:
|
||||||
|
|
@ -96,3 +98,11 @@ def champion_scenario_lab_graph_v1() -> ExecutionGraphVersion:
|
||||||
|
|
||||||
def champion_agent_investigation_graph_v1() -> ExecutionGraphVersion:
|
def champion_agent_investigation_graph_v1() -> ExecutionGraphVersion:
|
||||||
return _champion_graph(agent_investigation_graph_v1())
|
return _champion_graph(agent_investigation_graph_v1())
|
||||||
|
|
||||||
|
|
||||||
|
def champion_venture_discovery_graph_v1() -> ExecutionGraphVersion:
|
||||||
|
return _champion_graph(venture_discovery_graph_v1())
|
||||||
|
|
||||||
|
|
||||||
|
def champion_venture_discovery_cohort_graph_v1() -> ExecutionGraphVersion:
|
||||||
|
return _champion_graph(venture_discovery_cohort_graph_v1())
|
||||||
|
|
|
||||||
107
graph/venture_cohort.py
Normal file
107
graph/venture_cohort.py
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from agents.venture_discovery import VentureDiscoveryService
|
||||||
|
from control_plane.ventures.models import VentureCohort
|
||||||
|
from graph.native_runtime import GraphExecutionContext
|
||||||
|
from graph.registry import NodeHandlerRegistry, NodeResult
|
||||||
|
from graph.spec import ExecutionGraphSpec, GraphEdgeSpec, GraphNodeSpec
|
||||||
|
|
||||||
|
|
||||||
|
def venture_discovery_cohort_graph_v1() -> ExecutionGraphSpec:
|
||||||
|
nodes = ["prepare_cohort", "generate_independent_proposals", "initial_research", "fingerprint_theses", "collision_analysis", "run_individual_diligence", "portfolio_compare", "portfolio_ic", "aggregate_capabilities", "produce_cohort_report", "complete"]
|
||||||
|
spec = ExecutionGraphSpec(name="venture_discovery_cohort", version=1, graph_type="VENTURE_DISCOVERY_COHORT", entry="prepare_cohort", nodes={node: GraphNodeSpec(node, node if node == "complete" else f"venture_cohort_{node}") for node in nodes}, edges=[GraphEdgeSpec(nodes[index], nodes[index + 1], "success") for index in range(len(nodes) - 1)], terminal_nodes=["complete"], metadata={"description": "Venture Discovery V0.2: 10-company independent cohort, portfolio IC, collisions, capability demand, and report."})
|
||||||
|
spec.validate()
|
||||||
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
class CohortNode:
|
||||||
|
idempotent = True
|
||||||
|
replay_safe = True
|
||||||
|
destructive = False
|
||||||
|
|
||||||
|
def __init__(self, service: VentureDiscoveryService, node_type: str, *, cohort_size: int = 10, concurrency: int = 1) -> None:
|
||||||
|
self.service = service
|
||||||
|
self.node_type = node_type
|
||||||
|
self.cohort_size = cohort_size
|
||||||
|
self.concurrency = concurrency
|
||||||
|
|
||||||
|
def cohort(self, context: GraphExecutionContext) -> VentureCohort:
|
||||||
|
return VentureCohort.objects.get(id=context.graph_run.metadata["cohort_id"])
|
||||||
|
|
||||||
|
|
||||||
|
class PrepareCohortNode(CohortNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
cohort = self.service.prepare_cohort(size=self.cohort_size, graph_run=context.graph_run, concurrency=self.concurrency)
|
||||||
|
metadata = dict(context.graph_run.metadata)
|
||||||
|
metadata["cohort_id"] = str(cohort.id)
|
||||||
|
metadata["cohort_stable_id"] = cohort.cohort_id
|
||||||
|
metadata["no_real_spend"] = True
|
||||||
|
metadata["no_real_customer_outreach"] = True
|
||||||
|
context.graph_run.metadata = metadata
|
||||||
|
context.graph_run.save(update_fields=["metadata", "updated_at"])
|
||||||
|
return NodeResult("COMPLETE", "success", {"cohort_id": cohort.cohort_id, "size": cohort.cohort_size})
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateIndependentNode(CohortNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
proposals = self.service.generate_independent_proposals(self.cohort(context))
|
||||||
|
return NodeResult("COMPLETE", "success", {"proposal_count": len(proposals), "independent_generation": True})
|
||||||
|
|
||||||
|
|
||||||
|
class ResearchNode(CohortNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
cohort = self.cohort(context)
|
||||||
|
self.service.research_cohort(cohort)
|
||||||
|
cohort.refresh_from_db()
|
||||||
|
return NodeResult("COMPLETE", "success", cohort.metrics)
|
||||||
|
|
||||||
|
|
||||||
|
class FingerprintNode(CohortNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
fingerprints = self.service.fingerprint_cohort(self.cohort(context))
|
||||||
|
return NodeResult("COMPLETE", "success", {"fingerprint_count": len(fingerprints)})
|
||||||
|
|
||||||
|
|
||||||
|
class CollisionNode(CohortNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
collisions = self.service.analyze_collisions(self.cohort(context))
|
||||||
|
return NodeResult("COMPLETE", "success", {"pair_count": len(collisions)})
|
||||||
|
|
||||||
|
|
||||||
|
class DiligenceNode(CohortNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
cohort = self.cohort(context)
|
||||||
|
self.service.run_individual_diligence_for_cohort(cohort)
|
||||||
|
return NodeResult("COMPLETE", "success", {"diligence_count": cohort.members.count()})
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioNode(CohortNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
review = self.service.portfolio_ic(self.cohort(context))
|
||||||
|
return NodeResult("COMPLETE", "success", {"ranked_count": len(review.rankings), "top_3": review.top_3})
|
||||||
|
|
||||||
|
|
||||||
|
class CapabilityNode(CohortNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
cohort = self.cohort(context)
|
||||||
|
demand = self.service.aggregate_capability_demand(cohort)
|
||||||
|
top_3 = self.service.aggregate_capability_demand(cohort, top_3_only=True)
|
||||||
|
return NodeResult("COMPLETE", "success", {"capability_count": len(demand), "top_3_gap_count": len(top_3)})
|
||||||
|
|
||||||
|
|
||||||
|
class ReportNode(CohortNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
artifact = self.service.produce_cohort_report(self.cohort(context))
|
||||||
|
return NodeResult("COMPLETE", "success", {"artifact_id": str(artifact.id), "artifact_type": artifact.artifact_type})
|
||||||
|
|
||||||
|
|
||||||
|
class NoopNode(CohortNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
return NodeResult("COMPLETE", "success")
|
||||||
|
|
||||||
|
|
||||||
|
def venture_discovery_cohort_registry(service: VentureDiscoveryService, *, cohort_size: int = 10, concurrency: int = 1) -> NodeHandlerRegistry:
|
||||||
|
registry = NodeHandlerRegistry()
|
||||||
|
for handler in [PrepareCohortNode(service, "venture_cohort_prepare_cohort", cohort_size=cohort_size, concurrency=concurrency), GenerateIndependentNode(service, "venture_cohort_generate_independent_proposals"), ResearchNode(service, "venture_cohort_initial_research"), FingerprintNode(service, "venture_cohort_fingerprint_theses"), CollisionNode(service, "venture_cohort_collision_analysis"), DiligenceNode(service, "venture_cohort_run_individual_diligence"), NoopNode(service, "venture_cohort_portfolio_compare"), PortfolioNode(service, "venture_cohort_portfolio_ic"), CapabilityNode(service, "venture_cohort_aggregate_capabilities"), ReportNode(service, "venture_cohort_produce_cohort_report")]:
|
||||||
|
registry.register(handler)
|
||||||
|
return registry
|
||||||
163
graph/venture_discovery.py
Normal file
163
graph/venture_discovery.py
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from agents.venture_discovery import VentureDiscoveryService
|
||||||
|
from control_plane.ventures.models import CompanyMandate, CompanyProposal, ICDiligence, ICDecision, PortfolioCapabilityGap
|
||||||
|
from graph.native_runtime import GraphExecutionContext
|
||||||
|
from graph.registry import NodeHandlerRegistry, NodeResult
|
||||||
|
from graph.spec import ExecutionGraphSpec, GraphEdgeSpec, GraphNodeSpec
|
||||||
|
|
||||||
|
|
||||||
|
def venture_discovery_graph_v1() -> ExecutionGraphSpec:
|
||||||
|
nodes = ["prepare_mandate", "generate_company", "conduct_research", "board_review", "revise_pitch", "ic_first_pass", "generate_questions", "company_response", "red_team", "final_response", "score", "ic_decision", "capability_analysis", "produce_investment_memo", "complete"]
|
||||||
|
spec = ExecutionGraphSpec(
|
||||||
|
name="venture_discovery",
|
||||||
|
version=1,
|
||||||
|
graph_type="VENTURE_DISCOVERY",
|
||||||
|
entry="prepare_mandate",
|
||||||
|
nodes={node: GraphNodeSpec(node, node if node == "complete" else f"venture_{node}") for node in nodes},
|
||||||
|
edges=[GraphEdgeSpec(nodes[index], nodes[index + 1], "success") for index in range(len(nodes) - 1)],
|
||||||
|
terminal_nodes=["complete"],
|
||||||
|
metadata={"description": "Venture Discovery V0: exactly one startup proposal through Board, IC diligence, scoring, memo, and capability-gap analysis."},
|
||||||
|
)
|
||||||
|
spec.validate()
|
||||||
|
return spec
|
||||||
|
|
||||||
|
|
||||||
|
class VentureNode:
|
||||||
|
idempotent = True
|
||||||
|
replay_safe = True
|
||||||
|
destructive = False
|
||||||
|
|
||||||
|
def __init__(self, service: VentureDiscoveryService, node_type: str) -> None:
|
||||||
|
self.service = service
|
||||||
|
self.node_type = node_type
|
||||||
|
|
||||||
|
def mandate(self, context: GraphExecutionContext) -> CompanyMandate:
|
||||||
|
return CompanyMandate.objects.get(id=context.graph_run.metadata["mandate_id"])
|
||||||
|
|
||||||
|
def proposal(self, context: GraphExecutionContext) -> CompanyProposal:
|
||||||
|
return CompanyProposal.objects.get(id=context.graph_run.metadata["proposal_id"])
|
||||||
|
|
||||||
|
def diligence(self, context: GraphExecutionContext) -> ICDiligence:
|
||||||
|
return ICDiligence.objects.get(id=context.graph_run.metadata["diligence_id"])
|
||||||
|
|
||||||
|
def gap(self, context: GraphExecutionContext) -> PortfolioCapabilityGap:
|
||||||
|
return PortfolioCapabilityGap.objects.get(id=context.graph_run.metadata["capability_gap_id"])
|
||||||
|
|
||||||
|
|
||||||
|
class PrepareMandateNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
mandate = self.service.create_v0_mandate()
|
||||||
|
metadata = dict(context.graph_run.metadata)
|
||||||
|
metadata["mandate_id"] = str(mandate.id)
|
||||||
|
metadata["exactly_one_company_required"] = True
|
||||||
|
metadata["no_real_spend"] = True
|
||||||
|
metadata["no_real_customer_outreach"] = True
|
||||||
|
context.graph_run.metadata = metadata
|
||||||
|
context.graph_run.save(update_fields=["metadata", "updated_at"])
|
||||||
|
return NodeResult("COMPLETE", "success", {"mandate_id": str(mandate.id), "objective": mandate.objective})
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateCompanyNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
proposal = self.service.generate_single_company(self.mandate(context), graph_run=context.graph_run)
|
||||||
|
metadata = dict(context.graph_run.metadata)
|
||||||
|
metadata["proposal_id"] = str(proposal.id)
|
||||||
|
context.graph_run.metadata = metadata
|
||||||
|
context.graph_run.save(update_fields=["metadata", "updated_at"])
|
||||||
|
return NodeResult("COMPLETE", "success", {"proposal_id": str(proposal.id), "company": proposal.title, "generation_source": proposal.metadata.get("generation_source")})
|
||||||
|
|
||||||
|
|
||||||
|
class BoardReviewNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
review = self.service.board_review(self.proposal(context), graph_run=context.graph_run)
|
||||||
|
return NodeResult("COMPLETE", "success", {"board_review_id": str(review.id), "recommendation": review.recommendation})
|
||||||
|
|
||||||
|
|
||||||
|
class ResearchNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
research = self.service.conduct_market_research(self.proposal(context), graph_run=context.graph_run)
|
||||||
|
return NodeResult("COMPLETE", "success", {"source_count": len(research.get("sources", [])), "unverified_categories": research.get("unverified_categories", [])})
|
||||||
|
|
||||||
|
|
||||||
|
class RevisePitchNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
proposal = self.proposal(context)
|
||||||
|
latest = proposal.board_reviews.order_by("-created_at").first()
|
||||||
|
if latest and latest.revised_pitch:
|
||||||
|
proposal.pitch = latest.revised_pitch
|
||||||
|
proposal.save(update_fields=["pitch", "updated_at"])
|
||||||
|
return NodeResult("COMPLETE", "success", {"proposal_id": str(proposal.id), "pitch_revised": latest is not None})
|
||||||
|
|
||||||
|
|
||||||
|
class ICFirstPassNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
diligence = self.service.start_ic_diligence(self.proposal(context), graph_run=context.graph_run)
|
||||||
|
metadata = dict(context.graph_run.metadata)
|
||||||
|
metadata["diligence_id"] = str(diligence.id)
|
||||||
|
context.graph_run.metadata = metadata
|
||||||
|
context.graph_run.save(update_fields=["metadata", "updated_at"])
|
||||||
|
return NodeResult("COMPLETE", "success", {"diligence_id": str(diligence.id), "status": diligence.status})
|
||||||
|
|
||||||
|
|
||||||
|
class ICQuestionsNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
questions = self.service.generate_ic_questions(self.diligence(context), graph_run=context.graph_run)
|
||||||
|
return NodeResult("COMPLETE", "success", {"question_count": len(questions), "question_ids": [str(q.id) for q in questions]})
|
||||||
|
|
||||||
|
|
||||||
|
class CompanyResponseNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
responses = self.service.answer_questions(self.diligence(context), graph_run=context.graph_run)
|
||||||
|
return NodeResult("COMPLETE", "success", {"response_count": len(responses)})
|
||||||
|
|
||||||
|
|
||||||
|
class RedTeamNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
challenge = self.service.red_team(self.diligence(context), graph_run=context.graph_run)
|
||||||
|
return NodeResult("COMPLETE", "success", {"concern_count": len(challenge.get("concerns", []))})
|
||||||
|
|
||||||
|
|
||||||
|
class FinalResponseNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
response = self.service.final_company_response(self.diligence(context), graph_run=context.graph_run)
|
||||||
|
return NodeResult("COMPLETE", "success", {"kill_criteria_count": len(response.get("kill_criteria", []))})
|
||||||
|
|
||||||
|
|
||||||
|
class ScoreNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
decision = self.service.score_and_decide(self.diligence(context), graph_run=context.graph_run)
|
||||||
|
metadata = dict(context.graph_run.metadata)
|
||||||
|
metadata["decision_id"] = str(decision.id)
|
||||||
|
context.graph_run.metadata = metadata
|
||||||
|
context.graph_run.save(update_fields=["metadata", "updated_at"])
|
||||||
|
return NodeResult("COMPLETE", "success", {"decision_id": str(decision.id), "score": decision.composite_score})
|
||||||
|
|
||||||
|
|
||||||
|
class ICDecisionNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
decision = ICDecision.objects.get(id=context.graph_run.metadata["decision_id"])
|
||||||
|
return NodeResult("COMPLETE", "success", {"decision": decision.decision, "initial_tranche": str(decision.initial_tranche or "")})
|
||||||
|
|
||||||
|
|
||||||
|
class CapabilityAnalysisNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
gap = self.service.capability_analysis(self.proposal(context), graph_run=context.graph_run)
|
||||||
|
metadata = dict(context.graph_run.metadata)
|
||||||
|
metadata["capability_gap_id"] = str(gap.id)
|
||||||
|
context.graph_run.metadata = metadata
|
||||||
|
context.graph_run.save(update_fields=["metadata", "updated_at"])
|
||||||
|
return NodeResult("COMPLETE", "success", {"capability_gap_id": str(gap.id), "missing_count": len(gap.missing)})
|
||||||
|
|
||||||
|
|
||||||
|
class InvestmentMemoNode(VentureNode):
|
||||||
|
def run(self, context: GraphExecutionContext) -> NodeResult:
|
||||||
|
memo = self.service.produce_investment_memo(self.diligence(context), self.gap(context), graph_run=context.graph_run)
|
||||||
|
return NodeResult("COMPLETE", "success", {"memo_artifact_id": str(memo.id), "artifact_type": memo.artifact_type})
|
||||||
|
|
||||||
|
|
||||||
|
def venture_discovery_registry(service: VentureDiscoveryService) -> NodeHandlerRegistry:
|
||||||
|
registry = NodeHandlerRegistry()
|
||||||
|
for handler in [PrepareMandateNode(service, "venture_prepare_mandate"), GenerateCompanyNode(service, "venture_generate_company"), ResearchNode(service, "venture_conduct_research"), BoardReviewNode(service, "venture_board_review"), RevisePitchNode(service, "venture_revise_pitch"), ICFirstPassNode(service, "venture_ic_first_pass"), ICQuestionsNode(service, "venture_generate_questions"), CompanyResponseNode(service, "venture_company_response"), RedTeamNode(service, "venture_red_team"), FinalResponseNode(service, "venture_final_response"), ScoreNode(service, "venture_score"), ICDecisionNode(service, "venture_ic_decision"), CapabilityAnalysisNode(service, "venture_capability_analysis"), InvestmentMemoNode(service, "venture_produce_investment_memo")]:
|
||||||
|
registry.register(handler)
|
||||||
|
return registry
|
||||||
2
templates/control_plane/activity.html
Normal file
2
templates/control_plane/activity.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>Activity{% if project %} / {{ project.name }}{% endif %}</h2><p class="muted">Unified persisted event stream.</p></div></header><section class="panel">{% include "control_plane/partials/event_list.html" with events=events %}</section>{% endblock %}
|
||||||
2
templates/control_plane/agent_detail.html
Normal file
2
templates/control_plane/agent_detail.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>{{ version.agent }} v{{ version.version }}</h2><p>{{ version.system_contract }}</p></div><span class="badge {{ health.status }}">{{ health.status }}</span></header><section class="grid"><div class="panel"><h3>Champion / Challenger</h3><p>Status: {{ version.promotion_status }}</p><p>Benchmark: {{ version.benchmark_status }}</p><p>Parent: {{ version.parent_version_id|default:"none" }}</p><h4>Challengers</h4>{% for challenger in challengers.challengers %}<p>{{ challenger.agent }} v{{ challenger.version }}</p>{% empty %}<p>No challengers.</p>{% endfor %}</div><div class="panel"><h3>Model / Policies</h3><p>{{ version.model }}</p><pre>{{ version.context_policy }}</pre><pre>{{ version.tool_policy }}</pre></div><div class="panel"><h3>Health Reasons</h3>{% for reason in health.reasons %}<p>{{ reason }}</p>{% endfor %}</div></section><section class="grid"><div class="panel"><h3>Competencies</h3>{% for c in version.competencies %}<p>{{ c.competency__key }} / proficiency {{ c.proficiency }} / confidence {{ c.confidence }}</p>{% empty %}<p>No competencies.</p>{% endfor %}</div><div class="panel"><h3>Performance</h3><pre>{{ performance }}</pre></div><div class="panel"><h3>Usage</h3><pre>{{ usage }}</pre></div></section><section class="panel"><h3>Progeny</h3><pre>{{ progeny }}</pre></section>{% endblock %}
|
||||||
2
templates/control_plane/agents.html
Normal file
2
templates/control_plane/agents.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>Agent Control Room</h2><p class="muted">Governed workforce inventory, champion/challenger state, health, performance, and teams.</p></div></header><table><thead><tr><th>Agent</th><th>Role</th><th>Scope</th><th>Champion</th><th>Health</th><th>Performance</th></tr></thead><tbody>{% for agent in agents %}<tr><td>{{ agent.name }}<br><span class="muted">{{ agent.purpose }}</span></td><td>{{ agent.role }}</td><td>{{ agent.scope }}</td><td>{% if agent.champion_version %}<a href="{% url 'agent_detail' agent.champion_version %}">{{ agent.champion_version }}</a>{% else %}None{% endif %}</td><td><span class="badge {{ agent.health.status }}">{{ agent.health.status }}</span><br>{{ agent.health.reasons|join:", " }}</td><td>{% if agent.performance.quality %}Completion {{ agent.performance.quality.task_completion_rate|default:"n/a" }}<br>Test pass {{ agent.performance.quality.test_pass_rate|default:"n/a" }}{% else %}No data{% endif %}</td></tr>{% empty %}<tr><td colspan="6">No agents.</td></tr>{% endfor %}</tbody></table><section class="panel"><h3>Teams</h3>{% for team in teams %}<article class="card"><strong>{{ team.name }}</strong><p>{{ team.purpose }}</p><table><thead><tr><th>Role</th><th>AgentVersion</th><th>Status</th></tr></thead><tbody>{% for member in team.members %}<tr><td>{{ member.role }}</td><td>{{ member.agent_version__agent__name }} v{{ member.agent_version__version }}</td><td>{{ member.status }}</td></tr>{% endfor %}</tbody></table></article>{% empty %}<p>No teams.</p>{% endfor %}</section>{% endblock %}
|
||||||
2
templates/control_plane/approvals.html
Normal file
2
templates/control_plane/approvals.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>Approvals</h2><p class="muted">Durable approval inbox for paused graph decisions.</p></div></header><table><thead><tr><th>Source</th><th>Project</th><th>Reason</th><th>Evidence</th><th>Requested</th><th>Actions</th></tr></thead><tbody>{% for approval in approvals %}<tr><td><a href="{% url 'graph_run_detail' approval.graph_run.id %}">{{ approval.graph_run.execution_graph_version.graph.name }} v{{ approval.graph_run.execution_graph_version.version }}</a></td><td>{{ approval.graph_run.project.name|default:"-" }}</td><td>{{ approval.reason }}</td><td><pre>{{ approval.payload }}</pre></td><td>{{ approval.created_at }}</td><td><form class="actions" method="post" action="{% url 'approval_action' approval.id %}">{% csrf_token %}<button name="action" value="approve">Approve</button><button class="secondary" name="action" value="reject">Reject</button></form></td></tr>{% empty %}<tr><td colspan="6">No pending approvals.</td></tr>{% endfor %}</tbody></table>{% endblock %}
|
||||||
2
templates/control_plane/archaeologist.html
Normal file
2
templates/control_plane/archaeologist.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>Archaeologist / {{ project.name }}</h2><p class="muted">Observed system, inferred intent, and gap evidence.</p></div><a class="button secondary" href="{% url 'project_brain' project.id %}">Discuss with Project Brain</a></header><section class="grid"><div class="panel"><h3>Observed System</h3><p>{{ observed|default:"No architecture summary yet." }}</p></div><div class="panel"><h3>Evidence Artifacts</h3>{% for artifact in artifacts %}<p>{{ artifact.name }} <span class="badge">{{ artifact.artifact_type }}</span></p>{% empty %}<p>No archaeologist artifacts.</p>{% endfor %}</div><div class="panel"><h3>Gap Analysis</h3>{% for finding in findings %}<p><span class="badge {{ finding.severity }}">{{ finding.severity }}</span> {{ finding.title }}</p>{% empty %}<p>No findings.</p>{% endfor %}</div></section>{% endblock %}
|
||||||
64
templates/control_plane/base.html
Normal file
64
templates/control_plane/base.html
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{% block title %}Artifex{% endblock %}</title>
|
||||||
|
<style>
|
||||||
|
:root { --bg: #f6f2ea; --panel: #fffdfa; --line: #d8cfc2; --ink: #162033; --muted: #685f55; --accent: #284c7e; --danger: #9f2d2d; --warn: #9b681c; --ok: #23704a; }
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--ink); background: var(--bg); }
|
||||||
|
a { color: var(--accent); text-decoration: none; }
|
||||||
|
a:hover { text-decoration: underline; }
|
||||||
|
.shell { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh; }
|
||||||
|
nav { border-right: 1px solid var(--line); background: #ede6da; padding: 1.25rem; position: sticky; top: 0; height: 100vh; }
|
||||||
|
nav h1 { font-size: 1.15rem; letter-spacing: .12em; margin: 0 0 1.5rem; }
|
||||||
|
nav a { display: block; padding: .55rem .65rem; border-radius: 8px; color: var(--ink); }
|
||||||
|
nav a:hover { background: rgba(40, 76, 126, .09); text-decoration: none; }
|
||||||
|
main { padding: 1.5rem; max-width: 1480px; width: 100%; }
|
||||||
|
header.page { display: flex; justify-content: space-between; gap: 1rem; align-items: flex-start; margin-bottom: 1rem; }
|
||||||
|
h2, h3 { margin-bottom: .4rem; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }
|
||||||
|
.card, .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 1rem; margin-bottom: 1rem; box-shadow: 0 1px 0 rgba(0,0,0,.03); }
|
||||||
|
.metric { font-size: 2rem; font-weight: 760; line-height: 1; }
|
||||||
|
.muted { color: var(--muted); }
|
||||||
|
.badge { display: inline-block; padding: .18rem .45rem; border-radius: 999px; border: 1px solid var(--line); background: #f4efe6; font-size: .75rem; font-weight: 700; letter-spacing: .04em; }
|
||||||
|
.badge.COMPLETE, .badge.PASS, .badge.HEALTHY, .badge.APPROVED { color: var(--ok); border-color: rgba(35,112,74,.35); }
|
||||||
|
.badge.FAILED, .badge.FAIL, .badge.DEGRADED, .badge.REJECTED { color: var(--danger); border-color: rgba(159,45,45,.35); }
|
||||||
|
.badge.RUNNING, .badge.PAUSED, .badge.WATCH, .badge.PENDING { color: var(--warn); border-color: rgba(155,104,28,.35); }
|
||||||
|
table { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
|
||||||
|
th, td { text-align: left; padding: .65rem .75rem; border-bottom: 1px solid var(--line); vertical-align: top; }
|
||||||
|
th { font-size: .78rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); background: #f1eadf; }
|
||||||
|
tr:last-child td { border-bottom: 0; }
|
||||||
|
button, .button { border: 1px solid var(--accent); color: white; background: var(--accent); border-radius: 8px; padding: .45rem .7rem; cursor: pointer; font-weight: 650; }
|
||||||
|
button.secondary, .button.secondary { color: var(--ink); background: transparent; border-color: var(--line); }
|
||||||
|
.actions { display: flex; gap: .4rem; flex-wrap: wrap; }
|
||||||
|
pre { white-space: pre-wrap; word-break: break-word; background: #201d19; color: #f8f2e8; padding: .8rem; border-radius: 10px; max-height: 360px; overflow: auto; }
|
||||||
|
.timeline { border-left: 3px solid var(--line); margin-left: .5rem; padding-left: 1rem; }
|
||||||
|
.node { margin: .5rem 0; }
|
||||||
|
@media (max-width: 860px) { .shell { grid-template-columns: 1fr; } nav { position: static; height: auto; } nav a { display: inline-block; } main { padding: 1rem; } }
|
||||||
|
</style>
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<nav aria-label="Primary navigation">
|
||||||
|
<h1>ARTIFEX</h1>
|
||||||
|
<a href="/">Dashboard</a>
|
||||||
|
<a href="/projects/">Projects</a>
|
||||||
|
<a href="/agents/">Agents</a>
|
||||||
|
<a href="/progeny/">Progeny</a>
|
||||||
|
<a href="/steward/">Steward</a>
|
||||||
|
<a href="/roadmap/">Roadmap</a>
|
||||||
|
<a href="/scenario-lab/">Scenario Lab</a>
|
||||||
|
<a href="/approvals/">Approvals</a>
|
||||||
|
<a href="/resources/">Resources</a>
|
||||||
|
<a href="/activity/">Activity</a>
|
||||||
|
<a href="/admin/">Settings</a>
|
||||||
|
</nav>
|
||||||
|
<main id="main">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
templates/control_plane/dashboard.html
Normal file
16
templates/control_plane/dashboard.html
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block title %}Dashboard / Artifex{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<header class="page"><div><h2>Dashboard</h2><p class="muted">Operational control plane for projects, graphs, agents, approvals, and autonomous lifecycle work.</p></div><a class="button secondary" href="/approvals/">Approval Inbox: {{ approval_count }}</a></header>
|
||||||
|
<section class="grid">
|
||||||
|
<div class="card"><div class="metric">{{ project_summary.total }}</div><strong>Projects</strong><p>{{ project_summary.active }} active, {{ project_summary.blocked_failed }} blocked/failed</p></div>
|
||||||
|
<div class="card"><div class="metric">{{ execution_summary.active }}</div><strong>Active GraphRuns</strong><p>{{ execution_summary.failed }} failed. Champion task graph: {{ execution_summary.champion_task_graph|default:"not bootstrapped" }}</p></div>
|
||||||
|
<div class="card"><div class="metric">{{ steward_summary.open_findings }}</div><strong>Steward Findings</strong><p>{{ steward_summary.high_findings }} high/critical across {{ steward_summary.enrolled_projects }} enrolled projects</p></div>
|
||||||
|
<div class="card"><div class="metric">{{ progeny_summary.unresolved_signals }}</div><strong>Progeny Signals</strong><p>{{ progeny_summary.open_investigations }} investigations, {{ progeny_summary.challengers }} challengers</p></div>
|
||||||
|
<div class="card"><div class="metric">{{ agent_summary.agent_count }}</div><strong>Agents</strong><p>{{ agent_summary.watch_degraded }} watch/degraded, {{ agent_summary.active_runs }} active runs</p></div>
|
||||||
|
</section>
|
||||||
|
<section class="grid">
|
||||||
|
<div class="panel"><h3>Recent GraphRuns</h3>{% include "control_plane/partials/graph_run_table.html" with graph_runs=recent.graph_runs %}</div>
|
||||||
|
<div class="panel"><h3>Recent Activity</h3>{% include "control_plane/partials/event_list.html" with events=recent.events %}</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
2
templates/control_plane/explore.html
Normal file
2
templates/control_plane/explore.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>Explore{% if project %} / {{ project.name }}{% endif %}</h2><p class="muted">Ranked opportunities. Explorer results never directly invoke Coder.</p></div>{% if project %}<form method="post" action="{% url 'run_explore' project.id %}">{% csrf_token %}<button type="submit">Run Explore</button></form>{% endif %}</header><table><thead><tr><th>Opportunity</th><th>Project</th><th>Type</th><th>Scores</th><th>Status</th><th>Actions</th></tr></thead><tbody>{% for item in opportunities %}<tr><td>{{ item.title }}<br><span class="muted">{{ item.description }}</span></td><td>{{ item.project.name }}</td><td>{{ item.opportunity_type }}</td><td>V {{ item.value_score }} / E {{ item.effort_score }} / R {{ item.risk_score }} / C {{ item.confidence }} / T {{ item.technical_fit }} / S {{ item.strategic_fit }}</td><td><span class="badge {{ item.status }}">{{ item.status }}</span></td><td><form class="actions" method="post" action="{% url 'opportunity_action' item.id %}">{% csrf_token %}<button name="action" value="extend">Extend</button><button name="action" value="evolve">Evolve</button><button name="action" value="roadmap">Add to Roadmap</button><button class="secondary" name="action" value="defer">Defer</button><button class="secondary" name="action" value="reject">Reject</button></form></td></tr>{% empty %}<tr><td colspan="6">No opportunities.</td></tr>{% endfor %}</tbody></table>{% endblock %}
|
||||||
3
templates/control_plane/graph_run.html
Normal file
3
templates/control_plane/graph_run.html
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block head %}<meta http-equiv="refresh" content="5">{% endblock %}
|
||||||
|
{% block content %}<header class="page"><div><h2>GraphRun {{ graph_run.id }}</h2><p class="muted">Live execution graph viewer. This page auto-refreshes every 5 seconds.</p></div><a class="button secondary" href="{% url 'graph_run_json' graph_run.id %}">JSON</a></header>{% include "control_plane/partials/graph_run_status.html" %}<section class="grid"><div class="panel"><h3>Edges</h3>{% for edge in traversals %}<p>{{ edge.source_node }} → {{ edge.target_node }} <span class="badge">{{ edge.result|default:edge.condition }}</span></p>{% empty %}<p>No traversed edges.</p>{% endfor %}</div><div class="panel"><h3>Approvals</h3>{% for approval in approvals %}<p><span class="badge {{ approval.status }}">{{ approval.status }}</span> {{ approval.reason }}</p>{% empty %}<p>No approvals.</p>{% endfor %}</div></section>{% endblock %}
|
||||||
1
templates/control_plane/partials/event_list.html
Normal file
1
templates/control_plane/partials/event_list.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{% for event in events %}<p><span class="badge">{{ event.event_type }}</span> {{ event.created_at }} {% if event.project %}<a href="{% url 'project_workspace' event.project.id %}">{{ event.project.name }}</a>{% endif %}<br><span class="muted">{{ event.actor }}</span></p>{% empty %}<p>No activity.</p>{% endfor %}
|
||||||
1
templates/control_plane/partials/graph_run_status.html
Normal file
1
templates/control_plane/partials/graph_run_status.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<section class="panel"><h3>{{ graph_run.execution_graph_version.graph.name }} v{{ graph_run.execution_graph_version.version }} <span class="badge {{ graph_run.status }}">{{ graph_run.status }}</span></h3><p>Runtime: LangGraphRuntime / Current node: {{ graph_run.current_node }}</p><div class="timeline">{% for node in nodes %}<div class="node"><span class="badge {{ node.status }}">{{ node.status }}</span> <strong>{{ node.node_id }}</strong> visit {{ node.visit_index }}{% if node.agent_version %}<br><span class="muted">{{ node.agent_version.agent.name }} v{{ node.agent_version.version }} / {{ node.agent_version.model }}</span>{% endif %}{% if node.failure_evidence %}<pre>{{ node.failure_evidence }}</pre>{% endif %}{% if node.output_metadata %}<pre>{{ node.output_metadata }}</pre>{% endif %}</div>{% empty %}<p>No node runs.</p>{% endfor %}</div></section>
|
||||||
1
templates/control_plane/partials/graph_run_table.html
Normal file
1
templates/control_plane/partials/graph_run_table.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<table><thead><tr><th>Graph</th><th>Project</th><th>Status</th><th>Current Node</th><th>Updated</th></tr></thead><tbody>{% for run in graph_runs %}<tr><td><a href="{% url 'graph_run_detail' run.id %}">{{ run.execution_graph_version.graph.name }} v{{ run.execution_graph_version.version }}</a></td><td>{{ run.project.name|default:"-" }}</td><td><span class="badge {{ run.status }}">{{ run.status }}</span></td><td>{{ run.current_node }}</td><td>{{ run.updated_at }}</td></tr>{% empty %}<tr><td colspan="5">No graph runs.</td></tr>{% endfor %}</tbody></table>
|
||||||
1
templates/control_plane/partials/project_dag.html
Normal file
1
templates/control_plane/partials/project_dag.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{% for milestone in milestones %}<article class="card"><h4>{{ milestone.key }} / {{ milestone.title }} <span class="badge {{ milestone.status }}">{{ milestone.status }}</span></h4><p>{{ milestone.goal }}</p>{% for feature in milestone.features.all %}<div class="card"><strong>{{ feature.title }}</strong> <span class="badge {{ feature.status }}">{{ feature.status }}</span><ul>{% for task in feature.tasks.all %}<li><a href="{% url 'task_detail' task.id %}">{{ task.goal|truncatechars:120 }}</a> <span class="badge {{ task.status }}">{{ task.status }}</span></li>{% endfor %}</ul></div>{% empty %}<ul>{% for task in milestone.tasks.all %}<li><a href="{% url 'task_detail' task.id %}">{{ task.goal|truncatechars:120 }}</a> <span class="badge {{ task.status }}">{{ task.status }}</span></li>{% empty %}<li>No tasks.</li>{% endfor %}</ul>{% endfor %}</article>{% empty %}<p>No DAG materialized.</p>{% endfor %}
|
||||||
2
templates/control_plane/progeny.html
Normal file
2
templates/control_plane/progeny.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>Progeny</h2><p class="muted">Signals, investigations, experiments, and improvement candidates.</p></div></header><section class="grid"><div class="panel"><h3>Grouped Inbox</h3>{% for key,count in groups %}<p><span class="badge">{{ count }}</span> {{ key }}</p>{% empty %}<p>No open groups.</p>{% endfor %}</div><div class="panel"><h3>Investigations</h3>{% for inv in investigations %}<p><span class="badge {{ inv.status }}">{{ inv.status }}</span> {{ inv.recommended_target }} / {{ inv.recommended_route }} / confidence {{ inv.confidence }}</p>{% empty %}<p>No investigations.</p>{% endfor %}</div></section><section class="panel"><h3>Signals</h3><table><thead><tr><th>Signal</th><th>Source</th><th>Project</th><th>Agent</th><th>Severity</th><th>Status</th></tr></thead><tbody>{% for signal in signals %}<tr><td>{{ signal.summary }}<br><span class="muted">{{ signal.failure_category }} / {{ signal.grouping_key }}</span></td><td>{{ signal.source }}</td><td>{{ signal.project.name|default:"-" }}</td><td>{{ signal.agent_version.agent.name|default:"-" }}</td><td><span class="badge {{ signal.severity }}">{{ signal.severity }}</span></td><td><span class="badge {{ signal.status }}">{{ signal.status }}</span></td></tr>{% empty %}<tr><td colspan="6">No signals.</td></tr>{% endfor %}</tbody></table></section><section class="panel"><h3>Replay Arena Experiments</h3>{% for experiment in experiments %}<p>{{ experiment.hypothesis }} <span class="badge {{ experiment.status }}">{{ experiment.status }}</span></p>{% empty %}<p>No experiments.</p>{% endfor %}</section>{% endblock %}
|
||||||
2
templates/control_plane/project_brain.html
Normal file
2
templates/control_plane/project_brain.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>Project Brain / {{ project.name }}</h2><p class="muted">Project-scoped strategic decisions, plans, and artifacts.</p></div></header><section class="panel"><form method="post">{% csrf_token %}<label for="message">Conversation note / proposed decision</label><textarea id="message" name="message" rows="4" style="width:100%"></textarea><p><button type="submit">Persist Project Brain Note</button></p></form></section><section class="grid"><div class="panel"><h3>Decisions</h3>{% for decision in decisions %}<p><span class="badge">{{ decision.decision_type }}</span> {{ decision.decision }}</p>{% empty %}<p>No decisions.</p>{% endfor %}</div><div class="panel"><h3>Plans</h3>{% for plan in plans %}<p>v{{ plan.version }} {{ plan.scope|default:plan.goal }}</p>{% empty %}<p>No plans.</p>{% endfor %}</div></section>{% endblock %}
|
||||||
9
templates/control_plane/project_workspace.html
Normal file
9
templates/control_plane/project_workspace.html
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block title %}{{ project.name }} / Artifex{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<header class="page"><div><h2>{{ project.name }}</h2><p>{{ project.goal }}</p><p class="muted">{{ project.repository_path|default:project.repository_url }}</p></div><div class="actions"><a class="button secondary" href="{% url 'project_brain' project.id %}">Project Brain</a><a class="button secondary" href="{% url 'project_archaeologist' project.id %}">Archaeologist</a><form method="post" action="{% url 'run_explore' project.id %}">{% csrf_token %}<button type="submit">Explore</button></form></div></header>
|
||||||
|
<section class="grid"><div class="card"><strong>Status</strong><br><span class="badge {{ project.status }}">{{ project.status }}</span><p>{{ warnings|join:", "|default:"No active warnings" }}</p></div><div class="card"><strong>Current Plan</strong><p>v{{ plan.version|default:"none" }} {{ plan.scope|default:"" }}</p></div><div class="card"><strong>Tasks</strong><p>{{ tasks|length }} total</p></div><div class="card"><strong>Scenario Coverage</strong><p>{% for key,value in scenario_coverage.items %}{{ key }}: {{ value }} {% empty %}None{% endfor %}</p></div></section>
|
||||||
|
<section class="panel"><h3>Project DAG</h3>{% include "control_plane/partials/project_dag.html" %}</section>
|
||||||
|
<section class="grid"><div class="panel"><h3>Graph Runs</h3>{% include "control_plane/partials/graph_run_table.html" with graph_runs=graph_runs %}</div><div class="panel"><h3>Roadmap NOW</h3>{% for item in roadmap.NOW %}<p><a href="{% url 'project_roadmap' project.id %}">{{ item.title }}</a> <span class="badge">{{ item.target_action }}</span></p>{% empty %}<p>No NOW items.</p>{% endfor %}</div></section>
|
||||||
|
<section class="grid"><div class="panel"><h3>Latest Commits</h3>{% for commit in commits %}<p><code>{{ commit.sha|slice:":10" }}</code> {{ commit.message|truncatechars:90 }}</p>{% empty %}<p>No commits.</p>{% endfor %}</div><div class="panel"><h3>Activity</h3>{% include "control_plane/partials/event_list.html" with events=activity %}</div></section>
|
||||||
|
{% endblock %}
|
||||||
8
templates/control_plane/projects.html
Normal file
8
templates/control_plane/projects.html
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block title %}Projects / Artifex{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<header class="page"><div><h2>Projects</h2><p class="muted">Create and inspect autonomous engineering workspaces.</p></div></header>
|
||||||
|
<table><thead><tr><th>Project</th><th>Status</th><th>Repository</th><th>Tasks</th><th>Steward</th><th>Latest GraphRun</th><th>Warnings</th><th>Actions</th></tr></thead><tbody>
|
||||||
|
{% for row in rows %}<tr><td><a href="{% url 'project_workspace' row.project.id %}">{{ row.project.name }}</a><br><span class="muted">{{ row.project.goal|truncatechars:100 }}</span></td><td><span class="badge {{ row.project.status }}">{{ row.project.status }}</span></td><td>{{ row.project.repository_path|default:row.project.repository_url }}</td><td>{{ row.task_complete }}/{{ row.task_total }}</td><td>{{ row.steward_state.status|default:"not enrolled" }}<br>{{ row.open_findings }} findings</td><td>{% if row.latest_graph_run %}<a href="{% url 'graph_run_detail' row.latest_graph_run.id %}">{{ row.latest_graph_run.execution_graph_version.graph.name }} / {{ row.latest_graph_run.status }}</a>{% else %}None{% endif %}</td><td>{{ row.warnings|join:", "|default:"none" }}</td><td class="actions"><a class="button secondary" href="{% url 'project_workspace' row.project.id %}">Open</a><form method="post" action="{% url 'run_explore' row.project.id %}">{% csrf_token %}<button type="submit">Explore</button></form></td></tr>{% empty %}<tr><td colspan="8">No projects yet.</td></tr>{% endfor %}
|
||||||
|
</tbody></table>
|
||||||
|
{% endblock %}
|
||||||
2
templates/control_plane/resources.html
Normal file
2
templates/control_plane/resources.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>Resources</h2><p class="muted">Compute/provider visibility without credentials.</p></div></header><table><thead><tr><th>Resource</th><th>Kind</th><th>Provider</th><th>Health</th><th>Roles</th><th>Recent Requests</th><th>Median Latency</th></tr></thead><tbody>{% for row in resources %}<tr><td>{{ row.resource.name }}</td><td>{{ row.resource.kind }}</td><td>{{ row.resource.provider }} {{ row.resource.config.model|default:"" }}</td><td><span class="badge {{ row.resource.health_status }}">{{ row.resource.health_status }}</span></td><td>{{ row.resource.roles }}</td><td>{{ row.request_count }}</td><td>{{ row.median_latency|default:"n/a" }}</td></tr>{% empty %}<tr><td colspan="7">No resources.</td></tr>{% endfor %}</tbody></table>{% endblock %}
|
||||||
2
templates/control_plane/roadmap.html
Normal file
2
templates/control_plane/roadmap.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>Roadmap{% if project %} / {{ project.name }}{% endif %}</h2><p class="muted">Persistent future intent. Conversion creates lifecycle candidates, not immediate coding work.</p></div></header><section class="grid">{% for horizon,items in board.items %}<div class="panel"><h3>{{ horizon }}</h3>{% for item in items %}<article class="card"><strong>{{ item.title }}</strong><p>{{ item.description }}</p><p><span class="badge">{{ item.source }}</span> <span class="badge">{{ item.target_action }}</span> <span class="badge {{ item.status }}">{{ item.status }}</span></p><p>Composite {{ item.composite_score }} / Value {{ item.value_score }} / Effort {{ item.effort_score }} / Risk {{ item.risk_score }}</p><form class="actions" method="post" action="{% url 'roadmap_action' item.id %}">{% csrf_token %}<button name="action" value="NOW">NOW</button><button name="action" value="NEXT">NEXT</button><button name="action" value="LATER">LATER</button><button name="action" value="EXPLORING">EXPLORING</button><button name="action" value="extend">Extend</button><button name="action" value="evolve">Evolve</button><button class="secondary" name="action" value="defer">Defer</button><button class="secondary" name="action" value="reject">Reject</button></form></article>{% empty %}<p>No items.</p>{% endfor %}</div>{% endfor %}</section>{% endblock %}
|
||||||
2
templates/control_plane/scenarios.html
Normal file
2
templates/control_plane/scenarios.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>Scenario Lab{% if project %} / {{ project.name }}{% endif %}</h2><p class="muted">Unusual, degraded, adversarial, and high-load scenario portfolio.</p></div></header><section class="grid"><div class="panel"><h3>Coverage</h3>{% for key,value in coverage.items %}<p>{{ key }}: {{ value }}</p>{% empty %}<p>No coverage yet.</p>{% endfor %}</div><div class="panel"><h3>Suites</h3>{% for suite in suites %}<p>{{ suite.name }} v{{ suite.version }} <span class="badge {{ suite.status }}">{{ suite.status }}</span> / {{ suite.project.name }}</p>{% empty %}<p>No suites.</p>{% endfor %}</div></section><section class="panel"><h3>Recent Runs</h3><table><thead><tr><th>Scenario</th><th>Project</th><th>Result</th><th>Evidence</th></tr></thead><tbody>{% for run in runs %}<tr><td>{{ run.scenario.title }}</td><td>{{ run.project.name }}</td><td><span class="badge {{ run.result }}">{{ run.result }}</span></td><td><pre>{{ run.failure_evidence }}</pre></td></tr>{% empty %}<tr><td colspan="4">No runs.</td></tr>{% endfor %}</tbody></table></section><section class="panel"><h3>Findings</h3><table><thead><tr><th>Finding</th><th>Route</th><th>Status</th><th>Action</th></tr></thead><tbody>{% for finding in findings %}<tr><td>{{ finding.title }}<br>{{ finding.summary }}</td><td>{{ finding.recommended_action }} / {{ finding.recommended_route }}</td><td><span class="badge {{ finding.status }}">{{ finding.status }}</span></td><td><form method="post" action="{% url 'scenario_finding_action' finding.id %}">{% csrf_token %}<button name="action" value="roadmap">Route / Add Roadmap</button></form></td></tr>{% empty %}<tr><td colspan="4">No findings.</td></tr>{% endfor %}</tbody></table></section>{% endblock %}
|
||||||
2
templates/control_plane/steward.html
Normal file
2
templates/control_plane/steward.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>Steward{% if project %} / {{ project.name }}{% endif %}</h2><p class="muted">Enrollment runs, classified findings, evidence, and routed lifecycle actions.</p></div></header><table><thead><tr><th>Finding</th><th>Project</th><th>Severity</th><th>Route</th><th>Occurrences</th><th>Seen</th><th>Evidence</th></tr></thead><tbody>{% for finding in findings %}<tr><td>{{ finding.title }}<br><span class="muted">{{ finding.summary }}</span></td><td>{{ finding.project.name }}</td><td><span class="badge {{ finding.severity }}">{{ finding.severity }}</span></td><td>{{ finding.recommended_action }} / {{ finding.recommended_route }}</td><td>{{ finding.occurrence_count }}</td><td>{{ finding.first_seen|default:finding.created_at }} → {{ finding.last_seen|default:finding.updated_at }}</td><td><pre>{{ finding.evidence }}</pre></td></tr>{% empty %}<tr><td colspan="7">No Steward findings.</td></tr>{% endfor %}</tbody></table>{% endblock %}
|
||||||
2
templates/control_plane/task.html
Normal file
2
templates/control_plane/task.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% extends "control_plane/base.html" %}
|
||||||
|
{% block content %}<header class="page"><div><h2>Task</h2><p>{{ task.goal }}</p></div><span class="badge {{ task.status }}">{{ task.status }}</span></header><section class="grid"><div class="panel"><h3>Acceptance</h3><pre>{{ task.acceptance_criteria }}</pre><h3>Dependencies</h3>{% for dep in dependencies %}<p>{{ dep.goal }}</p>{% empty %}<p>No dependencies.</p>{% endfor %}</div><div class="panel"><h3>Attempts</h3>{% for attempt in attempts %}<p>#{{ attempt.attempt_number }} <span class="badge {{ attempt.status }}">{{ attempt.status }}</span> {{ attempt.coder.agent.name }} v{{ attempt.coder.version }}</p>{% empty %}<p>No attempts.</p>{% endfor %}</div></section><section class="grid"><div class="panel"><h3>GraphRuns</h3>{% include "control_plane/partials/graph_run_table.html" with graph_runs=graph_runs %}</div><div class="panel"><h3>Tests / Reviews / Commits</h3>{% for test in tests %}<p>Test: <span class="badge {{ test.status }}">{{ test.status }}</span> {{ test.command }}</p>{% endfor %}{% for review in reviews %}<p>Review: <span class="badge {{ review.status }}">{{ review.status }}</span></p>{% endfor %}{% for commit in commits %}<p>Commit <code>{{ commit.sha|slice:":10" }}</code> {{ commit.message }}</p>{% endfor %}</div></section>{% endblock %}
|
||||||
123
tests/test_control_plane_ui_v1.py
Normal file
123
tests/test_control_plane_ui_v1.py
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from django.test import Client
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from agents.control_room import AgentControlRoomService
|
||||||
|
from control_plane.agents.management.commands.seed_core_agents import Command as SeedAgentsCommand
|
||||||
|
from control_plane.agents.models import AgentRole, AgentVersion, PromotionStatus, ProgenySignal
|
||||||
|
from control_plane.events.bus import EventBus
|
||||||
|
from control_plane.events.models import EventType
|
||||||
|
from control_plane.projects.models import Exploration, ExplorationOpportunity, Project, ProjectPlan, Milestone, RoadmapItem, Scenario, ScenarioFinding, ScenarioRun, ScenarioSuite, StewardFinding, Task, TaskAttempt, TaskStatus
|
||||||
|
from control_plane.resources.models import Resource, ResourceKind
|
||||||
|
from control_plane.verification.models import Review, TestRun, Verification, VerificationLevel, VerificationResult
|
||||||
|
from graph.bootstrap import champion_project_exploration_graph_v1, champion_task_execution_graph_v1
|
||||||
|
from graph.models import GraphApproval, GraphRun, GraphRunStatus
|
||||||
|
|
||||||
|
|
||||||
|
def fixture() -> dict[str, object]:
|
||||||
|
SeedAgentsCommand().handle()
|
||||||
|
AgentControlRoomService().bootstrap_frontend_agents()
|
||||||
|
team = AgentControlRoomService().create_software_feature_team()
|
||||||
|
Resource.objects.create(name="Qwen", kind=ResourceKind.MODEL, provider="local_inference", roles=["CODING"], health_status="AVAILABLE", config={"model": "qwen38"})
|
||||||
|
project = Project.objects.create(name="UI Dogfood", goal="Operate Artifex through the UI", repository_path="/tmp/ui")
|
||||||
|
plan = ProjectPlan.objects.create(project=project, version=1, goal=project.goal, scope="UI V1")
|
||||||
|
milestone = Milestone.objects.create(project=project, plan=plan, key="UI", title="UI", goal="Build UI")
|
||||||
|
task = Task.objects.create(project=project, milestone=milestone, task_type="implementation", status=TaskStatus.COMPLETE, goal="Render dashboard", acceptance_criteria=["loads"])
|
||||||
|
coder = AgentVersion.objects.get(agent__role=AgentRole.CODER, promotion_status=PromotionStatus.CHAMPION)
|
||||||
|
reviewer = AgentVersion.objects.get(agent__role=AgentRole.REVIEWER, promotion_status=PromotionStatus.CHAMPION)
|
||||||
|
judge = AgentVersion.objects.get(agent__role=AgentRole.PROJECT_JUDGE, promotion_status=PromotionStatus.CHAMPION)
|
||||||
|
TaskAttempt.objects.create(task=task, attempt_number=1, coder=coder, status="COMPLETE")
|
||||||
|
TestRun.objects.create(project=project, task=task, command="pytest", status="PASS")
|
||||||
|
Review.objects.create(task=task, reviewer=reviewer, status="PASS")
|
||||||
|
Verification.objects.create(project=project, task=task, judge=judge, level=VerificationLevel.TASK, result=VerificationResult.PASS)
|
||||||
|
graph_version = champion_task_execution_graph_v1()
|
||||||
|
graph_run = GraphRun.objects.create(project=project, task=task, execution_graph_version=graph_version, current_node="review", status=GraphRunStatus.PAUSED, metadata={"telemetry": {}})
|
||||||
|
node_run = graph_run.node_runs.create(node_id="review", node_type="review", status="PAUSED", visit_index=1, agent_version=reviewer, output_metadata={"review_status": "RUNNING"})
|
||||||
|
approval = GraphApproval.objects.create(graph_run=graph_run, node_run=node_run, reason="AWAITING_UI_APPROVAL")
|
||||||
|
exploration = Exploration.objects.create(project=project, status="COMPLETE")
|
||||||
|
opportunity = ExplorationOpportunity.objects.create(exploration=exploration, project=project, title="Add status filter", description="Filter dashboard cards", opportunity_type="UX", recommended_action="EXTEND", value_score=0.8, effort_score=0.2, risk_score=0.2, confidence=0.8, technical_fit=0.8, strategic_fit=0.8, composite_score=0.75, grouping_key="ui-opp")
|
||||||
|
roadmap_item = RoadmapItem.objects.create(project=project, title="Improve graph viewer", description="Expose node details", horizon="NOW", target_action="EXTEND", grouping_key="ui-roadmap")
|
||||||
|
suite = ScenarioSuite.objects.create(project=project, name="UI Suite")
|
||||||
|
scenario = Scenario.objects.create(project=project, suite=suite, name="Permission denial", title="Permission denial", scenario_type="PERMISSION", target_type="PROJECT", target_id=str(project.id), injected_condition={"mechanism": "permission_denial"}, expected_invariants=["safe"], success_criteria=["pass"], resource_budget={"max_seconds": 5})
|
||||||
|
run = ScenarioRun.objects.create(project=project, scenario=scenario, result="FAIL", status="COMPLETE", failure_evidence={"summary": "denied"})
|
||||||
|
finding = ScenarioFinding.objects.create(project=project, scenario=scenario, scenario_run=run, title="Scenario failed", summary="permission denied", failure_category="PERMISSION", recommended_action="EXTEND", recommended_route="RoadmapItem", grouping_key="ui-scenario")
|
||||||
|
StewardFinding.objects.create(project=project, finding_type="RELIABILITY", title="Open issue", summary="Needs attention", severity="HIGH", confidence=0.8, recommended_action="REPAIR", grouping_key="ui-steward")
|
||||||
|
ProgenySignal.objects.create(project=project, agent_version=coder, source="ui", severity="MEDIUM", failure_category="MALFORMED", summary="Signal", grouping_key="ui-signal")
|
||||||
|
EventBus().publish(EventType.TASK_COMPLETED, project=project, task=task, actor="test")
|
||||||
|
return {"project": project, "task": task, "graph_run": graph_run, "approval": approval, "opportunity": opportunity, "roadmap_item": roadmap_item, "finding": finding, "coder": coder, "team": team}
|
||||||
|
|
||||||
|
|
||||||
|
def assert_ok(client: Client, url: str, text: str) -> None:
|
||||||
|
response = client.get(url)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert text.encode() in response.content
|
||||||
|
|
||||||
|
|
||||||
|
def test_control_plane_core_pages_render_and_link() -> None:
|
||||||
|
data = fixture()
|
||||||
|
client = Client()
|
||||||
|
project = data["project"]
|
||||||
|
task = data["task"]
|
||||||
|
graph_run = data["graph_run"]
|
||||||
|
coder = data["coder"]
|
||||||
|
|
||||||
|
assert_ok(client, reverse("dashboard"), "Dashboard")
|
||||||
|
assert_ok(client, reverse("projects"), "UI Dogfood")
|
||||||
|
assert_ok(client, reverse("project_workspace", args=[project.id]), "Project DAG")
|
||||||
|
assert_ok(client, reverse("project_brain", args=[project.id]), "Project Brain")
|
||||||
|
assert_ok(client, reverse("project_archaeologist", args=[project.id]), "Archaeologist")
|
||||||
|
assert_ok(client, reverse("graph_run_detail", args=[graph_run.id]), "Live execution graph viewer")
|
||||||
|
assert_ok(client, reverse("task_detail", args=[task.id]), "Acceptance")
|
||||||
|
assert_ok(client, reverse("steward"), "Open issue")
|
||||||
|
assert_ok(client, reverse("explore"), "Add status filter")
|
||||||
|
assert_ok(client, reverse("roadmap"), "Improve graph viewer")
|
||||||
|
assert_ok(client, reverse("scenarios"), "Permission denial")
|
||||||
|
assert_ok(client, reverse("progeny"), "Signal")
|
||||||
|
assert_ok(client, reverse("agent_control_room"), "Agent Control Room")
|
||||||
|
assert_ok(client, reverse("agent_detail", args=[coder.id]), "Competencies")
|
||||||
|
assert_ok(client, reverse("resources"), "Qwen")
|
||||||
|
assert_ok(client, reverse("approvals"), "AWAITING_UI_APPROVAL")
|
||||||
|
assert_ok(client, reverse("activity"), "TASK_COMPLETED")
|
||||||
|
|
||||||
|
|
||||||
|
def test_json_endpoints_and_ui_actions_work() -> None:
|
||||||
|
data = fixture()
|
||||||
|
client = Client()
|
||||||
|
project = data["project"]
|
||||||
|
graph_run = data["graph_run"]
|
||||||
|
approval = data["approval"]
|
||||||
|
opportunity = data["opportunity"]
|
||||||
|
roadmap_item = data["roadmap_item"]
|
||||||
|
finding = data["finding"]
|
||||||
|
|
||||||
|
assert client.get(reverse("graph_run_json", args=[graph_run.id])).json()["status"] == "PAUSED"
|
||||||
|
assert client.get(reverse("project_dag_json", args=[project.id])).json()["tasks"]
|
||||||
|
assert client.get(reverse("agent_performance_json", args=[data["coder"].id])).json()["quality"]
|
||||||
|
|
||||||
|
response = client.post(reverse("opportunity_action", args=[opportunity.id]), {"action": "roadmap"})
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert project.roadmap_items.filter(title="Add status filter").exists()
|
||||||
|
|
||||||
|
response = client.post(reverse("roadmap_action", args=[roadmap_item.id]), {"action": "NEXT"})
|
||||||
|
assert response.status_code == 302
|
||||||
|
roadmap_item.refresh_from_db()
|
||||||
|
assert roadmap_item.horizon == "NEXT"
|
||||||
|
|
||||||
|
response = client.post(reverse("scenario_finding_action", args=[finding.id]), {"action": "roadmap"})
|
||||||
|
assert response.status_code == 302
|
||||||
|
|
||||||
|
response = client.post(reverse("approval_action", args=[approval.id]), {"action": "approve"})
|
||||||
|
assert response.status_code == 302
|
||||||
|
approval.refresh_from_db()
|
||||||
|
graph_run.refresh_from_db()
|
||||||
|
assert approval.status == "APPROVED"
|
||||||
|
assert graph_run.status == "RUNNING"
|
||||||
|
|
||||||
|
|
||||||
|
def test_ui_can_start_safe_explore_graph() -> None:
|
||||||
|
data = fixture()
|
||||||
|
client = Client()
|
||||||
|
response = client.post(reverse("run_explore", args=[data["project"].id]))
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert GraphRun.objects.filter(execution_graph_version__graph__name="project_exploration", project=data["project"]).exists()
|
||||||
102
tests/test_venture_discovery_cohort_v02.py
Normal file
102
tests/test_venture_discovery_cohort_v02.py
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from agents.venture_discovery import EVIDENCE_CEILINGS, SCORE_DEFINITIONS, SCORE_DIMENSIONS, VentureDiscoveryService
|
||||||
|
from control_plane.ventures.models import CompanyProposal, EvidenceTier, OverlapClassification, PortfolioICReview, VentureCapabilityDemand, VentureCohort, VentureCollision, VentureThesisFingerprint
|
||||||
|
from graph.bootstrap import champion_venture_discovery_cohort_graph_v1
|
||||||
|
from graph.langgraph_runtime import LangGraphRuntime
|
||||||
|
from graph.models import GraphRun, GraphRunStatus
|
||||||
|
from graph.venture_cohort import venture_discovery_cohort_registry
|
||||||
|
from model_router.router import ModelProvider, ModelRequestContract, ModelResponseContract, ModelRouter
|
||||||
|
|
||||||
|
|
||||||
|
class SequenceProvider(ModelProvider):
|
||||||
|
provider_name = "sol-sequence"
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.company_calls = 0
|
||||||
|
self.research_calls = 0
|
||||||
|
|
||||||
|
def complete(self, request: ModelRequestContract) -> ModelResponseContract:
|
||||||
|
if "Bounded public web market research" in request.prompt:
|
||||||
|
self.research_calls += 1
|
||||||
|
categories = ["competitors", "pricing", "customer_pain", "market_alternatives", "regulatory_platform_risks"]
|
||||||
|
return ModelResponseContract("luna", json.dumps({"sources": [{"url": f"https://example.com/{self.research_calls}/{cat}", "title": cat, "category": cat, "summary": cat} for cat in categories], "findings": {cat: [cat] for cat in categories}, "coverage": {cat: True for cat in categories}}), {})
|
||||||
|
self.company_calls += 1
|
||||||
|
n = self.company_calls
|
||||||
|
industries = ["Shopify", "Developer", "Legal", "Healthcare", "Real Estate", "Restaurant", "Security", "Education", "Logistics", "Finance"]
|
||||||
|
industry = industries[(n - 1) % len(industries)]
|
||||||
|
return ModelResponseContract("sol", json.dumps({"title": f"{industry} Validation Offer {n}", "one_line_thesis": f"Sell a productized {industry.lower()} audit to a narrow buyer before building software.", "description": f"A fixed-scope {industry} audit.", "problem": f"{industry} buyers have urgent operational gaps.", "target_customer": f"Small {industry} operators with active revenue.", "proposed_solution": f"Manual {industry} audit with prioritized fixes.", "business_model": "Productized service", "pricing_hypothesis": "$99 audit", "acquisition_strategy": "Compliant community posts and direct referrals after approval", "validation_plan": "Collect 5 credible target-customer responses or 1 willingness-to-pay signal before build/spend.", "capital_requested": "50", "time_to_first_dollar_estimate": "3-7 days after outreach approval", "expected_margin": "80-90% gross margin", "build_complexity": "LOW", "market_evidence": [], "differentiation": "Fast narrow audit", "major_risks": ["Demand unproven"], "confidence": 0.65}), {})
|
||||||
|
|
||||||
|
def health(self) -> str:
|
||||||
|
return "AVAILABLE"
|
||||||
|
|
||||||
|
|
||||||
|
def service() -> VentureDiscoveryService:
|
||||||
|
provider = SequenceProvider()
|
||||||
|
return VentureDiscoveryService(ModelRouter({"sol": provider, "luna": provider}), web_research_available=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_evidence_tiers_probability_ceiling_and_score_orientation() -> None:
|
||||||
|
svc = service()
|
||||||
|
proposal = svc.generate_single_company(svc.create_v0_mandate())
|
||||||
|
assert proposal.evidence_tier == EvidenceTier.TIER_0_THESIS
|
||||||
|
assert svc.calibrate_probability(proposal, raw_probability=80)["evidence_adjusted_probability"] == EVIDENCE_CEILINGS[EvidenceTier.TIER_0_THESIS]
|
||||||
|
svc.conduct_market_research(proposal)
|
||||||
|
assert proposal.evidence_tier == EvidenceTier.TIER_1_PUBLIC_EVIDENCE
|
||||||
|
assert svc.calibrate_probability(proposal, raw_probability=80)["evidence_adjusted_probability"] == EVIDENCE_CEILINGS[EvidenceTier.TIER_1_PUBLIC_EVIDENCE]
|
||||||
|
diligence = svc.start_ic_diligence(proposal)
|
||||||
|
svc.generate_ic_questions(diligence)
|
||||||
|
svc.answer_questions(diligence)
|
||||||
|
svc.red_team(diligence)
|
||||||
|
svc.final_company_response(diligence)
|
||||||
|
decision = svc.score_and_decide(diligence)
|
||||||
|
assert set(decision.component_scores) == set(SCORE_DIMENSIONS)
|
||||||
|
assert all("100 = highly attractive" in definition for definition in decision.score_definitions.values())
|
||||||
|
assert decision.probability_500_within_30_days <= decision.evidence_ceiling
|
||||||
|
assert decision.raw_probability_500_within_30_days >= decision.probability_500_within_30_days
|
||||||
|
|
||||||
|
|
||||||
|
def test_identity_contamination_detection_and_fingerprint_collision() -> None:
|
||||||
|
svc = service()
|
||||||
|
proposal = svc.generate_single_company(svc.create_v0_mandate())
|
||||||
|
contaminated = {"Company": "Unrelated CRM SaaS", "ICP": "enterprise banks", "Problem": "loan defaults", "Product": "risk platform"}
|
||||||
|
result = svc.validate_identity_content(proposal, contaminated)
|
||||||
|
assert result["passed"] is False
|
||||||
|
fp = svc.fingerprint_proposal(proposal)
|
||||||
|
assert isinstance(fp, VentureThesisFingerprint)
|
||||||
|
other = svc.generate_single_company(proposal.mandate)
|
||||||
|
other.title = proposal.title + " Copy"
|
||||||
|
other.target_customer = proposal.target_customer
|
||||||
|
other.problem = proposal.problem
|
||||||
|
other.proposed_solution = proposal.proposed_solution
|
||||||
|
other.business_model = proposal.business_model
|
||||||
|
other.save(update_fields=["title", "target_customer", "problem", "proposed_solution", "business_model", "updated_at"])
|
||||||
|
overlap = svc.classify_overlap(proposal, other)
|
||||||
|
assert overlap["classification"] in {OverlapClassification.NEAR_DUPLICATE, OverlapClassification.DUPLICATE, OverlapClassification.COMPETITIVE}
|
||||||
|
|
||||||
|
|
||||||
|
def test_cohort_size_ranking_top3_capability_aggregation_and_graph_lineage() -> None:
|
||||||
|
svc = service()
|
||||||
|
version = champion_venture_discovery_cohort_graph_v1()
|
||||||
|
graph_run = GraphRun.objects.create(execution_graph_version=version, current_node=version.graph_spec["entry"])
|
||||||
|
LangGraphRuntime(venture_discovery_cohort_registry(svc, cohort_size=10, concurrency=2)).run_until_terminal_or_paused(graph_run)
|
||||||
|
graph_run.refresh_from_db()
|
||||||
|
cohort = VentureCohort.objects.get(id=graph_run.metadata["cohort_id"])
|
||||||
|
|
||||||
|
assert graph_run.status == GraphRunStatus.COMPLETE
|
||||||
|
assert cohort.members.count() == 10
|
||||||
|
assert CompanyProposal.objects.count() == 10
|
||||||
|
assert cohort.members.filter(is_top_3=True).count() == 3
|
||||||
|
assert PortfolioICReview.objects.filter(cohort=cohort).exists()
|
||||||
|
assert VentureCollision.objects.filter(cohort=cohort).count() == 45
|
||||||
|
assert VentureCapabilityDemand.objects.filter(cohort=cohort).exists()
|
||||||
|
assert cohort.metadata["real_spend"] == 0
|
||||||
|
assert cohort.metadata["real_customer_outreach"] is False
|
||||||
|
assert cohort.concurrency == 2
|
||||||
|
assert cohort.metrics["peak_concurrency"] == 2
|
||||||
|
report = cohort.mandate.artifacts.get(artifact_type="VENTURE_DISCOVERY_COHORT_REPORT")
|
||||||
|
assert len(report.content["rankings"]) == 10
|
||||||
|
assert len(report.content["top_3"]) == 3
|
||||||
|
assert report.content["total_spend"] == 0
|
||||||
217
tests/test_venture_discovery_v0.py
Normal file
217
tests/test_venture_discovery_v0.py
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from agents.venture_discovery import PITCH_SECTIONS, SCORE_DIMENSIONS, VentureDiscoveryService
|
||||||
|
from control_plane.ventures.models import CapabilityPriority, CapabilityStatus, CompanyProposal, CompanyProposalStatus, ICDecisionType, VentureArtifact
|
||||||
|
from graph.bootstrap import champion_venture_discovery_graph_v1
|
||||||
|
from graph.langgraph_runtime import LangGraphRuntime
|
||||||
|
from graph.models import GraphRun, GraphRunStatus
|
||||||
|
from graph.venture_discovery import venture_discovery_registry
|
||||||
|
from model_router.router import ModelProvider, ModelRequestContract, ModelResponseContract, ModelRouter
|
||||||
|
|
||||||
|
|
||||||
|
class SolOneCompanyProvider(ModelProvider):
|
||||||
|
provider_name = "sol-test"
|
||||||
|
|
||||||
|
def complete(self, request: ModelRequestContract) -> ModelResponseContract:
|
||||||
|
return ModelResponseContract(
|
||||||
|
model="sol",
|
||||||
|
content=json.dumps(
|
||||||
|
{
|
||||||
|
"title": "LaunchLens",
|
||||||
|
"one_line_thesis": "A paid validation audit helps solo technical founders avoid wasting weeks on unvalidated AI microbusinesses.",
|
||||||
|
"description": "A fixed-scope validation and launch-readiness report for one startup idea.",
|
||||||
|
"problem": "Builders overbuild before proving demand.",
|
||||||
|
"target_customer": "Solo technical founders and small service operators considering an AI-assisted microbusiness.",
|
||||||
|
"proposed_solution": "A productized audit covering ICP, first-dollar path, validation gates, build plan, risks, and capability gaps.",
|
||||||
|
"business_model": "Productized service first, optional software later.",
|
||||||
|
"pricing_hypothesis": "$49-$99 per audit.",
|
||||||
|
"acquisition_strategy": "Compliant founder community posts and personal network conversations after approval.",
|
||||||
|
"validation_plan": "Collect 5 credible target-customer responses or 1 willingness-to-pay signal before build/spend.",
|
||||||
|
"capital_requested": "50",
|
||||||
|
"time_to_first_dollar_estimate": "3-10 days after outreach approval",
|
||||||
|
"expected_margin": "70-85% gross margin",
|
||||||
|
"build_complexity": "LOW",
|
||||||
|
"market_evidence": [{"type": "reasoning", "source": "sol", "summary": "Service-led validation minimizes build risk."}],
|
||||||
|
"differentiation": "IC-style diligence plus Artifex execution/capability-gap awareness.",
|
||||||
|
"major_risks": ["Demand unproven", "Distribution may fail", "Generic consulting competition"],
|
||||||
|
"confidence": 0.66,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
metadata={"usage": {"prompt_tokens": 1, "completion_tokens": 1}},
|
||||||
|
)
|
||||||
|
|
||||||
|
def health(self) -> str:
|
||||||
|
return "AVAILABLE"
|
||||||
|
|
||||||
|
|
||||||
|
class ResearchProvider(ModelProvider):
|
||||||
|
provider_name = "luna-test"
|
||||||
|
|
||||||
|
def __init__(self, complete: bool = True) -> None:
|
||||||
|
self.complete_coverage = complete
|
||||||
|
|
||||||
|
def complete(self, request: ModelRequestContract) -> ModelResponseContract:
|
||||||
|
categories = ["competitors", "pricing", "customer_pain", "market_alternatives", "regulatory_platform_risks"] if self.complete_coverage else ["competitors"]
|
||||||
|
return ModelResponseContract(
|
||||||
|
model="luna",
|
||||||
|
content=json.dumps(
|
||||||
|
{
|
||||||
|
"sources": [{"url": f"https://example.com/{category}", "title": category.replace("_", " ").title(), "category": category, "summary": f"Source-linked evidence for {category}."} for category in categories],
|
||||||
|
"findings": {category: f"Finding for {category}." for category in categories},
|
||||||
|
"coverage": {category: category in categories for category in ["competitors", "pricing", "customer_pain", "market_alternatives", "regulatory_platform_risks"]},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
metadata={},
|
||||||
|
)
|
||||||
|
|
||||||
|
def health(self) -> str:
|
||||||
|
return "AVAILABLE"
|
||||||
|
|
||||||
|
|
||||||
|
def service(*, web_research_available: bool = False, complete_research: bool = True) -> VentureDiscoveryService:
|
||||||
|
return VentureDiscoveryService(ModelRouter({"sol": SolOneCompanyProvider(), "luna": ResearchProvider(complete_research)}), web_research_available=web_research_available)
|
||||||
|
|
||||||
|
|
||||||
|
def test_company_proposal_lifecycle_mandate_limits_and_pitch_schema() -> None:
|
||||||
|
svc = service()
|
||||||
|
mandate = svc.create_v0_mandate()
|
||||||
|
proposal = svc.generate_single_company(mandate)
|
||||||
|
|
||||||
|
assert mandate.max_validation_capital == Decimal("50")
|
||||||
|
assert mandate.target_net_new_cash == Decimal("500")
|
||||||
|
assert mandate.target_window_days == 30
|
||||||
|
assert mandate.constraints["no_real_spend_in_v0"] is True
|
||||||
|
assert mandate.constraints["no_real_customer_outreach_in_v0"] is True
|
||||||
|
assert CompanyProposal.objects.count() == 1
|
||||||
|
assert proposal.status == CompanyProposalStatus.SUBMITTED
|
||||||
|
assert proposal.capital_requested <= Decimal("50")
|
||||||
|
assert all(section in proposal.pitch for section in PITCH_SECTIONS)
|
||||||
|
assert proposal.metadata["generation_source"] == "sol"
|
||||||
|
assert proposal.metadata["real_spend"] == 0
|
||||||
|
assert proposal.metadata["real_customer_outreach"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_board_review_ic_questions_responses_and_bounded_diligence() -> None:
|
||||||
|
svc = service()
|
||||||
|
proposal = svc.generate_single_company(svc.create_v0_mandate())
|
||||||
|
board = svc.board_review(proposal)
|
||||||
|
diligence = svc.start_ic_diligence(proposal)
|
||||||
|
questions = svc.generate_ic_questions(diligence)
|
||||||
|
responses = svc.answer_questions(diligence)
|
||||||
|
challenge = svc.red_team(diligence)
|
||||||
|
final = svc.final_company_response(diligence)
|
||||||
|
|
||||||
|
assert set(board.observations) == {"CEO", "CTO", "CFO", "CRO", "Independent Director"}
|
||||||
|
assert board.metadata["company_does_not_grade_itself"] is True
|
||||||
|
assert diligence.rounds == ["Initial Pitch", "Diligence Round 1", "Final Challenge", "Decision"]
|
||||||
|
assert diligence.metadata["bounded_rounds"] is True
|
||||||
|
assert len(questions) == 8
|
||||||
|
assert all(q.evidence_required for q in questions)
|
||||||
|
assert len(responses) == len(questions)
|
||||||
|
assert all(r.metadata["no_customer_outreach"] and r.metadata["no_spend"] for r in responses)
|
||||||
|
assert challenge["recommendation"] == "continue_to_final_response"
|
||||||
|
assert final["kill_criteria"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ic_scoring_decision_capability_gap_and_memo_artifact() -> None:
|
||||||
|
svc = service(web_research_available=True)
|
||||||
|
proposal = svc.generate_single_company(svc.create_v0_mandate())
|
||||||
|
research = svc.conduct_market_research(proposal)
|
||||||
|
svc.board_review(proposal)
|
||||||
|
diligence = svc.start_ic_diligence(proposal)
|
||||||
|
svc.generate_ic_questions(diligence)
|
||||||
|
svc.answer_questions(diligence)
|
||||||
|
svc.red_team(diligence)
|
||||||
|
svc.final_company_response(diligence)
|
||||||
|
decision = svc.score_and_decide(diligence)
|
||||||
|
gap = svc.capability_analysis(proposal)
|
||||||
|
memo = svc.produce_investment_memo(diligence, gap)
|
||||||
|
|
||||||
|
assert decision.decision in ICDecisionType.values
|
||||||
|
assert set(SCORE_DIMENSIONS) == set(decision.component_scores)
|
||||||
|
assert decision.decision == ICDecisionType.CONDITIONAL_FUND
|
||||||
|
assert decision.initial_tranche in {Decimal("10.00"), Decimal("20.00")}
|
||||||
|
assert "5 credible target-customer responses" in decision.validation_condition
|
||||||
|
assert decision.metadata["no_actual_funding"] is True
|
||||||
|
assert len(research["sources"]) == 5
|
||||||
|
assert "WEB_MARKET_RESEARCH" in gap.available
|
||||||
|
assert gap.metadata["web_market_research_status"] == "AVAILABLE"
|
||||||
|
assert {item["priority"] for item in gap.ranked_missing}.issuperset({CapabilityPriority.BEFORE_VALIDATION, CapabilityPriority.BEFORE_FIRST_CUSTOMER})
|
||||||
|
assert memo.artifact_type == "FINAL_INVESTMENT_MEMO"
|
||||||
|
assert memo.content["Company"] == proposal.title
|
||||||
|
assert "Capability gap" in memo.content
|
||||||
|
|
||||||
|
|
||||||
|
def test_fallback_is_marked_and_reduces_confidence_when_research_unavailable() -> None:
|
||||||
|
svc = VentureDiscoveryService(web_research_available=False)
|
||||||
|
proposal = svc.generate_single_company(svc.create_v0_mandate())
|
||||||
|
gap = svc.capability_analysis(proposal)
|
||||||
|
|
||||||
|
assert proposal.metadata["generation_source"] == "deterministic_fallback"
|
||||||
|
assert proposal.metadata["fallback_evidence"] is True
|
||||||
|
assert proposal.confidence <= 0.58
|
||||||
|
assert any(item.get("fallback_evidence") for item in proposal.market_evidence)
|
||||||
|
assert "WEB_MARKET_RESEARCH" in gap.missing
|
||||||
|
|
||||||
|
|
||||||
|
def test_venture_discovery_v1_graph_lineage_and_no_automatic_execution() -> None:
|
||||||
|
version = champion_venture_discovery_graph_v1()
|
||||||
|
graph_run = GraphRun.objects.create(execution_graph_version=version, current_node=version.graph_spec["entry"])
|
||||||
|
|
||||||
|
LangGraphRuntime(venture_discovery_registry(service())).run_until_terminal_or_paused(graph_run)
|
||||||
|
graph_run.refresh_from_db()
|
||||||
|
|
||||||
|
assert graph_run.status == GraphRunStatus.COMPLETE
|
||||||
|
assert graph_run.execution_graph_version.graph.name == "venture_discovery"
|
||||||
|
assert graph_run.execution_graph_version.version == 1
|
||||||
|
assert CompanyProposal.objects.count() == 1
|
||||||
|
proposal = CompanyProposal.objects.get()
|
||||||
|
assert proposal.metadata["real_spend"] == 0
|
||||||
|
assert proposal.metadata["real_customer_outreach"] is False
|
||||||
|
assert graph_run.node_runs.count() == 14
|
||||||
|
assert graph_run.edge_traversals.count() == 14
|
||||||
|
artifact_types = set(VentureArtifact.objects.values_list("artifact_type", flat=True))
|
||||||
|
assert {"STANDARDIZED_COMPANY_PITCH", "MARKET_RESEARCH", "COMPANY_BOARD_REVIEW", "IC_QUESTIONS", "IC_RESPONSES", "IC_RED_TEAM", "IC_FINAL_RESPONSE", "IC_FINAL_SCORE", "CAPABILITY_GAP_REPORT", "FINAL_INVESTMENT_MEMO"}.issubset(artifact_types)
|
||||||
|
decision = proposal.ic_diligence.get().decision
|
||||||
|
svc = service()
|
||||||
|
svc.request_human_approval(decision, "approve_for_validation")
|
||||||
|
decision.refresh_from_db()
|
||||||
|
assert decision.metadata["real_spend_still_blocked"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_materially_different_pitches_receive_different_scores_and_tranches() -> None:
|
||||||
|
strong = service(web_research_available=True, complete_research=True)
|
||||||
|
strong_proposal = strong.generate_single_company(strong.create_v0_mandate())
|
||||||
|
strong.conduct_market_research(strong_proposal)
|
||||||
|
strong_diligence = strong.start_ic_diligence(strong_proposal)
|
||||||
|
strong.generate_ic_questions(strong_diligence)
|
||||||
|
strong.answer_questions(strong_diligence)
|
||||||
|
strong.red_team(strong_diligence)
|
||||||
|
strong.final_company_response(strong_diligence)
|
||||||
|
strong_decision = strong.score_and_decide(strong_diligence)
|
||||||
|
|
||||||
|
weak = service(web_research_available=True, complete_research=False)
|
||||||
|
weak_proposal = weak.generate_single_company(weak.create_v0_mandate())
|
||||||
|
weak_proposal.title = "Enterprise RegTech Platform"
|
||||||
|
weak_proposal.business_model = "SaaS platform requiring integrations and long enterprise sales cycles."
|
||||||
|
weak_proposal.build_complexity = "HIGH"
|
||||||
|
weak_proposal.expected_margin = "35-45% gross margin before support burden"
|
||||||
|
weak_proposal.validation_plan = "Build a prototype and then seek feedback."
|
||||||
|
weak_proposal.pitch = {**weak_proposal.pitch, "Company name": weak_proposal.title, "Business model": weak_proposal.business_model, "Validation plan": weak_proposal.validation_plan}
|
||||||
|
weak_proposal.save(update_fields=["title", "business_model", "build_complexity", "expected_margin", "validation_plan", "pitch", "updated_at"])
|
||||||
|
weak.conduct_market_research(weak_proposal)
|
||||||
|
weak_diligence = weak.start_ic_diligence(weak_proposal)
|
||||||
|
weak.generate_ic_questions(weak_diligence)
|
||||||
|
weak.answer_questions(weak_diligence)
|
||||||
|
weak.red_team(weak_diligence)
|
||||||
|
weak.final_company_response(weak_diligence)
|
||||||
|
weak_decision = weak.score_and_decide(weak_diligence)
|
||||||
|
|
||||||
|
assert strong_decision.component_scores["Demand Evidence"] > weak_decision.component_scores["Demand Evidence"]
|
||||||
|
assert strong_decision.component_scores["Build Simplicity"] > weak_decision.component_scores["Build Simplicity"]
|
||||||
|
assert strong_decision.composite_score - weak_decision.composite_score >= 10
|
||||||
|
assert strong_decision.probability_500_within_30_days > weak_decision.probability_500_within_30_days
|
||||||
|
assert strong_decision.initial_tranche != weak_decision.initial_tranche
|
||||||
Loading…
Add table
Reference in a new issue