Add venture discovery cohort V0.2
This commit is contained in:
parent
404e9d4949
commit
dc7e21d77c
9 changed files with 1210 additions and 38 deletions
|
|
@ -3,25 +3,33 @@ 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, ICDecision, ICDecisionType, ICDiligence, ICQuestion, ICResponse, PortfolioCapabilityGap, VentureArtifact, VentureThesis
|
||||
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", "Capital efficiency", "Validation cost", "Gross margin", "Distribution difficulty", "Build complexity", "Defensibility", "Market size", "Competition", "Risk", "Probability of reaching $500"]
|
||||
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) -> None:
|
||||
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(
|
||||
|
|
@ -33,15 +41,15 @@ class VentureDiscoveryService:
|
|||
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) -> CompanyProposal:
|
||||
payload, source = self._company_payload(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 = float(payload.get("confidence", 0.55))
|
||||
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})
|
||||
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,
|
||||
|
|
@ -65,11 +73,46 @@ class VentureDiscoveryService:
|
|||
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."],
|
||||
|
|
@ -81,13 +124,13 @@ class VentureDiscoveryService:
|
|||
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["Product / service"] = "Productized validation and launch-readiness audit for AI-assisted microbusiness ideas, delivered as a concise paid report before any software build."
|
||||
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"])
|
||||
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})
|
||||
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}, self._readable_board(review), "Company Board", graph_run=graph_run)
|
||||
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:
|
||||
|
|
@ -111,7 +154,8 @@ class VentureDiscoveryService:
|
|||
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"])
|
||||
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]}, self._readable_responses(responses), "Company reasoning roles", graph_run=graph_run)
|
||||
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]:
|
||||
|
|
@ -119,11 +163,13 @@ class VentureDiscoveryService:
|
|||
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": "Solo technical founders and small service operators deciding whether to spend time building an AI-assisted microbusiness.", "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": "Narrowed to solo technical founders and small service operators.", "Validation plan": "Gate spend behind credible response/willingness-to-pay evidence."}}
|
||||
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"])
|
||||
|
|
@ -131,19 +177,19 @@ class VentureDiscoveryService:
|
|||
return response
|
||||
|
||||
def score_and_decide(self, diligence: ICDiligence, *, graph_run=None) -> ICDecision:
|
||||
scores = {"Demand evidence": 38, "Time to first dollar": 78, "Capital efficiency": 84, "Validation cost": 82, "Gross margin": 76, "Distribution difficulty": 48, "Build complexity": 72, "Defensibility": 34, "Market size": 58, "Competition": 44, "Risk": 42, "Probability of reaching $500": 45}
|
||||
if not self.web_research_available:
|
||||
scores["Demand evidence"] = 30
|
||||
scores["Competition"] = 36
|
||||
scores["Probability of reaching $500"] = 40
|
||||
scores = self._evidence_scores(diligence.proposal)
|
||||
composite = round(sum(scores.values()) / len(scores), 1)
|
||||
decision_type = ICDecisionType.CONDITIONAL_FUND if composite >= 55 else ICDecisionType.REVISE_AND_RESUBMIT
|
||||
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=Decimal("10.00") if decision_type == ICDecisionType.CONDITIONAL_FUND else None, 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": 10, "remaining_reserved": 40, "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})
|
||||
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, "decision": decision.decision, "composite_score": composite, "conditional_funding": {"initial_tranche": str(decision.initial_tranche), "condition": decision.validation_condition}}, self._readable_score(decision), "Independent IC", graph_run=graph_run)
|
||||
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:
|
||||
|
|
@ -157,14 +203,17 @@ class VentureDiscoveryService:
|
|||
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)
|
||||
gap = PortfolioCapabilityGap.objects.create(proposal=proposal, available=available, partial=partial, missing=missing, ranked_missing=ranked, report=report, metadata={"web_market_research_status": "MISSING" if not self.web_research_available else "PARTIAL"})
|
||||
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, "estimated time to first dollar": proposal.time_to_first_dollar_estimate, "expected gross margin": proposal.expected_margin, "validation cost": "$10 initial tranche; $50 maximum after approval", "build effort": proposal.build_complexity, "distribution difficulty": decision.component_scores["Distribution difficulty"]}, "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, "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}}
|
||||
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:
|
||||
|
|
@ -174,11 +223,182 @@ class VentureDiscoveryService:
|
|||
decision.save(update_fields=["metadata", "updated_at"])
|
||||
return decision
|
||||
|
||||
def _company_payload(self, mandate: CompanyMandate) -> tuple[dict[str, Any], str]:
|
||||
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:
|
||||
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. Mandate: " + json.dumps({"objective": mandate.objective, "constraints": mandate.constraints, "optimization_targets": mandate.optimization_targets})))
|
||||
parsed = json.loads(response.content)
|
||||
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:
|
||||
|
|
@ -198,6 +418,17 @@ class VentureDiscoveryService:
|
|||
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
|
||||
|
|
@ -211,6 +442,63 @@ class VentureDiscoveryService:
|
|||
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
|
||||
|
|
@ -226,12 +514,13 @@ class VentureDiscoveryService:
|
|||
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]]:
|
||||
web_status = CapabilityStatus.MISSING if not self.web_research_available else CapabilityStatus.PARTIAL
|
||||
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": "No public web research tool is configured in this service; sources cannot be verified.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {"web_research_available": self.web_research_available}},
|
||||
{"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": {}},
|
||||
|
|
@ -244,6 +533,32 @@ class VentureDiscoveryService:
|
|||
{"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)
|
||||
|
||||
|
|
@ -263,6 +578,11 @@ class VentureDiscoveryService:
|
|||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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')],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
@ -36,6 +36,23 @@ class CapabilityPriority(models.TextChoices):
|
|||
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)
|
||||
|
|
@ -55,6 +72,7 @@ class VentureThesis(TimestampedModel):
|
|||
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):
|
||||
|
|
@ -80,6 +98,7 @@ class CompanyProposal(TimestampedModel):
|
|||
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):
|
||||
|
|
@ -134,6 +153,92 @@ class ICDecision(TimestampedModel):
|
|||
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):
|
||||
|
|
|
|||
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,7 @@ from graph.roadmap import project_roadmap_review_graph_v1
|
|||
from graph.scenario_lab import scenario_lab_graph_v1
|
||||
from graph.steward import steward_run_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
|
||||
|
||||
|
||||
|
|
@ -101,3 +102,7 @@ def champion_agent_investigation_graph_v1() -> ExecutionGraphVersion:
|
|||
|
||||
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
|
||||
|
|
@ -8,7 +8,7 @@ from graph.spec import ExecutionGraphSpec, GraphEdgeSpec, GraphNodeSpec
|
|||
|
||||
|
||||
def venture_discovery_graph_v1() -> ExecutionGraphSpec:
|
||||
nodes = ["prepare_mandate", "generate_company", "board_review", "revise_pitch", "ic_first_pass", "generate_questions", "company_response", "red_team", "final_response", "score", "ic_decision", "capability_analysis", "produce_investment_memo", "complete"]
|
||||
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,
|
||||
|
|
@ -74,6 +74,12 @@ class BoardReviewNode(VentureNode):
|
|||
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)
|
||||
|
|
@ -152,6 +158,6 @@ class InvestmentMemoNode(VentureNode):
|
|||
|
||||
def venture_discovery_registry(service: VentureDiscoveryService) -> NodeHandlerRegistry:
|
||||
registry = NodeHandlerRegistry()
|
||||
for handler in [PrepareMandateNode(service, "venture_prepare_mandate"), GenerateCompanyNode(service, "venture_generate_company"), 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")]:
|
||||
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
|
||||
|
|
|
|||
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
|
||||
|
|
@ -47,8 +47,32 @@ class SolOneCompanyProvider(ModelProvider):
|
|||
return "AVAILABLE"
|
||||
|
||||
|
||||
def service(*, web_research_available: bool = False) -> VentureDiscoveryService:
|
||||
return VentureDiscoveryService(ModelRouter({"sol": SolOneCompanyProvider()}), web_research_available=web_research_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:
|
||||
|
|
@ -93,8 +117,9 @@ def test_board_review_ic_questions_responses_and_bounded_diligence() -> None:
|
|||
|
||||
|
||||
def test_ic_scoring_decision_capability_gap_and_memo_artifact() -> None:
|
||||
svc = service()
|
||||
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)
|
||||
|
|
@ -108,11 +133,12 @@ def test_ic_scoring_decision_capability_gap_and_memo_artifact() -> None:
|
|||
assert decision.decision in ICDecisionType.values
|
||||
assert set(SCORE_DIMENSIONS) == set(decision.component_scores)
|
||||
assert decision.decision == ICDecisionType.CONDITIONAL_FUND
|
||||
assert decision.initial_tranche == Decimal("10.00")
|
||||
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 "WEB_MARKET_RESEARCH" in gap.missing
|
||||
assert gap.metadata["web_market_research_status"] == "MISSING"
|
||||
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
|
||||
|
|
@ -145,12 +171,47 @@ def test_venture_discovery_v1_graph_lineage_and_no_automatic_execution() -> None
|
|||
proposal = CompanyProposal.objects.get()
|
||||
assert proposal.metadata["real_spend"] == 0
|
||||
assert proposal.metadata["real_customer_outreach"] is False
|
||||
assert graph_run.node_runs.count() == 13
|
||||
assert graph_run.edge_traversals.count() == 13
|
||||
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", "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)
|
||||
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