Add autonomous Venture research smoke

This commit is contained in:
Daniel Maddern 2026-08-16 17:48:04 +07:00
parent edee2c84bc
commit d25a4c34f1
9 changed files with 422 additions and 35 deletions

View file

@ -16,7 +16,7 @@ from django.db import close_old_connections, connection
from django.utils import timezone
from control_plane.events.bus import EventBus
from control_plane.ventures.models import CapabilityPriority, CapabilityStatus, CohortIdeationMandate, CompanyBoardReview, CompanyCapabilityRequirement, CompanyMandate, CompanyProposal, CompanyProposalStatus, EvidenceTier, ICDecision, ICDecisionType, ICDiligence, ICQuestion, ICResponse, NoveltyGateDecision, OpportunityTerritory, OverlapClassification, PortfolioCapabilityGap, PortfolioICReview, PortfolioSaturationAnalysis, PortfolioThesis, PortfolioThesisCluster, PortfolioThesisStatus, ThesisMatchClassification, VentureArtifact, VentureCapabilityDemand, VentureCohort, VentureCohortMember, VentureCollision, VentureGenerationRejection, VentureThesis, VentureThesisFingerprint
from control_plane.ventures.models import AutonomousCandidateStatus, AutonomousGateResult, AutonomousOperabilityAssessment, CapabilityPriority, CapabilityStatus, CohortIdeationMandate, CompanyBoardReview, CompanyCapabilityRequirement, CompanyMandate, CompanyProposal, CompanyProposalStatus, EvidenceTier, FounderDependencyLevel, ICDecision, ICDecisionType, ICDiligence, ICQuestion, ICResponse, NoveltyGateDecision, OpportunityTerritory, OverlapClassification, PortfolioCapabilityGap, PortfolioICReview, PortfolioSaturationAnalysis, PortfolioThesis, PortfolioThesisCluster, PortfolioThesisStatus, ThesisMatchClassification, VentureArtifact, VentureCapabilityDemand, VentureCohort, VentureCohortMember, VentureCollision, VentureGenerationRejection, VentureThesis, VentureThesisFingerprint, VentureTrack
from graph.models import GraphRun, GraphRunStatus
from model_router.providers import extract_json_object
from model_router.policy import model_for_role
@ -25,7 +25,7 @@ from research.searxng import SearxngSearchClient, WebPageFetcher
PITCH_SECTIONS = ["Company name", "One-line thesis", "Problem", "ICP", "Why now", "Product / service", "Business model", "Pricing", "Route to first customer", "Validation plan", "$50 capital allocation proposal", "Time to first dollar", "Path to $500 net cash", "Competition", "Differentiation", "Build requirements", "Distribution requirements", "Risks", "What would falsify the thesis", "Confidence"]
SCORE_DIMENSIONS = ["Demand Evidence", "Time-to-First-Dollar Attractiveness", "Capital Efficiency", "Validation Affordability", "Gross Margin Potential", "Distribution Feasibility", "Build Simplicity", "Defensibility", "Market Opportunity", "Competitive Position", "Risk Manageability", "AI Leverage", "Platformization Potential", "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", "AI Leverage", "Platformization Potential", "Autonomous Operability", "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}
AI_NATIVE_POLICY = {"preference": "Prefer AI-native opportunities where Artifex can build or deliver most value with existing agents, local inference, and owned workflow capabilities.", "preferred_classes": ["AI wrapper platforms", "AI-enabled services", "AI infrastructure / developer tools", "AI research / intelligence products", "AI automation for SMB / enterprise workflows"], "soft_portfolio_targets": {"ai_wrapper_platforms": "40-50%", "ai_enabled_services": "20-30%", "developer_infrastructure_intelligence": "10-20%", "non_ai_economic_outliers": "10-20%"}, "penalize_concentration": ["Shopify/ecommerce", "generic audits", "emergency fix services", "one-off consulting", "identical outbound-led service models"], "do_not": "Do not let AI novelty outweigh customer demand."}
@ -34,6 +34,7 @@ DEFAULT_SOFT_EXCLUSIONS = ["generic contract scanners", "Shopify audit products"
DEFAULT_TERRITORY_ALLOCATION = [OpportunityTerritory.VERTICAL_AI_WRAPPERS, OpportunityTerritory.VERTICAL_AI_WRAPPERS, OpportunityTerritory.ENTERPRISE_WORKFLOW_AUTOMATION, OpportunityTerritory.SMB_AUTOMATION, OpportunityTerritory.DEVELOPER_AI_INFRASTRUCTURE, OpportunityTerritory.INTELLIGENCE_MONITORING, OpportunityTerritory.DATA_DOCUMENT_AUTOMATION, OpportunityTerritory.AI_ENABLED_SERVICE_TO_PLATFORM, OpportunityTerritory.OPEN_CATEGORY, OpportunityTerritory.OPEN_CATEGORY]
DEFAULT_DUPLICATE_POLICY = {"max_near_duplicate_per_thesis_per_cohort": 1, "max_competitive_per_thesis_per_cohort": 2, "max_attempts_per_slot": 3, "cohort_attempt_budget_multiplier": 4}
RESEARCH_CATEGORIES = ["competitors", "pricing", "customer_pain", "market_alternatives", "regulatory_platform_risks"]
AUTONOMOUS_ARCHETYPES = ["SELF_SERVICE_AI_TOOL", "AUTOMATED_MONITORING_PRODUCT", "DIGITAL_ANALYSIS_SERVICE", "AUTOMATED_TRANSFORMATION_SERVICE", "MICRO_SAAS", "DATA_INTELLIGENCE_SUBSCRIPTION", "DEVELOPER_TOOL", "AUTONOMOUS_DIGITAL_PRODUCT", "MARKETPLACE_DELIVERED_SERVICE", "AGENT_AS_A_SERVICE"]
class VentureDiscoveryService:
@ -49,10 +50,10 @@ class VentureDiscoveryService:
def create_v0_mandate(self) -> CompanyMandate:
mandate = CompanyMandate.objects.create(
objective="Design a business that could plausibly turn at most $50 of external validation capital into $500 net new cash.",
constraints={"external_validation_capital_max": 50, "target_net_new_cash": 500, "target_window_days": 30, "no_equity_raise": True, "no_debt": True, "no_illegal_or_deceptive_activity": True, "no_spam": True, "no_fake_traction": True, "no_fabricated_customer_evidence": True, "no_real_spend_in_v0": True, "no_real_customer_outreach_in_v0": True, "existing_artifex_compute_sunk_available": True},
optimization_targets=["time to first dollar", "capital efficiency", "real demand evidence", "high gross margin", "realistic execution", "low external dependency", "ability to validate cheaply", "AI leverage where economically justified", "service-to-platform evolution potential"],
metadata={"milestone": "VENTURE_DISCOVERY_V0", "spend_authorized": False, "customer_outreach_authorized": False, "ai_native_policy": AI_NATIVE_POLICY},
objective="Design a business that Artifex itself could plausibly operate and turn at most $50 of external validation capital into at least $500 net new cash within 30 days, with no more than 30 minutes/week of routine human intervention.",
constraints={"external_validation_capital_max": 50, "target_net_new_cash": 500, "target_window_days": 30, "max_human_routine_minutes_per_week": 30, "default_venture_track": VentureTrack.AUTONOMOUS, "routine_human_involvement_not_allowed": ["founder-led sales calls", "manual prospecting", "routine support", "manual fulfillment", "bespoke consulting", "routine QA", "manual payment chasing", "routine onboarding"], "allowed_human_involvement": ["approve account/legal setup", "approve initial spend", "approve material legal commitments", "exceptional safety/security escalation", "irreversible capital decisions"], "no_equity_raise": True, "no_debt": True, "no_illegal_or_deceptive_activity": True, "no_spam": True, "no_fake_traction": True, "no_fabricated_customer_evidence": True, "no_real_spend_in_v0": True, "no_real_customer_outreach_in_v0": True, "existing_artifex_compute_sunk_available": True, "validation_deployment": "SUBDOMAIN_DEPLOYMENT"},
optimization_targets=["autonomous fulfillment", "autonomous customer acquisition", "self-service onboarding", "standardized digital delivery", "AI leverage", "platformization", "recurring revenue", "simple payment flow", "low support burden", "low regulatory burden", "low integration burden", "rapid validation"],
metadata={"milestone": "VENTURE_DISCOVERY_V0_AUTONOMOUS", "venture_track": VentureTrack.AUTONOMOUS, "spend_authorized": False, "customer_outreach_authorized": False, "ai_native_policy": AI_NATIVE_POLICY, "autonomous_archetypes": AUTONOMOUS_ARCHETYPES},
)
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
@ -92,7 +93,7 @@ class VentureDiscoveryService:
confidence=confidence,
status=CompanyProposalStatus.SUBMITTED,
pitch=pitch,
metadata={"generation_source": source, "fallback_evidence": source == "deterministic_fallback", "web_research_available": self.web_research_available, "real_spend": 0, "real_customer_outreach": False},
metadata={"generation_source": source, "fallback_evidence": source == "deterministic_fallback", "web_research_available": self.web_research_available, "real_spend": 0, "real_customer_outreach": False, "validation_offer": payload.get("validation_offer", {}), "end_state_business_model": payload.get("end_state_business_model", {}), "fulfillment_contract": payload.get("fulfillment_contract", {}), "minutes_per_week_human": payload.get("minutes_per_week_human", None), "human_actions_required": self._as_list(payload.get("human_actions_required", []))},
evidence_tier=EvidenceTier.TIER_0_THESIS,
)
self._artifact(proposal, mandate, "STANDARDIZED_COMPANY_PITCH", "Standardized Company Pitch", pitch, self.readable_pitch(pitch), f"Company Brain/{source}" if source != "deterministic_fallback" else "deterministic_fallback", graph_run=graph_run)
@ -104,6 +105,10 @@ class VentureDiscoveryService:
research = {"coverage": {category: False for category in required}, "sources": [], "findings": {}, "unverified_categories": required, "research_available": False}
search_sources = self._searxng_sources(proposal, required, depth=depth) if self.web_research_available else []
page_corpus = self._page_corpus(search_sources) if search_sources else []
diagnostics = {"search_result_count": len(search_sources), "page_fetch_count": len(page_corpus), "model_provider": self.research_model_hint if self.router is not None else "none", "search_provider": "searxng" if search_sources else "none"}
search_diagnostics = getattr(self, "_last_search_diagnostics", {})
if search_diagnostics:
diagnostics["search_diagnostics"] = search_diagnostics
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). Use only the provided SearXNG search context and fetched page excerpts for source URLs; do not fabricate URLs. If a category has no strong relevant source, mark coverage false. Research depth: " + depth + ". Search context: " + json.dumps(search_sources, default=str) + " Page excerpts: " + json.dumps(page_corpus, default=str) + " Pitch: " + json.dumps(proposal.pitch, default=str)))
@ -119,7 +124,7 @@ class VentureDiscoveryService:
if page_corpus:
research = {**research, "page_corpus": page_corpus, "page_fetch_count": len(page_corpus)}
if not research.get("sources"):
research = self._missing_research(required, research.get("failure", "public web research unavailable or returned no source-linked evidence"))
research = {**self._missing_research(required, research.get("failure", "public web research unavailable or returned no source-linked evidence")), **diagnostics}
quality = self.filter_research_sources(proposal, research.get("sources", []), required)
research["sources"] = quality["accepted_sources"]
research["source_rejections"] = quality["rejected_sources"]
@ -134,7 +139,8 @@ class VentureDiscoveryService:
proposal.confidence = round(min(float(proposal.confidence), 0.45 + 0.35 * coverage_ratio), 2)
existing_research = proposal.metadata.get("research", {}) if isinstance(proposal.metadata, dict) else {}
proposal.market_evidence = [*self._as_list(proposal.market_evidence), *research.get("sources", []), {"type": "research_coverage", "source": "venture_research", "summary": f"{depth.title()} source-linked research coverage: {round(coverage_ratio * 100)}%", "coverage": coverage, "unverified_categories": research["unverified_categories"], "depth": depth}]
proposal.metadata = {**proposal.metadata, "research": {**existing_research, "coverage_ratio": coverage_ratio, "coverage": coverage, "unverified_categories": research["unverified_categories"], "source_count": len(research.get("sources", [])), "source_rejection_count": len(quality["rejected_sources"]), "page_fetch_count": research.get("page_fetch_count", 0), "provider": research.get("provider", "none"), "search_provider": research.get("search_provider", "none"), "depth": depth, "explicit_research_failure": research.get("failure", "") if coverage_ratio < 0.8 else "", "before_deep_coverage_ratio": existing_research.get("coverage_ratio") if depth == "deep" else existing_research.get("before_deep_coverage_ratio")}}
explicit_failure = "" if research.get("sources") else research.get("failure", "public web research unavailable or returned no source-linked evidence")
proposal.metadata = {**proposal.metadata, "research": {**existing_research, "coverage_ratio": coverage_ratio, "coverage": coverage, "unverified_categories": research["unverified_categories"], "source_count": len(research.get("sources", [])), "source_rejection_count": len(quality["rejected_sources"]), "search_result_count": research.get("search_result_count", len(search_sources)), "page_fetch_count": research.get("page_fetch_count", len(page_corpus)), "provider": research.get("provider", diagnostics.get("model_provider", "none")), "search_provider": research.get("search_provider", diagnostics["search_provider"]), "search_diagnostics": research.get("search_diagnostics", diagnostics.get("search_diagnostics", {})), "model_research_failure": research.get("failure", "") if research.get("sources") else "", "depth": depth, "explicit_research_failure": explicit_failure, "before_deep_coverage_ratio": existing_research.get("coverage_ratio") if depth == "deep" else existing_research.get("before_deep_coverage_ratio")}}
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
@ -208,7 +214,13 @@ class VentureDiscoveryService:
return response
def score_and_decide(self, diligence: ICDiligence, *, graph_run=None) -> ICDecision:
try:
assessment = diligence.proposal.autonomous_assessment
except ObjectDoesNotExist:
assessment = None
scores = self._evidence_scores(diligence.proposal)
if assessment is not None:
scores["Autonomous Operability"] = int(assessment.autonomous_operability_score)
composite = round(sum(scores.values()) / len(scores), 1)
calibration = self.calibrate_probability(diligence.proposal, raw_probability=float(scores["Probability of Reaching $500"]))
scores["Probability of Reaching $500"] = int(calibration["evidence_adjusted_probability"])
@ -419,6 +431,7 @@ class VentureDiscoveryService:
self.red_team(diligence)
self.final_company_response(diligence)
self.score_and_decide(diligence)
self.assess_autonomous_operability(proposal)
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
@ -431,7 +444,85 @@ class VentureDiscoveryService:
cohort.status = "INDIVIDUAL_DILIGENCE_COMPLETE"
cohort.save(update_fields=["metrics", "status", "updated_at"])
def assess_autonomous_operability_for_cohort(self, cohort: VentureCohort) -> list[AutonomousOperabilityAssessment]:
assessments = [self.assess_autonomous_operability(member.proposal) for member in cohort.members.select_related("proposal")]
cohort.metrics = {**cohort.metrics, "autonomous_eligible_count": sum(1 for item in assessments if item.gate_result == AutonomousGateResult.AUTONOMOUS_ELIGIBLE), "assisted_only_count": sum(1 for item in assessments if item.gate_result == AutonomousGateResult.ASSISTED_ONLY)}
cohort.save(update_fields=["metrics", "updated_at"])
return assessments
def assess_autonomous_operability(self, proposal: CompanyProposal) -> AutonomousOperabilityAssessment:
text = " ".join([proposal.title, proposal.description, proposal.problem, proposal.target_customer, proposal.proposed_solution, proposal.business_model, proposal.pricing_hypothesis, proposal.acquisition_strategy, proposal.validation_plan, proposal.differentiation]).lower()
positive_terms = ["self-service", "automated", "monitor", "subscription", "api", "dashboard", "report", "digital", "crawl", "alert", "standardized", "recurring", "checkout", "plugin", "cli", "no-code"]
negative_causes = {
"SALES": ["sales call", "enterprise sales", "procurement", "relationship"],
"DOMAIN_EXPERTISE": ["expert", "clinical", "medical", "licensed", "engineer review"],
"CUSTOMER_TRUST": ["trust", "advisor", "consultant"],
"RELATIONSHIP_MANAGEMENT": ["account management", "relationship"],
"MANUAL_QA": ["manual qa", "manual review", "human review"],
"REGULATORY_SIGNOFF": ["regulatory sign-off", "compliance signoff", "approval"],
"CUSTOM_IMPLEMENTATION": ["custom implementation", "integration", "bespoke"],
"SUPPORT_ESCALATION": ["high-touch support", "white glove"],
"OFFLINE_ACTIVITY": ["onsite", "offline", "in person"],
"LEGAL_JUDGMENT": ["legal judgment", "legal advice", "lawyer"],
"NEGOTIATION": ["negotiation"],
"PERSONAL_BRAND": ["personal brand", "founder credibility"],
}
causes = [cause for cause, terms in negative_causes.items() if any(term in text for term in terms)]
score = 45 + min(30, sum(5 for term in positive_terms if term in text)) - min(45, len(causes) * 8)
ai = self.ai_leverage_score(proposal)
platform = self.platformization_potential(proposal)
score += 10 if ai >= 70 else 0
score += 10 if platform >= 70 else 0
score = max(0, min(100, score))
minutes = 5 if score >= 90 else 15 if score >= 80 else 30 if score >= 70 else 90 if score >= 50 else 240
dependency = FounderDependencyLevel.NONE if minutes <= 5 and not causes else FounderDependencyLevel.LOW if minutes <= 30 and len(causes) <= 1 else FounderDependencyLevel.MEDIUM if minutes <= 90 else FounderDependencyLevel.HIGH if minutes <= 240 else FounderDependencyLevel.CRITICAL
if score >= 80 and dependency in {FounderDependencyLevel.NONE, FounderDependencyLevel.LOW} and ai >= 70 and platform >= 70:
gate = AutonomousGateResult.AUTONOMOUS_ELIGIBLE
track = VentureTrack.AUTONOMOUS
elif score >= 70 and dependency in {FounderDependencyLevel.LOW, FounderDependencyLevel.MEDIUM}:
gate = AutonomousGateResult.AUTONOMOUS_BORDERLINE
track = VentureTrack.AUTONOMOUS
elif score >= 45:
gate = AutonomousGateResult.ASSISTED_ONLY
track = VentureTrack.ASSISTED
else:
gate = AutonomousGateResult.REJECT_OPERABILITY
track = VentureTrack.ASSISTED
loop = self.autonomous_operating_loop(proposal, gate)
contract = self.autonomous_fulfillment_contract(proposal)
assessment, _ = AutonomousOperabilityAssessment.objects.update_or_create(
proposal=proposal,
defaults={"venture_track": track, "gate_result": gate, "autonomous_operability_score": score, "commercial_score": self.commercial_score(proposal), "founder_dependency": dependency, "dependency_causes": causes, "minutes_per_week_human": minutes, "human_actions_required": contract["escalation"], "human_action_categories": ["LEGAL_SETUP", "SPEND_APPROVAL", "EXCEPTION_ESCALATION"] if minutes <= 30 else causes, "operating_loop": loop, "fulfillment_contract": contract, "validation_offer": self.validation_offer(proposal), "end_state_business_model": self.end_state_business_model(proposal), "structural_blockers": causes, "platform_blockers": self.platform_blockers(proposal), "component_scores": {"customer_acquisition_autonomy": loop["discover"]["score"], "onboarding_autonomy": loop["onboard"]["score"], "fulfillment_autonomy": loop["fulfill"]["score"], "support_autonomy": loop["support"]["score"], "billing_payment_autonomy": loop["checkout"]["score"], "quality_verification": loop["verify"]["score"], "repeatability": 90 if "recurring" in text or "subscription" in text else 65}, "rationale": f"{gate}: score {score}, dependency {dependency}, causes {', '.join(causes) or 'none'}."},
)
proposal.metadata = {**proposal.metadata, "venture_track": track, "autonomous_operability_score": score, "founder_dependency": dependency, "autonomous_gate_result": gate, "minutes_per_week_human": minutes, "validation_offer": assessment.validation_offer, "end_state_business_model": assessment.end_state_business_model}
proposal.save(update_fields=["metadata", "updated_at"])
self._artifact(proposal, proposal.mandate, "AUTONOMOUS_OPERABILITY_REPORT", "Autonomous Operability Report", {"score": score, "gate_result": gate, "founder_dependency": dependency, "dependency_causes": causes, "operating_loop": loop, "fulfillment_contract": contract, "validation_offer": assessment.validation_offer, "end_state_business_model": assessment.end_state_business_model, "platform_blockers": assessment.platform_blockers, "structural_blockers": assessment.structural_blockers}, self._readable_memo({"score": score, "gate_result": gate, "founder_dependency": dependency, "dependency_causes": causes, "operating_loop": loop}), "Autonomous Operability IC")
return assessment
def commercial_score(self, proposal: CompanyProposal) -> float:
diligence = proposal.ic_diligence.order_by("-created_at").first()
return float(diligence.decision.composite_score) if diligence and hasattr(diligence, "decision") else 0.0
def autonomous_operating_loop(self, proposal: CompanyProposal, gate: str) -> dict[str, dict[str, Any]]:
automated = gate in {AutonomousGateResult.AUTONOMOUS_ELIGIBLE, AutonomousGateResult.AUTONOMOUS_BORDERLINE}
state = "AUTONOMOUS" if automated else "HUMAN_ROUTINE_REQUIRED"
score = 90 if automated else 35
return {stage: {"capability": "AVAILABLE" if stage in {"discover", "checkout", "deliver", "measure"} else "PARTIAL", "operation": state, "score": score} for stage in ["discover", "qualify", "acquire", "checkout", "onboard", "fulfill", "verify", "deliver", "support", "measure", "retain_upsell"]}
def autonomous_fulfillment_contract(self, proposal: CompanyProposal) -> dict[str, Any]:
return {"customer_input": "Customer supplies a URL, file, repository, document, dataset, or narrow workflow input through a self-service intake.", "artifex_process": "Artifex agents run retrieval, analysis/transformation, structured QA, report generation, and delivery workflows.", "customer_output": proposal.proposed_solution, "quality_verification": "Automated checks validate schema completeness, source links, threshold scores, and known failure conditions before delivery.", "failure_recovery": "If verification fails, rerun once with stricter constraints; unresolved failures create exceptional human escalation.", "billing_event": "Payment is collected at checkout before one-off validation delivery or at subscription activation.", "support_model": "Agent-authored help, status updates, and retry guidance; human handles only exceptional safety/legal/account issues.", "escalation": ["material legal commitment", "irreversible spend", "security/safety exception", "verification repeatedly fails"]}
def validation_offer(self, proposal: CompanyProposal) -> dict[str, Any]:
return {"offer": "$49-$99 fixed-scope automated validation output", "price": "$49-$99", "proof": "willingness-to-pay for a standardized digital result before full product build", "aligned_with_end_state": True}
def end_state_business_model(self, proposal: CompanyProposal) -> dict[str, Any]:
return {"model": proposal.business_model, "likely_pricing": proposal.pricing_hypothesis, "recurring_path": "subscription, usage, or repeat purchase if validation demand repeats"}
def platform_blockers(self, proposal: CompanyProposal) -> list[str]:
return ["STRIPE_IMPLEMENTATION_MISSING", "SUBDOMAIN_DEPLOYMENT", "EMAIL_DELIVERY", "SUPPORT_INBOX"]
def portfolio_ic(self, cohort: VentureCohort) -> PortfolioICReview:
self.assess_autonomous_operability_for_cohort(cohort)
preliminary_rows = self.portfolio_ranking_rows(cohort)
preliminary_top_5 = preliminary_rows[:5]
before_coverage = {}
@ -452,15 +543,17 @@ class VentureDiscoveryService:
rows = self.portfolio_ranking_rows(cohort)
preliminary_rank_by_id = {row["proposal_id"]: index + 1 for index, row in enumerate(preliminary_rows)}
ranking_changes = [{"proposal_id": row["proposal_id"], "company": row["company"], "preliminary_rank": preliminary_rank_by_id.get(row["proposal_id"]), "final_rank": index + 1} for index, row in enumerate(rows) if preliminary_rank_by_id.get(row["proposal_id"]) != index + 1]
top_3 = [row for row in rows if row.get("autonomous_gate_result") == AutonomousGateResult.AUTONOMOUS_ELIGIBLE][:3]
top_3_ids = {row["proposal_id"] for row in top_3}
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.is_top_3 = row["proposal_id"] in top_3_ids
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, "preliminary_rankings": preliminary_rows, "preliminary_top_5": preliminary_top_5, "finalist_research_coverage_before": before_coverage, "finalist_research_coverage_after": after_coverage, "ranking_changes_after_deep_research": ranking_changes}})
review, _ = PortfolioICReview.objects.update_or_create(cohort=cohort, defaults={"rankings": rows, "top_3": top_3, "concentration": concentration, "metadata": {"no_funding": True, "preliminary_rankings": preliminary_rows, "preliminary_top_5": preliminary_top_5, "finalist_research_coverage_before": before_coverage, "finalist_research_coverage_after": after_coverage, "ranking_changes_after_deep_research": ranking_changes, "autonomous_top_3_shortfall": max(0, 3 - len(top_3))}})
cohort.metrics = {**cohort.metrics, "finalist_deep_research_count": len(preliminary_top_5), "finalist_deep_research_runtime_seconds": round(time.monotonic() - deep_started, 2), "finalist_research_coverage_before": before_coverage, "finalist_research_coverage_after": after_coverage, "ranking_changes_after_deep_research": ranking_changes}
cohort.status = "PORTFOLIO_IC_COMPLETE"
cohort.save(update_fields=["metrics", "status", "updated_at"])
@ -475,11 +568,13 @@ class VentureDiscoveryService:
for member in cohort.members.select_related("proposal"):
proposal = member.proposal
decision = proposal.ic_diligence.order_by("-created_at").first().decision
assessment = self.assess_autonomous_operability(proposal)
capability_burden = proposal.capability_requirements.filter(status=CapabilityStatus.MISSING).count()
concentration_penalty = self._generic_concentration_penalty(proposal)
ai_bonus = (decision.component_scores.get("AI Leverage", 0) * 0.08) + (decision.component_scores.get("Platformization Potential", 0) * 0.06)
score = round(decision.composite_score + decision.probability_500_within_30_days * 0.2 + ai_bonus - capability_burden * 1.5 - collision_risk[str(proposal.id)] * 2 - concentration_penalty, 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"), "ai_leverage": decision.component_scores.get("AI Leverage", 0), "platformization_potential": decision.component_scores.get("Platformization Potential", 0), "portfolio_score": score, "capability_burden": capability_burden, "collision_risk": collision_risk[str(proposal.id)], "concentration_penalty": concentration_penalty})
autonomy_penalty = 35 if assessment.gate_result == AutonomousGateResult.ASSISTED_ONLY else 70 if assessment.gate_result == AutonomousGateResult.REJECT_OPERABILITY else 12 if assessment.gate_result == AutonomousGateResult.AUTONOMOUS_BORDERLINE else 0
score = round(decision.composite_score * 0.45 + assessment.autonomous_operability_score * 0.45 + decision.probability_500_within_30_days * 0.12 + ai_bonus - capability_burden * 1.5 - collision_risk[str(proposal.id)] * 2 - concentration_penalty - autonomy_penalty, 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, "commercial_score": assessment.commercial_score, "autonomy_score": assessment.autonomous_operability_score, "autonomous_gate_result": assessment.gate_result, "founder_dependency": assessment.founder_dependency, "minutes_per_week_human": assessment.minutes_per_week_human, "probability": decision.probability_500_within_30_days, "decision": decision.decision, "evidence_tier": decision.evidence_tier, "initial_tranche": str(decision.initial_tranche or "0"), "ai_leverage": decision.component_scores.get("AI Leverage", 0), "platformization_potential": decision.component_scores.get("Platformization Potential", 0), "portfolio_score": score, "capability_burden": capability_burden, "collision_risk": collision_risk[str(proposal.id)], "concentration_penalty": concentration_penalty})
rows.sort(key=lambda item: item["portfolio_score"], reverse=True)
return rows
@ -547,9 +642,15 @@ class VentureDiscoveryService:
thesis.first_seen_cohort = thesis.first_seen_cohort or cohort
thesis.last_seen_cohort = cohort
thesis.status = analysis.status_recommendation
best_assessment = self.assess_autonomous_operability(best[0]) if best else None
if best_assessment:
thesis.venture_track = best_assessment.venture_track
thesis.best_autonomous_operability_score = max(float(thesis.best_autonomous_operability_score or 0.0), best_assessment.autonomous_operability_score)
thesis.best_founder_dependency = best_assessment.founder_dependency
thesis.autonomous_candidate_status = AutonomousCandidateStatus.AUTONOMOUS_CANDIDATE if best_assessment.gate_result == AutonomousGateResult.AUTONOMOUS_ELIGIBLE else AutonomousCandidateStatus.ASSISTED_CANDIDATE if best_assessment.gate_result == AutonomousGateResult.ASSISTED_ONLY else AutonomousCandidateStatus.AUTONOMOUS_REJECTED if best_assessment.gate_result == AutonomousGateResult.REJECT_OPERABILITY else AutonomousCandidateStatus.NOT_ASSESSED
thesis.metadata = {**thesis.metadata, "last_saturation_rationale": analysis.rationale, "last_reopen_reason": thesis.metadata.get("last_reopen_reason", "")}
thesis.save(update_fields=["proposal_count", "best_ic_score", "best_company", "first_seen_cohort", "last_seen_cohort", "status", "metadata", "updated_at"])
updates.append({"thesis": thesis.canonical_name, "status": thesis.status, "proposal_count": thesis.proposal_count, "best_ic_score": thesis.best_ic_score})
thesis.save(update_fields=["proposal_count", "best_ic_score", "best_company", "first_seen_cohort", "last_seen_cohort", "status", "venture_track", "best_autonomous_operability_score", "best_founder_dependency", "autonomous_candidate_status", "metadata", "updated_at"])
updates.append({"thesis": thesis.canonical_name, "status": thesis.status, "proposal_count": thesis.proposal_count, "best_ic_score": thesis.best_ic_score, "venture_track": thesis.venture_track, "best_autonomous_operability_score": thesis.best_autonomous_operability_score, "best_founder_dependency": thesis.best_founder_dependency, "autonomous_candidate_status": thesis.autonomous_candidate_status})
cohort.metadata = {**cohort.metadata, "thesis_registry_after": self.registry_snapshot()}
cohort.save(update_fields=["metadata", "updated_at"])
return updates
@ -587,15 +688,20 @@ class VentureDiscoveryService:
review = cohort.portfolio_review
ideation_mandate = getattr(cohort, "ideation_mandate", None)
clusters = [{"company": cluster.proposal.title, "portfolio_thesis": cluster.portfolio_thesis.canonical_name if cluster.portfolio_thesis else "", "classification": cluster.classification, "similarity_score": cluster.similarity_score, "explanation": cluster.explanation} for cluster in cohort.thesis_clusters.select_related("proposal", "portfolio_thesis").order_by("created_at")]
autonomy = [self.autonomy_report_row(member.proposal) for member in cohort.members.select_related("proposal").order_by("rank", "created_at")]
generation_rejections = [{"slot_index": rejection.slot_index, "attempt": rejection.attempt, "decision": rejection.decision, "reason": rejection.reason, "candidate_title": rejection.candidate.get("title", "") if isinstance(rejection.candidate, dict) else "", "similarity_score": rejection.similarity_score} for rejection in cohort.generation_rejections.order_by("slot_index", "attempt")]
saturation = [{"thesis": analysis.portfolio_thesis.canonical_name, "proposal_count": analysis.proposal_count, "best_ic_score": analysis.best_ic_score, "score_spread": analysis.score_spread, "status_recommendation": analysis.status_recommendation, "rationale": analysis.rationale} for analysis in cohort.saturation_analyses.select_related("portfolio_thesis").order_by("-proposal_count")]
accepted_count = cohort.members.count()
content = {"cohort_id": cohort.cohort_id, "mandate": cohort.mandate.objective, "accepted_count": accepted_count, "requested_count": cohort.cohort_size, "ideation_mandate": self.ideation_mandate_payload(ideation_mandate) if ideation_mandate else {}, "hard_exclusions": ideation_mandate.hard_exclusions if ideation_mandate else [], "soft_exclusions": ideation_mandate.soft_exclusions if ideation_mandate else [], "search_territories": ideation_mandate.opportunity_territories if ideation_mandate else [], "generation_rejections": generation_rejections, "thesis_registry_before": (ideation_mandate.registry_snapshot.get("theses", []) if ideation_mandate else []), "thesis_registry_after": cohort.metadata.get("thesis_registry_after", []), "thesis_clusters": clusters, "saturation_analysis": saturation, "idea_diversity_metrics": self.diversity_metrics(cohort), "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}
content = {"cohort_id": cohort.cohort_id, "mandate": cohort.mandate.objective, "accepted_count": accepted_count, "requested_count": cohort.cohort_size, "venture_track": VentureTrack.AUTONOMOUS, "autonomous_operability_report": autonomy, "assisted_only_companies": [row for row in autonomy if row.get("gate_result") == AutonomousGateResult.ASSISTED_ONLY], "ideation_mandate": self.ideation_mandate_payload(ideation_mandate) if ideation_mandate else {}, "hard_exclusions": ideation_mandate.hard_exclusions if ideation_mandate else [], "soft_exclusions": ideation_mandate.soft_exclusions if ideation_mandate else [], "search_territories": ideation_mandate.opportunity_territories if ideation_mandate else [], "generation_rejections": generation_rejections, "thesis_registry_before": (ideation_mandate.registry_snapshot.get("theses", []) if ideation_mandate else []), "thesis_registry_after": cohort.metadata.get("thesis_registry_after", []), "thesis_clusters": clusters, "saturation_analysis": saturation, "idea_diversity_metrics": self.diversity_metrics(cohort), "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" if accepted_count == cohort.cohort_size else "PARTIAL_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 autonomy_report_row(self, proposal: CompanyProposal) -> dict[str, Any]:
assessment = self.assess_autonomous_operability(proposal)
return {"company": proposal.title, "venture_track": assessment.venture_track, "gate_result": assessment.gate_result, "commercial_score": assessment.commercial_score, "autonomous_operability_score": assessment.autonomous_operability_score, "founder_dependency": assessment.founder_dependency, "dependency_causes": assessment.dependency_causes, "minutes_per_week_human": assessment.minutes_per_week_human, "human_actions_required": assessment.human_actions_required, "operating_loop": assessment.operating_loop, "fulfillment_contract": assessment.fulfillment_contract, "validation_offer": assessment.validation_offer, "end_state_business_model": assessment.end_state_business_model, "structural_blockers": assessment.structural_blockers, "platform_blockers": assessment.platform_blockers, "component_scores": assessment.component_scores, "rationale": assessment.rationale}
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))
@ -912,7 +1018,7 @@ class VentureDiscoveryService:
try:
slot = f" Independent cohort slot: {ideation_index}. Do not use or imitate other cohort ideas; no other ideas are visible." if ideation_index else ""
context = ideation_context or {}
response = self.router.complete(ModelRequestContract(purpose=ModelCapability.PLANNING, model_hint=self.ideation_model_hint, prompt="Generate exactly ONE startup idea for Venture Discovery V0.3. Return a single JSON object, not a list. Respect no spend and no outreach in V0. Keep the $50 to $500 in 30 days mandate. Prefer AI-native businesses where Artifex can deliver most value using agents/local inference, but do not allow 'uses AI' to substitute for real customer pain. Prefer AI wrapper platforms, agentic workflow products, vertical copilots, AI-enabled services with low human labor, developer/AI infrastructure, intelligence products, automation products, service-to-platform paths, recurring revenue, and owned-compute leverage. DO NOT propose companies substantially equivalent to these hard-excluded thesis areas, including semantic variants: " + json.dumps(context.get("hard_exclusions", [])) + ". Soft exclusions require material differentiation and an explicit exception rationale: " + json.dumps(context.get("soft_exclusions", [])) + ". Search territory for this slot: " + str(context.get("territory", "OPEN_CATEGORY")) + ". This is a creative search constraint, not a fixed solution. 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, and optional exception_rationale." + slot + " Mandate: " + json.dumps({"objective": mandate.objective, "constraints": mandate.constraints, "optimization_targets": mandate.optimization_targets, "ai_native_policy": AI_NATIVE_POLICY, "ideation_brief": context.get("brief", {})})))
response = self.router.complete(ModelRequestContract(purpose=ModelCapability.PLANNING, model_hint=self.ideation_model_hint, prompt="Generate exactly ONE startup idea for an AUTONOMOUS Venture Discovery cohort. Return a single JSON object, not a list. The business must be operable by Artifex with <=30 minutes/week routine human involvement. Prefer archetypes: " + json.dumps(AUTONOMOUS_ARCHETYPES) + ". Require concrete digital operating loop: discover lead/user -> qualify -> acquire -> checkout -> onboard -> fulfill -> verify -> deliver -> support -> measure -> retain/upsell. Include validation_offer and end_state_business_model as separate fields. Include fulfillment_contract with customer_input, artifex_process, customer_output, quality_verification, failure_recovery, billing_event, support_model, escalation. Avoid enterprise procurement, founder-led calls, bespoke consulting, legal/medical judgment, offline fulfillment, and custom implementation. Respect no spend and no outreach in V0. Keep the $50 to $500 in 30 days mandate. Prefer AI-native businesses where Artifex can deliver most value using agents/local inference, but do not allow 'uses AI' to substitute for real customer pain. DO NOT propose companies substantially equivalent to these hard-excluded thesis areas, including semantic variants: " + json.dumps(context.get("hard_exclusions", [])) + ". Soft exclusions require material differentiation and an explicit exception rationale: " + json.dumps(context.get("soft_exclusions", [])) + ". Search territory for this slot: " + str(context.get("territory", "OPEN_CATEGORY")) + ". 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, minutes_per_week_human, human_actions_required, and optional exception_rationale." + slot + " Mandate: " + json.dumps({"objective": mandate.objective, "constraints": mandate.constraints, "optimization_targets": mandate.optimization_targets, "ai_native_policy": AI_NATIVE_POLICY, "ideation_brief": context.get("brief", {})})))
parsed = extract_json_object(response.content)
if isinstance(parsed, dict) and parsed.get("title"):
return self._normalize_payload(parsed), self.ideation_model_hint
@ -933,6 +1039,9 @@ class VentureDiscoveryService:
normalized["major_risks"] = self._as_list(normalized.get("major_risks"))
if payload.get("exception_rationale"):
normalized["exception_rationale"] = payload["exception_rationale"]
for key in ["validation_offer", "end_state_business_model", "fulfillment_contract", "minutes_per_week_human", "human_actions_required"]:
if key in payload:
normalized[key] = payload[key]
return normalized
def _normalize_research(self, payload: dict[str, Any], required: list[str]) -> dict[str, Any]:
@ -945,7 +1054,9 @@ class VentureDiscoveryService:
def _searxng_sources(self, proposal: CompanyProposal, required: list[str], *, depth: str = "light") -> list[dict[str, Any]]:
client = self.search_client or SearxngSearchClient.from_resources()
self._last_search_diagnostics = {"queries": [], "errors": []}
if client is None:
self._last_search_diagnostics["errors"].append("no active searxng resource")
return []
sources = []
limit = 5 if depth == "deep" else 3
@ -959,11 +1070,34 @@ class VentureDiscoveryService:
for category in required:
query = f"{proposal.title} {proposal.problem[:120]} {proposal.target_customer} {query_terms.get(category, category)}"
try:
sources.extend(client.search(query, category=category, limit=limit))
except Exception:
results = self._search_client_results(client, query, category=category, limit=limit)
if not results:
fallback_query = f"{proposal.title} {query_terms.get(category, category)}"
results = self._search_client_results(client, fallback_query, category=category, limit=limit)
if not results:
broad_query = f"{proposal.target_customer} {query_terms.get(category, category)}"
results = self._search_client_results(client, broad_query, category=category, limit=limit)
sources.extend(results)
except Exception as exc:
self._last_search_diagnostics["errors"].append(f"{category}: {exc}")
continue
return sources[:25 if depth == "deep" else 15]
def _search_client_results(self, client: Any, query: str, *, category: str, limit: int) -> list[dict[str, Any]]:
if hasattr(client, "search_payload"):
payload = client.search_payload(query)
raw_results = payload.get("results", []) if isinstance(payload, dict) else []
unresponsive = payload.get("unresponsive_engines", []) if isinstance(payload, dict) else []
self._last_search_diagnostics["queries"].append({"category": category, "query": query, "result_count": len(raw_results), "unresponsive_engines": unresponsive})
return [
{"type": "public_web", "source": "searxng", "url": str(item["url"]), "title": str(item.get("title", "")), "category": category, "summary": str(item.get("content", item.get("snippet", ""))), "fallback_evidence": False}
for item in raw_results[: limit * 4]
if isinstance(item, dict) and item.get("url")
]
results = client.search(query, category=category, limit=limit)
self._last_search_diagnostics["queries"].append({"category": category, "query": query, "result_count": len(results)})
return results
def _page_corpus(self, search_sources: list[dict[str, Any]]) -> list[dict[str, Any]]:
if self.page_fetcher is None:
return []
@ -1024,9 +1158,13 @@ class VentureDiscoveryService:
}
company_score = self._jaccard(company_text, source_text)
category_score = self._jaccard(category_terms.get(category, category), source_text)
company_overlap = self._token_overlap(company_text, source_text)
category_overlap = self._token_overlap(category_terms.get(category, category), source_text)
if category.replace("_", " ") in source_text:
category_score = max(category_score, 0.5)
score = round(company_score * 0.65 + category_score * 0.35, 2)
score = round(company_score * 0.55 + category_score * 0.25 + min(company_overlap, 5) * 0.03 + min(category_overlap, 4) * 0.025, 2)
if company_overlap >= 2 and category_overlap >= 1:
return {"accepted": True, "quality": "strong" if score >= 0.13 or category_overlap >= 2 else "weak", "score": score, "reason": "relevant proposal and category overlap"}
if score < 0.08:
return {"accepted": False, "quality": "weak", "score": score, "reason": "insufficient semantic relevance"}
if score < 0.16:
@ -1106,7 +1244,12 @@ class VentureDiscoveryService:
ai_leverage = self.ai_leverage_score(proposal)
platformization = self.platformization_potential(proposal)
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, "AI Leverage": ai_leverage, "Platformization Potential": platformization, "Probability of Reaching $500": int(max(20, min(80, probability)))}
autonomy = 55
try:
autonomy = int(proposal.autonomous_assessment.autonomous_operability_score)
except ObjectDoesNotExist:
pass
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, "AI Leverage": ai_leverage, "Platformization Potential": platformization, "Autonomous Operability": autonomy, "Probability of Reaching $500": int(max(20, min(80, probability)))}
def ai_leverage_score(self, proposal: CompanyProposal) -> int:
text = " ".join([proposal.title, proposal.description, proposal.problem, proposal.proposed_solution, proposal.business_model, proposal.differentiation]).lower()
@ -1153,6 +1296,10 @@ class VentureDiscoveryService:
def _capability_requirements(self, proposal: CompanyProposal) -> list[dict[str, Any]]:
research = proposal.metadata.get("research", {}) if isinstance(proposal.metadata, dict) else {}
web_status = CapabilityStatus.MISSING if not self.web_research_available else CapabilityStatus.AVAILABLE if not research.get("unverified_categories") else CapabilityStatus.PARTIAL
try:
structural = proposal.autonomous_assessment.structural_blockers
except ObjectDoesNotExist:
structural = []
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": {}},
@ -1160,14 +1307,15 @@ class VentureDiscoveryService:
{"category": "WEB_MARKET_RESEARCH", "status": web_status, "rationale": "Bounded source-linked web research exists only when configured and category coverage is complete.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {"web_research_available": self.web_research_available, **research}},
{"category": "software build", "status": CapabilityStatus.AVAILABLE, "rationale": "Task execution, coding, review, tests, and graph runtime exist.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
{"category": "frontend design", "status": CapabilityStatus.AVAILABLE, "rationale": "Frontend agents and Django UI path exist.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
{"category": "deployment", "status": CapabilityStatus.PARTIAL, "rationale": "Deployment planning exists, but company-specific production deployment workflow is not implemented.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
{"category": "deployment", "status": CapabilityStatus.PARTIAL, "rationale": "Validation can use SUBDOMAIN_DEPLOYMENT under a shared parent domain; dedicated domains are a traction-stage upgrade.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {"deployment_model": "SUBDOMAIN_DEPLOYMENT", "domain_purchase_required_for_validation": False}},
{"category": "outbound sales", "status": CapabilityStatus.MISSING, "rationale": "No compliant outreach/sequence/customer contact system exists and V0 forbids outreach.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
{"category": "CRM", "status": CapabilityStatus.MISSING, "rationale": "No customer pipeline/contact tracking exists.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
{"category": "payments", "status": CapabilityStatus.MISSING, "rationale": "No payment collection system exists.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
{"category": "payments", "status": CapabilityStatus.PARTIAL, "rationale": "Stripe merchant account is available as CONFIGURED_EXTERNAL_PROVIDER, but no real charges or product checkout integration are implemented in this milestone.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {"payment_provider_state": "CONFIGURED_EXTERNAL_PROVIDER", "implementation_state": "IMPLEMENTATION_MISSING", "real_charges_allowed": False}},
{"category": "invoicing", "status": CapabilityStatus.MISSING, "rationale": "No invoicing workflow exists.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
{"category": "customer support", "status": CapabilityStatus.MISSING, "rationale": "No support inbox or customer service workflow exists.", "priority": CapabilityPriority.BEFORE_SCALING, "evidence": {}},
{"category": "company budget management", "status": CapabilityStatus.MISSING, "rationale": "V0 blocks spend but future validation needs tranche/budget controls.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
{"category": "legal/compliance", "status": CapabilityStatus.MISSING, "rationale": "No contracts, terms, privacy, or compliance review workflow exists.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
{"category": "COMPANY_STRUCTURAL_HUMAN_DEPENDENCY", "status": CapabilityStatus.MISSING if structural else CapabilityStatus.AVAILABLE, "rationale": "Structural human dependency is inherent to the company model and should route assisted-only if severe; it is not solved by platform infrastructure.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {"dependency_causes": structural}},
]
def _portfolio_concentration(self, cohort: VentureCohort) -> dict[str, Any]:

View file

@ -107,6 +107,32 @@ class Command(BaseCommand):
"market_evidence": proposal.market_evidence,
"fingerprint": self.fingerprint_payload(proposal),
"decision": self.decision_payload(proposal),
"autonomous_assessment": self.autonomous_payload(proposal),
}
def autonomous_payload(self, proposal) -> dict[str, Any] | None:
try:
assessment = proposal.autonomous_assessment
except ObjectDoesNotExist:
return None
return {
"venture_track": assessment.venture_track,
"gate_result": assessment.gate_result,
"commercial_score": assessment.commercial_score,
"autonomous_operability_score": assessment.autonomous_operability_score,
"founder_dependency": assessment.founder_dependency,
"dependency_causes": assessment.dependency_causes,
"minutes_per_week_human": assessment.minutes_per_week_human,
"human_actions_required": assessment.human_actions_required,
"human_action_categories": assessment.human_action_categories,
"operating_loop": assessment.operating_loop,
"fulfillment_contract": assessment.fulfillment_contract,
"validation_offer": assessment.validation_offer,
"end_state_business_model": assessment.end_state_business_model,
"structural_blockers": assessment.structural_blockers,
"platform_blockers": assessment.platform_blockers,
"component_scores": assessment.component_scores,
"rationale": assessment.rationale,
}
def fingerprint_payload(self, proposal) -> dict[str, Any] | None:
@ -182,12 +208,17 @@ class Command(BaseCommand):
def company_markdown(self, company: dict[str, Any], *, include_sources: bool, max_sources: int) -> list[str]:
decision = company.get("decision") or {}
research = company.get("research") or {}
autonomy = company.get("autonomous_assessment") or {}
lines = [
f"### Rank {company['rank']}: {company['title']}",
"",
"Portfolio score: "
f"`{company['portfolio_score']}`. IC decision: `{decision.get('decision', 'none')}`. "
f"Composite score: `{decision.get('composite_score', '')}`. "
f"Commercial score: `{autonomy.get('commercial_score', '')}`. "
f"Autonomous operability: `{autonomy.get('autonomous_operability_score', '')}`. "
f"Founder dependency: `{autonomy.get('founder_dependency', 'not_assessed')}`. "
f"Gate: `{autonomy.get('gate_result', 'not_assessed')}`. "
f"P($500/30d): `{decision.get('probability_500_within_30_days', '')}%`. "
f"Raw P($500/30d): `{decision.get('raw_probability_500_within_30_days', '')}%`. "
f"Evidence ceiling: `{decision.get('evidence_ceiling', '')}%`. "
@ -214,6 +245,12 @@ class Command(BaseCommand):
"",
f"Major risks: {self.inline_list(company.get('major_risks', []))}",
"",
f"Validation offer: {json.dumps(autonomy.get('validation_offer', {}), default=str)}",
"",
f"End-state business: {json.dumps(autonomy.get('end_state_business_model', {}), default=str)}",
"",
f"Human minutes/week: `{autonomy.get('minutes_per_week_human', '')}`. Structural blockers: {self.inline_list(autonomy.get('structural_blockers', []))}. Platform blockers: {self.inline_list(autonomy.get('platform_blockers', []))}.",
"",
"Research: "
f"Coverage `{research.get('coverage_ratio', 0)}`; sources `{research.get('source_count', 0)}`; "
f"page fetches `{research.get('page_fetch_count', 0)}`; provider `{research.get('provider', 'none')}`; "

View file

@ -0,0 +1,72 @@
from __future__ import annotations
import json
from django.core.management.base import BaseCommand
from agents.venture_discovery import VentureDiscoveryService
from model_router.providers import providers_from_resources
from model_router.router import ModelRouter
from research.searxng import SearxngSearchClient
class Command(BaseCommand):
help = "Run a single-company Venture Research smoke test."
def add_arguments(self, parser):
parser.add_argument("--depth", choices=["light", "deep"], default="deep")
parser.add_argument("--indent", type=int, default=2)
parser.add_argument("--searxng-url", default="")
def handle(self, *args, **options):
providers = providers_from_resources()
search_client = SearxngSearchClient(endpoint_url=str(options["searxng_url"])) if options["searxng_url"] else None
service = VentureDiscoveryService(ModelRouter(providers), web_research_available=True, search_client=search_client)
mandate = service.create_v0_mandate()
proposal = service.generate_single_company(
mandate,
payload={
"title": "AI Website Accessibility Regression Monitor",
"one_line_thesis": "A self-service monitor detects website accessibility regressions and sends automated prioritized reports.",
"description": "Customers submit a URL, Artifex crawls pages, detects accessibility regressions, verifies issues, and delivers a recurring report.",
"problem": "Small web teams need affordable continuous accessibility checks but cannot manually audit every change.",
"target_customer": "Small SaaS and ecommerce teams with public marketing sites.",
"proposed_solution": "Automated crawler, accessibility analysis, regression detection, report generation, and email/dashboard delivery.",
"business_model": "Self-service subscription plus a $49 validation scan.",
"pricing_hypothesis": "$49 validation scan, then $99/month subscription.",
"acquisition_strategy": "SEO/content around accessibility regression monitoring and self-service checkout.",
"validation_plan": "Offer a $49 automated scan with sample output before building a full dashboard.",
"capital_requested": "50",
"time_to_first_dollar_estimate": "1-7 days after approval",
"expected_margin": "85-95%",
"build_complexity": "LOW-MEDIUM",
"market_evidence": [],
"differentiation": "Autonomous recurring monitor with structured verification and self-service onboarding.",
"major_risks": ["Search demand may be low", "False positives may increase support"],
"confidence": 0.64,
},
source="research_smoke",
)
research = service.conduct_market_research(proposal, depth=str(options["depth"]))
proposal.refresh_from_db()
stored = proposal.metadata.get("research", {}) if isinstance(proposal.metadata, dict) else {}
payload = {
"proposal_id": str(proposal.id),
"title": proposal.title,
"search_result_count": stored.get("search_result_count", 0),
"page_fetch_count": stored.get("page_fetch_count", 0),
"accepted_source_count": stored.get("source_count", 0),
"rejected_source_count": stored.get("source_rejection_count", 0),
"accepted_categories": sorted({str(item.get("category", "")) for item in research.get("sources", []) if isinstance(item, dict) and item.get("url")}),
"coverage": stored.get("coverage", {}),
"coverage_ratio": stored.get("coverage_ratio", 0.0),
"provider": stored.get("provider", "none"),
"search_provider": stored.get("search_provider", "none"),
"search_diagnostics": stored.get("search_diagnostics", {}),
"persisted_url_count": len([item for item in proposal.market_evidence if isinstance(item, dict) and item.get("url")]),
"accepted_sources": research.get("sources", [])[:5],
"rejected_sources": research.get("source_rejections", [])[:5],
"explicit_research_failure": stored.get("explicit_research_failure", ""),
}
indent = None if int(options["indent"]) <= 0 else int(options["indent"])
self.stdout.write(json.dumps(payload, indent=indent, default=str))

View file

@ -0,0 +1,65 @@
# Generated by Django 5.2.16 on 2026-08-16 10:24
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ventures', '0003_cohortideationmandate_portfoliothesis_and_more'),
]
operations = [
migrations.AddField(
model_name='portfoliothesis',
name='autonomous_candidate_status',
field=models.CharField(choices=[('NOT_ASSESSED', 'Not Assessed'), ('AUTONOMOUS_CANDIDATE', 'Autonomous Candidate'), ('ASSISTED_CANDIDATE', 'Assisted Candidate'), ('AUTONOMOUS_REJECTED', 'Autonomous Rejected')], default='NOT_ASSESSED', max_length=32),
),
migrations.AddField(
model_name='portfoliothesis',
name='best_autonomous_operability_score',
field=models.FloatField(default=0.0),
),
migrations.AddField(
model_name='portfoliothesis',
name='best_founder_dependency',
field=models.CharField(choices=[('NONE', 'None'), ('LOW', 'Low'), ('MEDIUM', 'Medium'), ('HIGH', 'High'), ('CRITICAL', 'Critical')], default='MEDIUM', max_length=32),
),
migrations.AddField(
model_name='portfoliothesis',
name='venture_track',
field=models.CharField(choices=[('AUTONOMOUS', 'Autonomous'), ('ASSISTED', 'Assisted')], default='AUTONOMOUS', max_length=32),
),
migrations.CreateModel(
name='AutonomousOperabilityAssessment',
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)),
('venture_track', models.CharField(choices=[('AUTONOMOUS', 'Autonomous'), ('ASSISTED', 'Assisted')], default='AUTONOMOUS', max_length=32)),
('gate_result', models.CharField(choices=[('AUTONOMOUS_ELIGIBLE', 'Autonomous Eligible'), ('AUTONOMOUS_BORDERLINE', 'Autonomous Borderline'), ('ASSISTED_ONLY', 'Assisted Only'), ('REJECT_OPERABILITY', 'Reject Operability')], max_length=40)),
('autonomous_operability_score', models.FloatField(default=0.0)),
('commercial_score', models.FloatField(default=0.0)),
('founder_dependency', models.CharField(choices=[('NONE', 'None'), ('LOW', 'Low'), ('MEDIUM', 'Medium'), ('HIGH', 'High'), ('CRITICAL', 'Critical')], max_length=32)),
('dependency_causes', models.JSONField(blank=True, default=list)),
('minutes_per_week_human', models.PositiveIntegerField(default=30)),
('human_actions_required', models.JSONField(blank=True, default=list)),
('human_action_categories', models.JSONField(blank=True, default=list)),
('operating_loop', models.JSONField(blank=True, default=dict)),
('fulfillment_contract', models.JSONField(blank=True, default=dict)),
('validation_offer', models.JSONField(blank=True, default=dict)),
('end_state_business_model', models.JSONField(blank=True, default=dict)),
('structural_blockers', models.JSONField(blank=True, default=list)),
('platform_blockers', models.JSONField(blank=True, default=list)),
('component_scores', models.JSONField(blank=True, default=dict)),
('rationale', models.TextField(blank=True)),
('metadata', models.JSONField(blank=True, default=dict)),
('proposal', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='autonomous_assessment', to='ventures.companyproposal')),
],
options={
'abstract': False,
},
),
]

View file

@ -91,6 +91,33 @@ class OpportunityTerritory(models.TextChoices):
OPEN_CATEGORY = "OPEN_CATEGORY"
class VentureTrack(models.TextChoices):
AUTONOMOUS = "AUTONOMOUS"
ASSISTED = "ASSISTED"
class FounderDependencyLevel(models.TextChoices):
NONE = "NONE"
LOW = "LOW"
MEDIUM = "MEDIUM"
HIGH = "HIGH"
CRITICAL = "CRITICAL"
class AutonomousGateResult(models.TextChoices):
AUTONOMOUS_ELIGIBLE = "AUTONOMOUS_ELIGIBLE"
AUTONOMOUS_BORDERLINE = "AUTONOMOUS_BORDERLINE"
ASSISTED_ONLY = "ASSISTED_ONLY"
REJECT_OPERABILITY = "REJECT_OPERABILITY"
class AutonomousCandidateStatus(models.TextChoices):
NOT_ASSESSED = "NOT_ASSESSED"
AUTONOMOUS_CANDIDATE = "AUTONOMOUS_CANDIDATE"
ASSISTED_CANDIDATE = "ASSISTED_CANDIDATE"
AUTONOMOUS_REJECTED = "AUTONOMOUS_REJECTED"
class CompanyMandate(TimestampedModel):
objective = models.TextField()
constraints = models.JSONField(default=dict, blank=True)
@ -159,6 +186,10 @@ class PortfolioThesis(TimestampedModel):
first_seen_cohort = models.ForeignKey("VentureCohort", on_delete=models.SET_NULL, null=True, blank=True, related_name="first_seen_portfolio_theses")
last_seen_cohort = models.ForeignKey("VentureCohort", on_delete=models.SET_NULL, null=True, blank=True, related_name="last_seen_portfolio_theses")
metadata = models.JSONField(default=dict, blank=True)
venture_track = models.CharField(max_length=32, choices=VentureTrack.choices, default=VentureTrack.AUTONOMOUS)
best_autonomous_operability_score = models.FloatField(default=0.0)
best_founder_dependency = models.CharField(max_length=32, choices=FounderDependencyLevel.choices, default=FounderDependencyLevel.MEDIUM)
autonomous_candidate_status = models.CharField(max_length=32, choices=AutonomousCandidateStatus.choices, default=AutonomousCandidateStatus.NOT_ASSESSED)
class CohortIdeationMandate(TimestampedModel):
@ -208,6 +239,28 @@ class PortfolioSaturationAnalysis(TimestampedModel):
metadata = models.JSONField(default=dict, blank=True)
class AutonomousOperabilityAssessment(TimestampedModel):
proposal = models.OneToOneField(CompanyProposal, on_delete=models.CASCADE, related_name="autonomous_assessment")
venture_track = models.CharField(max_length=32, choices=VentureTrack.choices, default=VentureTrack.AUTONOMOUS)
gate_result = models.CharField(max_length=40, choices=AutonomousGateResult.choices)
autonomous_operability_score = models.FloatField(default=0.0)
commercial_score = models.FloatField(default=0.0)
founder_dependency = models.CharField(max_length=32, choices=FounderDependencyLevel.choices)
dependency_causes = models.JSONField(default=list, blank=True)
minutes_per_week_human = models.PositiveIntegerField(default=30)
human_actions_required = models.JSONField(default=list, blank=True)
human_action_categories = models.JSONField(default=list, blank=True)
operating_loop = models.JSONField(default=dict, blank=True)
fulfillment_contract = models.JSONField(default=dict, blank=True)
validation_offer = models.JSONField(default=dict, blank=True)
end_state_business_model = models.JSONField(default=dict, blank=True)
structural_blockers = models.JSONField(default=list, blank=True)
platform_blockers = models.JSONField(default=list, blank=True)
component_scores = models.JSONField(default=dict, blank=True)
rationale = models.TextField(blank=True)
metadata = models.JSONField(default=dict, blank=True)
class CompanyBoardReview(TimestampedModel):
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="board_reviews")
observations = models.JSONField(default=dict, blank=True)

View file

@ -8,8 +8,8 @@ from graph.spec import ExecutionGraphSpec, GraphEdgeSpec, GraphNodeSpec
def venture_discovery_cohort_graph_v1() -> ExecutionGraphSpec:
nodes = ["prepare_cohort", "portfolio_thesis_review", "create_ideation_mandate", "allocate_search_territories", "generate_independent_proposals", "novelty_gate", "initial_research", "fingerprint_theses", "collision_analysis", "cluster_theses", "run_individual_diligence", "portfolio_compare", "portfolio_ic", "saturation_analysis", "update_thesis_registry", "aggregate_capabilities", "produce_cohort_report", "complete"]
spec = ExecutionGraphSpec(name="venture_discovery_cohort", version=3, 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.4: concurrent replacement ideation, source-quality filtering, finalist deep research, rescore, and hardened completion status."})
nodes = ["prepare_cohort", "portfolio_thesis_review", "create_ideation_mandate", "allocate_search_territories", "generate_independent_proposals", "novelty_gate", "initial_research", "fingerprint_theses", "collision_analysis", "cluster_theses", "run_individual_diligence", "autonomous_operability_review", "portfolio_compare", "portfolio_ic", "saturation_analysis", "update_thesis_registry", "aggregate_capabilities", "produce_cohort_report", "complete"]
spec = ExecutionGraphSpec(name="venture_discovery_cohort", version=4, 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.5: autonomous operability track, research smoke hardening, finalist deep research, and autonomous-only finalists."})
spec.validate()
return spec
@ -105,6 +105,13 @@ class DiligenceNode(CohortNode):
return NodeResult("COMPLETE", "success", {"diligence_count": cohort.members.count()})
class AutonomousOperabilityNode(CohortNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
assessments = self.service.assess_autonomous_operability_for_cohort(self.cohort(context))
eligible = len([item for item in assessments if item.gate_result == "AUTONOMOUS_ELIGIBLE"])
return NodeResult("COMPLETE", "success", {"assessment_count": len(assessments), "autonomous_eligible_count": eligible})
class PortfolioNode(CohortNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
review = self.service.portfolio_ic(self.cohort(context))
@ -144,6 +151,6 @@ class NoopNode(CohortNode):
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), PortfolioThesisReviewNode(service, "venture_cohort_portfolio_thesis_review"), IdeationMandateNode(service, "venture_cohort_create_ideation_mandate"), SearchTerritoryNode(service, "venture_cohort_allocate_search_territories"), GenerateIndependentNode(service, "venture_cohort_generate_independent_proposals"), NoveltyGateNode(service, "venture_cohort_novelty_gate"), ResearchNode(service, "venture_cohort_initial_research"), FingerprintNode(service, "venture_cohort_fingerprint_theses"), CollisionNode(service, "venture_cohort_collision_analysis"), ClusterThesesNode(service, "venture_cohort_cluster_theses"), DiligenceNode(service, "venture_cohort_run_individual_diligence"), NoopNode(service, "venture_cohort_portfolio_compare"), PortfolioNode(service, "venture_cohort_portfolio_ic"), SaturationNode(service, "venture_cohort_saturation_analysis"), RegistryUpdateNode(service, "venture_cohort_update_thesis_registry"), CapabilityNode(service, "venture_cohort_aggregate_capabilities"), ReportNode(service, "venture_cohort_produce_cohort_report")]:
for handler in [PrepareCohortNode(service, "venture_cohort_prepare_cohort", cohort_size=cohort_size, concurrency=concurrency), PortfolioThesisReviewNode(service, "venture_cohort_portfolio_thesis_review"), IdeationMandateNode(service, "venture_cohort_create_ideation_mandate"), SearchTerritoryNode(service, "venture_cohort_allocate_search_territories"), GenerateIndependentNode(service, "venture_cohort_generate_independent_proposals"), NoveltyGateNode(service, "venture_cohort_novelty_gate"), ResearchNode(service, "venture_cohort_initial_research"), FingerprintNode(service, "venture_cohort_fingerprint_theses"), CollisionNode(service, "venture_cohort_collision_analysis"), ClusterThesesNode(service, "venture_cohort_cluster_theses"), DiligenceNode(service, "venture_cohort_run_individual_diligence"), AutonomousOperabilityNode(service, "venture_cohort_autonomous_operability_review"), NoopNode(service, "venture_cohort_portfolio_compare"), PortfolioNode(service, "venture_cohort_portfolio_ic"), SaturationNode(service, "venture_cohort_saturation_analysis"), RegistryUpdateNode(service, "venture_cohort_update_thesis_registry"), CapabilityNode(service, "venture_cohort_aggregate_capabilities"), ReportNode(service, "venture_cohort_produce_cohort_report")]:
registry.register(handler)
return registry

View file

@ -31,11 +31,7 @@ class SearxngSearchClient:
return "UNAVAILABLE"
def search(self, query: str, *, category: str = "general", limit: int = 5) -> list[dict[str, Any]]:
params = urllib.parse.urlencode({"q": query, "format": "json", "categories": "general", "language": "en"})
url = self.endpoint_url.rstrip("/") + "/search?" + params
request = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET")
with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
payload = json.loads(response.read().decode("utf-8"))
payload = self.search_payload(query)
results = []
for item in payload.get("results", [])[:limit]:
if not isinstance(item, dict) or not item.get("url"):
@ -53,6 +49,13 @@ class SearxngSearchClient:
)
return results
def search_payload(self, query: str) -> dict[str, Any]:
params = urllib.parse.urlencode({"q": query, "format": "json", "categories": "web", "language": "en"})
url = self.endpoint_url.rstrip("/") + "/search?" + params
request = urllib.request.Request(url, headers={"Accept": "application/json"}, method="GET")
with urllib.request.urlopen(request, timeout=self.timeout_seconds) as response:
return json.loads(response.read().decode("utf-8"))
@dataclass
class WebPageFetcher:

View file

@ -172,7 +172,7 @@ def test_cohort_size_ranking_top3_capability_aggregation_and_graph_lineage() ->
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 cohort.members.filter(is_top_3=True).count() == len(cohort.portfolio_review.top_3)
assert PortfolioICReview.objects.filter(cohort=cohort).exists()
assert VentureCollision.objects.filter(cohort=cohort).count() == 45
assert VentureCapabilityDemand.objects.filter(cohort=cohort).exists()
@ -187,7 +187,7 @@ def test_cohort_size_ranking_top3_capability_aggregation_and_graph_lineage() ->
assert "ranking_changes_after_deep_research" in cohort.metrics
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 len(report.content["top_3"]) <= 3
assert report.content["total_spend"] == 0
assert CohortIdeationMandate.objects.filter(cohort=cohort).exists()
assert PortfolioThesisCluster.objects.filter(cohort=cohort).count() == 10
@ -332,7 +332,7 @@ def test_run_venture_cohort_management_command_outputs_summary(monkeypatch) -> N
assert summary["concurrency"] == 2
assert summary["fallback_count"] == 0
assert summary["generation_sources"] == ["qwen"]
assert len(summary["top_3"]) == 3
assert len(summary["top_3"]) <= 3
assert cohort.members.count() == 3
@ -354,6 +354,8 @@ def test_export_venture_cohort_management_command_outputs_all_companies(tmp_path
assert "## All Company Details" in markdown
assert markdown.count("### Rank") == 3
assert "Validation condition:" in markdown
assert "Autonomous operability:" in markdown
assert payload["cohort"]["cohort_id"] == cohort.cohort_id
assert len(payload["companies"]) == 3
assert payload["companies"][0]["decision"]["decision"]
assert payload["companies"][0]["autonomous_assessment"]["venture_track"]

View file

@ -59,7 +59,7 @@ class ResearchProvider(ModelProvider):
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],
"sources": [{"url": f"https://example.com/{category}", "title": category.replace("_", " ").title(), "category": category, "summary": f"Source-linked evidence for LaunchLens validation audit serving solo technical founders with pricing, competitors, customer pain, market alternatives, regulatory compliance, tools, services, and subscription cost data 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"]},
}