Compare commits

...

2 commits

Author SHA1 Message Date
Daniel Maddern
dc7e21d77c Add venture discovery cohort V0.2 2026-08-15 21:42:46 +07:00
Daniel Maddern
404e9d4949 Add control plane UI and venture discovery V0 2026-08-15 20:56:56 +07:00
41 changed files with 2786 additions and 14 deletions

593
agents/venture_discovery.py Normal file
View file

@ -0,0 +1,593 @@
from __future__ import annotations
import hashlib
import json
import re
import time
from itertools import combinations
from decimal import Decimal
from typing import Any
from collections import Counter, defaultdict
from django.utils import timezone
from control_plane.events.bus import EventBus
from control_plane.ventures.models import CapabilityPriority, CapabilityStatus, CompanyBoardReview, CompanyCapabilityRequirement, CompanyMandate, CompanyProposal, CompanyProposalStatus, EvidenceTier, ICDecision, ICDecisionType, ICDiligence, ICQuestion, ICResponse, OverlapClassification, PortfolioCapabilityGap, PortfolioICReview, VentureArtifact, VentureCapabilityDemand, VentureCohort, VentureCohortMember, VentureCollision, VentureThesis, VentureThesisFingerprint
from graph.models import GraphRun, GraphRunStatus
from model_router.providers import extract_json_object
from model_router.router import ModelCapability, ModelRequestContract, ModelRouter
PITCH_SECTIONS = ["Company name", "One-line thesis", "Problem", "ICP", "Why now", "Product / service", "Business model", "Pricing", "Route to first customer", "Validation plan", "$50 capital allocation proposal", "Time to first dollar", "Path to $500 net cash", "Competition", "Differentiation", "Build requirements", "Distribution requirements", "Risks", "What would falsify the thesis", "Confidence"]
SCORE_DIMENSIONS = ["Demand Evidence", "Time-to-First-Dollar Attractiveness", "Capital Efficiency", "Validation Affordability", "Gross Margin Potential", "Distribution Feasibility", "Build Simplicity", "Defensibility", "Market Opportunity", "Competitive Position", "Risk Manageability", "Probability of Reaching $500"]
SCORE_DEFINITIONS = {dimension: "100 = highly attractive; 0 = highly unattractive" for dimension in SCORE_DIMENSIONS}
EVIDENCE_CEILINGS = {EvidenceTier.TIER_0_THESIS: 35, EvidenceTier.TIER_1_PUBLIC_EVIDENCE: 55, EvidenceTier.TIER_2_CUSTOMER_SIGNAL: 70, EvidenceTier.TIER_3_WILLINGNESS_TO_PAY: 85, EvidenceTier.TIER_4_PAID_CUSTOMER: 95, EvidenceTier.TIER_5_REPEATABLE_TRACTION: 100}
class VentureDiscoveryService:
def __init__(self, router: ModelRouter | None = None, bus: EventBus | None = None, web_research_available: bool = False, research_model_hint: str = "luna") -> None:
self.router = router
self.bus = bus or EventBus()
self.web_research_available = web_research_available
self.research_model_hint = research_model_hint
def create_v0_mandate(self) -> CompanyMandate:
mandate = CompanyMandate.objects.create(
objective="Design a business that could plausibly turn at most $50 of external validation capital into $500 net new cash.",
constraints={"external_validation_capital_max": 50, "target_net_new_cash": 500, "target_window_days": 30, "no_equity_raise": True, "no_debt": True, "no_illegal_or_deceptive_activity": True, "no_spam": True, "no_fake_traction": True, "no_fabricated_customer_evidence": True, "no_real_spend_in_v0": True, "no_real_customer_outreach_in_v0": True, "existing_artifex_compute_sunk_available": True},
optimization_targets=["time to first dollar", "capital efficiency", "real demand evidence", "high gross margin", "realistic execution", "low external dependency", "ability to validate cheaply"],
metadata={"milestone": "VENTURE_DISCOVERY_V0", "spend_authorized": False, "customer_outreach_authorized": False},
)
self._artifact(None, mandate, "VENTURE_MANDATE", "Venture Discovery V0 Mandate", {"objective": mandate.objective, "constraints": mandate.constraints, "optimization_targets": mandate.optimization_targets}, self._readable_mandate(mandate), "venture_discovery")
return mandate
def generate_single_company(self, mandate: CompanyMandate, *, graph_run=None, ideation_index: int | None = None) -> CompanyProposal:
payload, source = self._company_payload(mandate, ideation_index=ideation_index)
pitch = self._pitch(payload, fallback=source != "sol")
confidence = self._confidence(payload.get("confidence", 0.55))
evidence = self._as_list(payload.get("market_evidence", []))
if not self.web_research_available:
evidence.append({"type": "capability_gap", "source": "internal", "summary": "Public web market research is not configured; demand/competitor evidence is unverified.", "fallback_evidence": True})
confidence = min(confidence, 0.58)
thesis = VentureThesis.objects.create(mandate=mandate, title=str(payload["title"]), thesis=str(payload["one_line_thesis"]), similarity_fingerprint=self._fingerprint(payload), metadata={"source": source, "exactly_one_company_generated": True}, evidence_tier=EvidenceTier.TIER_0_THESIS)
proposal = CompanyProposal.objects.create(
mandate=mandate,
thesis=thesis,
title=str(payload["title"]),
description=str(payload["description"]),
problem=str(payload["problem"]),
target_customer=str(payload["target_customer"]),
proposed_solution=str(payload["proposed_solution"]),
business_model=str(payload["business_model"]),
pricing_hypothesis=str(payload["pricing_hypothesis"]),
acquisition_strategy=str(payload["acquisition_strategy"]),
validation_plan=str(payload["validation_plan"]),
capital_requested=self._money(payload.get("capital_requested", "50")),
time_to_first_dollar_estimate=str(payload["time_to_first_dollar_estimate"]),
expected_margin=str(payload["expected_margin"]),
build_complexity=str(payload["build_complexity"]),
market_evidence=evidence,
differentiation=str(payload["differentiation"]),
major_risks=self._as_list(payload["major_risks"]),
confidence=confidence,
status=CompanyProposalStatus.SUBMITTED,
pitch=pitch,
metadata={"generation_source": source, "fallback_evidence": source != "sol", "web_research_available": self.web_research_available, "real_spend": 0, "real_customer_outreach": False},
evidence_tier=EvidenceTier.TIER_0_THESIS,
)
self._artifact(proposal, mandate, "STANDARDIZED_COMPANY_PITCH", "Standardized Company Pitch", pitch, self.readable_pitch(pitch), "Company Brain/Sol" if source == "sol" else "deterministic_fallback", graph_run=graph_run)
self.bus.publish("VENTURE_COMPANY_PROPOSED", payload={"proposal_id": str(proposal.id), "source": source})
return proposal
def conduct_market_research(self, proposal: CompanyProposal, *, graph_run=None) -> dict[str, Any]:
required = ["competitors", "pricing", "customer_pain", "market_alternatives", "regulatory_platform_risks"]
research = {"coverage": {category: False for category in required}, "sources": [], "findings": {}, "unverified_categories": required, "research_available": False}
if self.web_research_available and self.router is not None:
try:
response = self.router.complete(ModelRequestContract(purpose=ModelCapability.REASONING, model_hint=self.research_model_hint, prompt="Bounded public web market research for exactly one startup pitch. Return JSON with keys: sources (list of {url,title,category,summary}), findings (object keyed by competitors, pricing, customer_pain, market_alternatives, regulatory_platform_risks), and coverage (object with each required key true/false). Do not fabricate URLs. If a category has no source, mark coverage false. Pitch: " + json.dumps(proposal.pitch, default=str)))
parsed = extract_json_object(response.content)
if isinstance(parsed, dict):
research = self._normalize_research(parsed, required)
research["research_available"] = True
research["provider"] = self.research_model_hint
except Exception as exc:
research["failure"] = str(exc)
if not research.get("sources"):
research = self._missing_research(required, research.get("failure", "public web research unavailable or returned no source-linked evidence"))
source_categories = {str(source.get("category", "")) for source in research.get("sources", []) if source.get("url")}
coverage = dict(research.get("coverage", {}))
for category in required:
coverage[category] = bool(coverage.get(category)) and category in source_categories
research["coverage"] = coverage
research["unverified_categories"] = [category for category in required if not coverage.get(category)]
coverage_ratio = (len(required) - len(research["unverified_categories"])) / len(required)
proposal.confidence = round(min(float(proposal.confidence), 0.45 + 0.35 * coverage_ratio), 2)
proposal.market_evidence = [*self._as_list(proposal.market_evidence), *research.get("sources", []), {"type": "research_coverage", "source": "venture_research", "summary": f"Source-linked research coverage: {round(coverage_ratio * 100)}%", "coverage": coverage, "unverified_categories": research["unverified_categories"]}]
proposal.metadata = {**proposal.metadata, "research": {"coverage_ratio": coverage_ratio, "unverified_categories": research["unverified_categories"], "source_count": len(research.get("sources", [])), "provider": research.get("provider", "none")}}
proposal.evidence_tier = EvidenceTier.TIER_1_PUBLIC_EVIDENCE if coverage_ratio > 0 else EvidenceTier.TIER_0_THESIS
if proposal.thesis:
proposal.thesis.evidence_tier = proposal.evidence_tier
proposal.thesis.save(update_fields=["evidence_tier", "updated_at"])
proposal.pitch = {**proposal.pitch, "Research evidence": research}
proposal.save(update_fields=["confidence", "market_evidence", "metadata", "pitch", "evidence_tier", "updated_at"])
self._artifact(proposal, proposal.mandate, "MARKET_RESEARCH", "Bounded Public Market Research", research, self._readable_research(research), "Luna/web research" if research.get("provider") else "research_gap", graph_run=graph_run)
return research
def board_review(self, proposal: CompanyProposal, *, graph_run=None) -> CompanyBoardReview:
observations = {
"CEO": ["Mandate fit is strongest if validation sells a narrow paid audit before product build.", "Keep the first dollar path service-led, not SaaS-led."],
"CTO": ["Build can use existing Artifex analysis, reporting, and frontend capabilities.", "Avoid integrations until willingness-to-pay evidence exists."],
"CFO": ["$50 cap is adequate only for lightweight landing page/listing tests, not paid acquisition learning.", "High margin is plausible because delivery is mostly labor/compute already available."],
"CRO": ["Founder/operator communities are reachable manually, but V0 cannot contact customers.", "Pricing must start as a paid diagnostic to avoid long SaaS evaluation cycles."],
"Independent Director": ["Demand evidence is weak without public research or customer conversations.", "The company must prove urgency before building automation."],
}
weaknesses = ["No customer outreach or paid test has occurred.", "Public web research is unavailable, so market evidence remains partial.", "First customers may require trust and examples before paying."]
revisions = ["Frame offer as a productized validation audit with optional Artifex-assisted build plan.", "Make falsification criteria explicit before spend."]
pitch = dict(proposal.pitch)
pitch["What would falsify the thesis"] = "Fewer than 5 credible target-customer responses or zero willingness-to-pay signals after a compliant validation test."
proposal.pitch = pitch
proposal.metadata = {**proposal.metadata, "board_revised_pitch": True}
proposal.save(update_fields=["pitch", "metadata", "updated_at"])
identity = self.validate_identity_content(proposal, pitch)
review = CompanyBoardReview.objects.create(proposal=proposal, observations=observations, strengths=["Service-led revenue path can precede product build.", "Uses current Artifex planning, engineering, and review capabilities.", "Small validation budget aligns with a narrow paid offer."], weaknesses=weaknesses, key_assumptions=["Target customers feel enough urgency to pay for validation clarity.", "Manual outbound or community posting can generate credible responses without spam.", "Artifex can produce a differentiated audit faster than generic consultants."], required_revisions=revisions, recommendation="PROCEED_TO_IC_WITH_REVISIONS", revised_pitch=pitch, metadata={"roles": list(observations), "company_does_not_grade_itself": True, "identity_validation": identity})
self._artifact(proposal, proposal.mandate, "COMPANY_BOARD_REVIEW", "Company Board Review", {"observations": observations, "strengths": review.strengths, "weaknesses": weaknesses, "required_revisions": revisions, "recommendation": review.recommendation, "revised_pitch": pitch, "identity_validation": identity}, self._readable_board(review), "Company Board", graph_run=graph_run)
return review
def start_ic_diligence(self, proposal: CompanyProposal, *, graph_run=None) -> ICDiligence:
proposal.status = CompanyProposalStatus.UNDER_DILIGENCE
proposal.save(update_fields=["status", "updated_at"])
diligence = ICDiligence.objects.create(proposal=proposal, status="FIRST_PASS", rounds=["Initial Pitch", "Diligence Round 1", "Final Challenge", "Decision"], metadata={"bounded_rounds": True, "self_grading": False})
self._artifact(proposal, proposal.mandate, "IC_FIRST_PASS", "IC First-Pass Review", {"status": diligence.status, "initial_concerns": ["Demand evidence is unverified.", "Distribution assumptions need evidence.", "Need validation gate before any spend."]}, "IC first pass: proceed to evidence-seeking questions; do not score final decision yet.", "Independent IC", graph_run=graph_run)
return diligence
def generate_ic_questions(self, diligence: ICDiligence, *, graph_run=None) -> list[ICQuestion]:
pitch = diligence.proposal.pitch
raw_questions = self._questions_from_pitch(pitch)
questions = [ICQuestion.objects.create(diligence=diligence, question=item["question"], category=item["category"], evidence_required=True) for item in raw_questions[:8]]
self._artifact(diligence.proposal, diligence.proposal.mandate, "IC_QUESTIONS", "IC Diligence Questions", {"questions": [q.question for q in questions]}, "\n".join(f"- {q.question}" for q in questions), "Independent IC", graph_run=graph_run)
return questions
def answer_questions(self, diligence: ICDiligence, *, graph_run=None) -> list[ICResponse]:
responses = []
for question in diligence.questions.all():
answer = self._answer_question(question)
responses.append(ICResponse.objects.create(question=question, answer=answer["answer"], evidence=answer["evidence"], uncertainty=answer["uncertainty"], pitch_changes=answer.get("pitch_changes", {}), metadata={"no_customer_outreach": True, "no_spend": True}))
diligence.status = "COMPANY_RESPONSE"
diligence.save(update_fields=["status", "updated_at"])
identity = self.validate_identity_content(diligence.proposal, {"responses": [r.answer for r in responses]})
self._artifact(diligence.proposal, diligence.proposal.mandate, "IC_RESPONSES", "Company Responses to IC", {"responses": [{"question": r.question.question, "answer": r.answer, "evidence": r.evidence, "uncertainty": r.uncertainty, "pitch_changes": r.pitch_changes} for r in responses], "identity_validation": identity}, self._readable_responses(responses), "Company reasoning roles", graph_run=graph_run)
return responses
def red_team(self, diligence: ICDiligence, *, graph_run=None) -> dict[str, object]:
challenge = {"concerns": ["The offer may be perceived as generic consulting unless anchored to a painful, immediate decision.", "Without public research or customer contact, demand remains a hypothesis.", "Manual distribution could fail if the target audience distrusts AI-generated audits."], "required_final_response": ["Narrow ICP further.", "Specify willingness-to-pay proof.", "Define hard kill criteria."], "recommendation": "continue_to_final_response"}
diligence.red_team_challenge = challenge
diligence.status = "FINAL_CHALLENGE"
diligence.save(update_fields=["red_team_challenge", "status", "updated_at"])
challenge["identity_validation"] = self.validate_identity_content(diligence.proposal, challenge)
self._artifact(diligence.proposal, diligence.proposal.mandate, "IC_RED_TEAM", "IC Red-Team Challenge", challenge, "Red-team concerns:\n" + "\n".join(f"- {c}" for c in challenge["concerns"]), "Independent IC Red Team", graph_run=graph_run)
return challenge
def final_company_response(self, diligence: ICDiligence, *, graph_run=None) -> dict[str, object]:
response = {"narrowed_icp": diligence.proposal.target_customer, "revised_validation_gate": "Before any spend, collect 5 credible target-customer responses or 1 explicit willingness-to-pay signal through compliant non-spam channels.", "kill_criteria": ["No credible responses after 10 targeted, compliant conversations/posts once outreach is approved.", "No willingness-to-pay signal at $49-$99.", "Customers only want free advice, not a paid report."], "pitch_changes": {"ICP": "Preserve canonical proposal ICP.", "Validation plan": "Gate spend behind credible response/willingness-to-pay evidence."}}
response["identity_validation"] = self.validate_identity_content(diligence.proposal, response)
diligence.final_response = response
diligence.status = "FINAL_RESPONSE"
diligence.save(update_fields=["final_response", "status", "updated_at"])
self._artifact(diligence.proposal, diligence.proposal.mandate, "IC_FINAL_RESPONSE", "Final Company Response", response, json.dumps(response, indent=2), "Company reasoning roles", graph_run=graph_run)
return response
def score_and_decide(self, diligence: ICDiligence, *, graph_run=None) -> ICDecision:
scores = self._evidence_scores(diligence.proposal)
composite = round(sum(scores.values()) / len(scores), 1)
calibration = self.calibrate_probability(diligence.proposal, raw_probability=float(scores["Probability of Reaching $500"]))
scores["Probability of Reaching $500"] = int(calibration["evidence_adjusted_probability"])
composite = round(sum(scores.values()) / len(scores), 1)
decision_type = ICDecisionType.REVISE_AND_RESUBMIT if scores["Demand Evidence"] < 35 or scores["Risk Manageability"] < 35 else ICDecisionType.CONDITIONAL_FUND if composite >= 55 else ICDecisionType.REVISE_AND_RESUBMIT
tranche = Decimal("20.00") if decision_type == ICDecisionType.CONDITIONAL_FUND and composite >= 70 else Decimal("10.00") if decision_type == ICDecisionType.CONDITIONAL_FUND else None
decision = ICDecision.objects.create(diligence=diligence, decision=decision_type, component_scores=scores, composite_score=composite, probability_500_within_30_days=scores["Probability of Reaching $500"], initial_tranche=tranche, validation_condition="Obtain 5 credible target-customer responses or 1 explicit willingness-to-pay signal before any build or further spend.", evidence_required=["response transcripts or public thread URLs", "proof of willingness-to-pay signal", "no-spam/no-fabrication compliance note"], recommended_allocation={"initial_tranche": float(tranche or 0), "remaining_reserved": 50 - float(tranche or 0), "no_spend_in_v0": True}, kill_criteria=diligence.final_response.get("kill_criteria", []), next_decision_point="After validation evidence is collected and before any real spend or customer delivery.", metadata={"decision_vocabulary": [item.value for item in ICDecisionType], "no_actual_funding": True, "score_basis": "source_linked_research_and_pitch_attributes", "probability_calibration": calibration}, evidence_tier=diligence.proposal.evidence_tier, raw_probability_500_within_30_days=calibration["raw_probability"], evidence_ceiling=calibration["evidence_ceiling"], probability_explanation=calibration["explanation"], score_definitions=SCORE_DEFINITIONS)
diligence.status = "DECISION"
diligence.save(update_fields=["status", "updated_at"])
diligence.proposal.status = CompanyProposalStatus.FUNDED_RECOMMENDED if decision.decision == ICDecisionType.CONDITIONAL_FUND else CompanyProposalStatus.REVISE
diligence.proposal.save(update_fields=["status", "updated_at"])
self._artifact(diligence.proposal, diligence.proposal.mandate, "IC_FINAL_SCORE", "IC Final Scoring and Decision", {"scores": scores, "score_definitions": SCORE_DEFINITIONS, "decision": decision.decision, "composite_score": composite, "conditional_funding": {"initial_tranche": str(decision.initial_tranche), "condition": decision.validation_condition}, "score_basis": decision.metadata["score_basis"], "probability_calibration": calibration}, self._readable_score(decision), "Independent IC", graph_run=graph_run)
return decision
def capability_analysis(self, proposal: CompanyProposal, *, graph_run=None) -> PortfolioCapabilityGap:
requirements = self._capability_requirements(proposal)
for item in requirements:
CompanyCapabilityRequirement.objects.create(proposal=proposal, **item)
available = [r["category"] for r in requirements if r["status"] == CapabilityStatus.AVAILABLE]
partial = [r["category"] for r in requirements if r["status"] == CapabilityStatus.PARTIAL]
missing = [r["category"] for r in requirements if r["status"] == CapabilityStatus.MISSING]
ranked = [{"category": r["category"], "priority": r["priority"], "rationale": r["rationale"]} for r in requirements if r["status"] == CapabilityStatus.MISSING]
priority_order = {CapabilityPriority.BEFORE_VALIDATION: 0, CapabilityPriority.BEFORE_FIRST_CUSTOMER: 1, CapabilityPriority.BEFORE_SCALING: 2}
ranked.sort(key=lambda item: priority_order[item["priority"]])
report = self._readable_capability_gap(available, partial, missing, ranked)
web_requirement = next((item for item in requirements if item["category"] == "WEB_MARKET_RESEARCH"), None)
gap = PortfolioCapabilityGap.objects.create(proposal=proposal, available=available, partial=partial, missing=missing, ranked_missing=ranked, report=report, metadata={"web_market_research_status": web_requirement["status"] if web_requirement else "MISSING"})
self._artifact(proposal, proposal.mandate, "CAPABILITY_GAP_REPORT", "Capability Gap Report", {"available": available, "partial": partial, "missing": missing, "ranked_missing": ranked}, report, "Venture Discovery Capability Analysis", graph_run=graph_run)
return gap
def produce_investment_memo(self, diligence: ICDiligence, gap: PortfolioCapabilityGap, *, graph_run=None) -> VentureArtifact:
decision = diligence.decision
proposal = diligence.proposal
memo = {"Company": proposal.title, "Thesis": proposal.pitch["One-line thesis"], "Mandate": proposal.mandate.objective, "Requested capital": str(proposal.capital_requested), "Recommended allocation": decision.recommended_allocation, "IC decision": decision.decision, "Key metrics": {"P($500 within 30 days)": decision.probability_500_within_30_days, "raw P($500 within 30 days)": decision.raw_probability_500_within_30_days, "evidence ceiling": decision.evidence_ceiling, "evidence tier": decision.evidence_tier, "estimated time to first dollar": proposal.time_to_first_dollar_estimate, "expected gross margin": proposal.expected_margin, "validation cost": "$10-$20 initial tranche; $50 maximum after approval", "build effort": proposal.build_complexity, "distribution feasibility": decision.component_scores["Distribution Feasibility"]}, "Research evidence": proposal.pitch.get("Research evidence", {}), "Why it may work": ["Revenue path starts with a paid diagnostic, not a full SaaS build.", "Artifex has planning, engineering, frontend, review, graph, and agent-control capabilities already.", "Validation budget can be gated behind evidence."], "Why it may fail": diligence.red_team_challenge.get("concerns", []), "Diligence questions": [q.question for q in diligence.questions.all()], "Company responses": [r.answer for r in ICResponse.objects.filter(question__diligence=diligence)], "Red-team concerns": diligence.red_team_challenge.get("concerns", []), "IC scoring": decision.component_scores, "Score definitions": decision.score_definitions, "Capital recommendation": decision.recommended_allocation, "Validation gates": [decision.validation_condition], "Kill criteria": decision.kill_criteria, "Next decision point": decision.next_decision_point, "Capability gap": {"available": gap.available, "partial": gap.partial, "missing": gap.missing}}
identity = self.validate_identity_content(proposal, memo)
memo["Identity validation"] = identity
return self._artifact(proposal, proposal.mandate, "FINAL_INVESTMENT_MEMO", "Final IC Investment Memo", memo, self._readable_memo(memo), "Independent IC", graph_run=graph_run)
def request_human_approval(self, decision: ICDecision, action: str) -> ICDecision:
if action not in {"approve_for_validation", "reject", "request_more_diligence"}:
raise ValueError("Unsupported venture approval action")
decision.metadata = {**decision.metadata, "human_approval_action": action, "real_spend_still_blocked": True}
decision.save(update_fields=["metadata", "updated_at"])
return decision
def prepare_cohort(self, *, size: int = 10, graph_run=None, concurrency: int = 1) -> VentureCohort:
mandate = self.create_v0_mandate()
cohort_id = f"VDV02-{timezone.now().strftime('%Y%m%d%H%M%S')}-{hashlib.sha1(str(mandate.id).encode()).hexdigest()[:8]}"
return VentureCohort.objects.create(cohort_id=cohort_id, mandate=mandate, cohort_size=size, graph_run=graph_run, concurrency=concurrency, status="PREPARING", graph_versions={"cohort": "venture_discovery_cohort v1", "company": "venture_discovery v1"}, research_policy={"stage_a": "lightweight_all", "deeper_research": "top_5_if_required", "no_spend": True, "no_customer_outreach": True}, scoring_policy={"dimensions": SCORE_DEFINITIONS}, evidence_calibration_policy={tier: ceiling for tier, ceiling in EVIDENCE_CEILINGS.items()}, metadata={"real_spend": 0, "real_customer_outreach": False})
def generate_independent_proposals(self, cohort: VentureCohort) -> list[CompanyProposal]:
proposals = []
for index in range(cohort.cohort_size):
proposal = self.generate_single_company(cohort.mandate, ideation_index=index + 1)
proposal.metadata = {**proposal.metadata, "cohort_id": cohort.cohort_id, "independent_generation_index": index + 1, "prior_ideas_visible": False}
proposal.save(update_fields=["metadata", "updated_at"])
VentureCohortMember.objects.create(cohort=cohort, proposal=proposal, metadata={"generation_index": index + 1})
proposals.append(proposal)
cohort.status = "PROPOSALS_GENERATED"
cohort.save(update_fields=["status", "updated_at"])
return proposals
def research_cohort(self, cohort: VentureCohort) -> None:
started = time.monotonic()
sources = 0
for member in cohort.members.select_related("proposal").order_by("created_at"):
research = self.conduct_market_research(member.proposal)
sources += len(research.get("sources", []))
cohort.metrics = {**cohort.metrics, "research_runtime_seconds": round(time.monotonic() - started, 2), "total_sources": sources, "public_research_queries": cohort.members.count(), "peak_concurrency": cohort.concurrency}
cohort.status = "RESEARCHED"
cohort.save(update_fields=["metrics", "status", "updated_at"])
def fingerprint_cohort(self, cohort: VentureCohort) -> list[VentureThesisFingerprint]:
return [self.fingerprint_proposal(member.proposal) for member in cohort.members.select_related("proposal")]
def analyze_collisions(self, cohort: VentureCohort) -> list[VentureCollision]:
collisions = []
proposals = [member.proposal for member in cohort.members.select_related("proposal")]
for a, b in combinations(proposals, 2):
result = self.classify_overlap(a, b)
collisions.append(VentureCollision.objects.create(cohort=cohort, company_a=a, company_b=b, classification=result["classification"], similarity_score=result["similarity_score"], explanation=result["explanation"], overlapping_dimensions=result["overlapping_dimensions"]))
return collisions
def run_individual_diligence_for_cohort(self, cohort: VentureCohort) -> None:
started = time.monotonic()
for member in cohort.members.select_related("proposal"):
proposal = member.proposal
self.board_review(proposal)
diligence = self.start_ic_diligence(proposal)
self.generate_ic_questions(diligence)
self.answer_questions(diligence)
self.red_team(diligence)
self.final_company_response(diligence)
self.score_and_decide(diligence)
gap = self.capability_analysis(proposal)
self.produce_investment_memo(diligence, gap)
member.child_graph_run = GraphRun.objects.create(execution_graph_version=cohort.graph_run.execution_graph_version if cohort.graph_run else None, status=GraphRunStatus.COMPLETE, metadata={"logical_child_company_run": True, "proposal_id": str(proposal.id), "cohort_id": cohort.cohort_id}) if cohort.graph_run else None
member.save(update_fields=["child_graph_run", "updated_at"])
cohort.metrics = {**cohort.metrics, "individual_diligence_runtime_seconds": round(time.monotonic() - started, 2)}
cohort.status = "INDIVIDUAL_DILIGENCE_COMPLETE"
cohort.save(update_fields=["metrics", "status", "updated_at"])
def portfolio_ic(self, cohort: VentureCohort) -> PortfolioICReview:
rows = []
collision_risk = defaultdict(int)
for collision in cohort.collisions.exclude(classification=OverlapClassification.NONE):
collision_risk[str(collision.company_a_id)] += 1
collision_risk[str(collision.company_b_id)] += 1
for member in cohort.members.select_related("proposal"):
proposal = member.proposal
decision = proposal.ic_diligence.order_by("-created_at").first().decision
capability_burden = proposal.capability_requirements.filter(status=CapabilityStatus.MISSING).count()
score = round(decision.composite_score + decision.probability_500_within_30_days * 0.2 - capability_burden * 1.5 - collision_risk[str(proposal.id)] * 2, 1)
rows.append({"proposal_id": str(proposal.id), "company": proposal.title, "thesis": proposal.pitch.get("One-line thesis", proposal.description), "ic_score": decision.composite_score, "probability": decision.probability_500_within_30_days, "decision": decision.decision, "evidence_tier": decision.evidence_tier, "initial_tranche": str(decision.initial_tranche or "0"), "portfolio_score": score, "capability_burden": capability_burden, "collision_risk": collision_risk[str(proposal.id)]})
rows.sort(key=lambda item: item["portfolio_score"], reverse=True)
for rank, row in enumerate(rows, start=1):
member = cohort.members.get(proposal_id=row["proposal_id"])
member.rank = rank
member.is_top_3 = rank <= 3
member.portfolio_score = row["portfolio_score"]
member.save(update_fields=["rank", "is_top_3", "portfolio_score", "updated_at"])
row["rank"] = rank
concentration = self._portfolio_concentration(cohort)
review, _ = PortfolioICReview.objects.update_or_create(cohort=cohort, defaults={"rankings": rows, "top_3": rows[:3], "concentration": concentration, "metadata": {"no_funding": True}})
cohort.status = "PORTFOLIO_IC_COMPLETE"
cohort.save(update_fields=["status", "updated_at"])
return review
def aggregate_capability_demand(self, cohort: VentureCohort, *, top_3_only: bool = False) -> list[dict[str, Any]]:
members = cohort.members.filter(is_top_3=True) if top_3_only else cohort.members.all()
proposals = [member.proposal for member in members.select_related("proposal")]
by_capability: dict[str, list[CompanyCapabilityRequirement]] = defaultdict(list)
for proposal in proposals:
for req in proposal.capability_requirements.all():
by_capability[req.category].append(req)
stage_order = {CapabilityPriority.BEFORE_VALIDATION: 0, CapabilityPriority.BEFORE_FIRST_CUSTOMER: 1, CapabilityPriority.BEFORE_SCALING: 2}
results = []
for capability, reqs in by_capability.items():
earliest = sorted([req.priority for req in reqs], key=lambda item: stage_order[item])[0]
statuses = Counter(req.status for req in reqs)
top_3_count = sum(1 for req in reqs if req.proposal.cohort_memberships.filter(cohort=cohort, is_top_3=True).exists())
priority_score = len(reqs) * 10 + top_3_count * 8 + (12 if earliest == CapabilityPriority.BEFORE_VALIDATION else 6 if earliest == CapabilityPriority.BEFORE_FIRST_CUSTOMER else 2) + statuses.get(CapabilityStatus.MISSING, 0) * 4
row = {"capability": capability, "count": len(reqs), "percentage": round((len(reqs) / max(1, len(proposals))) * 100, 1), "earliest_stage": earliest, "companies": [req.proposal.title for req in reqs], "status_distribution": dict(statuses), "top_3_count": top_3_count, "priority_score": priority_score}
if not top_3_only:
VentureCapabilityDemand.objects.update_or_create(cohort=cohort, capability=capability, defaults={k: v for k, v in row.items() if k != "capability"})
results.append(row)
results.sort(key=lambda item: item["priority_score"], reverse=True)
if hasattr(cohort, "portfolio_review"):
review = cohort.portfolio_review
if top_3_only:
review.top_3_capability_gaps = results[:10]
else:
review.capability_demand = results
review.recommended_build_priorities = [item["capability"] for item in results if CapabilityStatus.MISSING in item["status_distribution"]][:5]
review.save(update_fields=["capability_demand", "top_3_capability_gaps", "recommended_build_priorities", "updated_at"])
return results
def produce_cohort_report(self, cohort: VentureCohort) -> VentureArtifact:
review = cohort.portfolio_review
content = {"cohort_id": cohort.cohort_id, "mandate": cohort.mandate.objective, "runtime": cohort.metrics, "total_spend": 0, "customer_outreach": "none", "rankings": review.rankings, "top_3": review.top_3, "collisions": self._collision_summary(cohort), "portfolio_concentration": review.concentration, "capability_demand": review.capability_demand, "top_3_capability_gaps": review.top_3_capability_gaps, "recommended_build_priorities": review.recommended_build_priorities}
readable = self._readable_cohort_report(content)
cohort.status = "COMPLETE"
cohort.save(update_fields=["status", "updated_at"])
return VentureArtifact.objects.create(mandate=cohort.mandate, graph_run=cohort.graph_run, artifact_type="VENTURE_DISCOVERY_COHORT_REPORT", name="Venture Discovery Cohort Report", content=content, readable=readable, generated_by="Portfolio IC")
def calibrate_probability(self, proposal: CompanyProposal, *, raw_probability: float) -> dict[str, Any]:
tier = proposal.evidence_tier or EvidenceTier.TIER_0_THESIS
ceiling = float(EVIDENCE_CEILINGS.get(tier, 35))
adjusted = min(float(raw_probability), ceiling)
return {"raw_probability": float(raw_probability), "evidence_adjusted_probability": adjusted, "evidence_ceiling": ceiling, "evidence_tier": tier, "explanation": f"{tier} caps P($500/30d) at {ceiling}%; IC uses {adjusted}%."}
def validate_identity_content(self, proposal: CompanyProposal, content: Any) -> dict[str, Any]:
text = json.dumps(content, default=str).lower()
checks = {
"company_identity": self._token_overlap(proposal.title, text) > 0,
"icp": self._token_overlap(proposal.target_customer, text) >= 1,
"problem": self._token_overlap(proposal.problem, text) >= 1,
"offer": self._token_overlap(proposal.proposed_solution, text) >= 1,
"business_model": self._token_overlap(proposal.business_model, text) >= 1,
}
passed = sum(1 for value in checks.values() if value) >= 3 and checks["company_identity"]
result = {"passed": passed, "checks": checks, "warning": "" if passed else "identity_consistency_warning"}
return result
def fingerprint_proposal(self, proposal: CompanyProposal) -> VentureThesisFingerprint:
existing = getattr(proposal, "fingerprint", None)
data = self._fingerprint_data(proposal)
digest = hashlib.sha256(json.dumps(data, sort_keys=True).encode()).hexdigest()
fields = {**data, "fingerprint_hash": digest, "metadata": {"deterministic": True}}
if existing:
for key, value in fields.items():
setattr(existing, key, value)
existing.save(update_fields=[*fields.keys(), "updated_at"])
return existing
return VentureThesisFingerprint.objects.create(proposal=proposal, **fields)
def classify_overlap(self, a: CompanyProposal, b: CompanyProposal) -> dict[str, Any]:
fa = self.fingerprint_proposal(a)
fb = self.fingerprint_proposal(b)
dimensions = ["industry", "business_model", "primary_distribution_channel", "price_band", "time_to_first_cash_band", "service_software_hybrid", "regulatory_dependency"]
overlap = [dim for dim in dimensions if getattr(fa, dim) and getattr(fa, dim) == getattr(fb, dim)]
text_score = self._jaccard(f"{fa.icp} {fa.problem} {fa.offer}", f"{fb.icp} {fb.problem} {fb.offer}")
score = round((len(overlap) / len(dimensions)) * 0.55 + text_score * 0.45, 2)
if score >= 0.82:
classification = OverlapClassification.DUPLICATE
elif score >= 0.62:
classification = OverlapClassification.NEAR_DUPLICATE
elif getattr(fa, "industry") == getattr(fb, "industry") and getattr(fa, "icp") == getattr(fb, "icp"):
classification = OverlapClassification.COMPETITIVE
elif getattr(fa, "industry") == getattr(fb, "industry") or getattr(fa, "business_model") == getattr(fb, "business_model"):
classification = OverlapClassification.ADJACENT
else:
classification = OverlapClassification.NONE
return {"classification": classification, "similarity_score": score, "overlapping_dimensions": overlap, "explanation": f"Overlap {overlap}; text similarity {text_score:.2f}."}
def _company_payload(self, mandate: CompanyMandate, *, ideation_index: int | None = None) -> tuple[dict[str, Any], str]:
if self.router is not None:
try:
slot = f" Independent cohort slot: {ideation_index}. Do not use or imitate other cohort ideas; no other ideas are visible." if ideation_index else ""
response = self.router.complete(ModelRequestContract(purpose=ModelCapability.PLANNING, model_hint="sol", prompt="Generate exactly ONE startup idea for Venture Discovery V0. Return a single JSON object, not a list. Respect no spend and no outreach in V0. Include title, one_line_thesis, description, problem, target_customer, proposed_solution, business_model, pricing_hypothesis, acquisition_strategy, validation_plan, capital_requested, time_to_first_dollar_estimate, expected_margin, build_complexity, market_evidence, differentiation, major_risks, confidence." + slot + " Mandate: " + json.dumps({"objective": mandate.objective, "constraints": mandate.constraints, "optimization_targets": mandate.optimization_targets})))
parsed = extract_json_object(response.content)
if isinstance(parsed, dict) and parsed.get("title"):
return self._normalize_payload(parsed), "sol"
except Exception:
pass
payload = self._fallback_company_payload()
payload["market_evidence"] = [{"type": "fallback_hypothesis", "source": "deterministic_fallback", "summary": "No Sol/web research was used; this is an internal hypothesis for test/resilience only.", "fallback_evidence": True}]
payload["confidence"] = 0.52
return payload, "deterministic_fallback"
def _fallback_company_payload(self) -> dict[str, Any]:
return {"title": "LaunchLens", "one_line_thesis": "A productized validation audit helps solo builders decide whether an AI-enabled microbusiness is worth pursuing before they spend weeks building.", "description": "LaunchLens sells a concise validation and launch-readiness report for one microbusiness idea.", "problem": "Solo technical founders often overbuild AI products before proving willingness to pay.", "target_customer": "Solo technical founders and small service operators considering an AI-assisted microbusiness.", "proposed_solution": "A fixed-scope paid audit that evaluates ICP, first-dollar route, validation gates, build plan, risks, and Artifex capability gaps.", "business_model": "Productized service first, with optional later software tooling if demand is proven.", "pricing_hypothesis": "$49-$99 per audit, with a higher-touch $250 implementation planning upsell after validation.", "acquisition_strategy": "Compliant founder-community posts, personal network asks, and content showing anonymized example audits after outreach is approved.", "validation_plan": "Before any build, seek 5 credible target-customer responses or 1 willingness-to-pay signal through compliant channels once approved.", "capital_requested": "50", "time_to_first_dollar_estimate": "3-10 days after outreach is approved", "expected_margin": "70-85% gross margin after manual delivery time", "build_complexity": "LOW", "market_evidence": [], "differentiation": "Combines venture IC-style diligence with Artifex's software/agent execution awareness and explicit capability-gap reporting.", "major_risks": ["Demand may be consulting-like and hard to differentiate.", "Manual distribution may not produce urgent buyers.", "No V0 customer evidence exists yet."], "confidence": 0.52}
def _normalize_payload(self, payload: dict[str, Any]) -> dict[str, Any]:
fallback = self._fallback_company_payload()
normalized = {key: payload.get(key, value) for key, value in fallback.items()}
normalized["market_evidence"] = self._as_list(normalized.get("market_evidence"))
normalized["major_risks"] = self._as_list(normalized.get("major_risks"))
return normalized
def _normalize_research(self, payload: dict[str, Any], required: list[str]) -> dict[str, Any]:
sources = []
for source in self._as_list(payload.get("sources"))[:12]:
if isinstance(source, dict) and source.get("url"):
sources.append({"type": "public_web", "url": str(source["url"]), "title": str(source.get("title", "")), "category": str(source.get("category", "")), "summary": str(source.get("summary", "")), "fallback_evidence": False})
coverage = {category: bool(dict(payload.get("coverage", {})).get(category)) for category in required}
return {"coverage": coverage, "sources": sources, "findings": dict(payload.get("findings", {})), "unverified_categories": []}
def _missing_research(self, required: list[str], reason: str) -> dict[str, Any]:
return {"coverage": {category: False for category in required}, "sources": [], "findings": {}, "unverified_categories": required, "research_available": False, "failure": reason}
def _as_list(self, value: Any) -> list[Any]:
if isinstance(value, list):
return value
if value in (None, ""):
return []
return [value]
def _money(self, value: Any) -> Decimal:
match = re.search(r"\d+(?:\.\d+)?", str(value))
if match is None:
return Decimal("50")
return min(Decimal(match.group(0)), Decimal("50"))
def _token_overlap(self, source: str, target_text: str) -> int:
tokens = {token for token in re.findall(r"[a-z0-9]{4,}", source.lower()) if token not in {"with", "that", "from", "this", "service", "business", "model", "customer", "customers"}}
return len([token for token in tokens if token in target_text])
def _jaccard(self, a: str, b: str) -> float:
left = {token for token in re.findall(r"[a-z0-9]{4,}", a.lower())}
right = {token for token in re.findall(r"[a-z0-9]{4,}", b.lower())}
if not left or not right:
return 0.0
return len(left & right) / len(left | right)
def _fingerprint_data(self, proposal: CompanyProposal) -> dict[str, Any]:
text = " ".join([proposal.title, proposal.description, proposal.problem, proposal.target_customer, proposal.proposed_solution, proposal.business_model, proposal.acquisition_strategy]).lower()
industry = "shopify/ecommerce" if "shopify" in text or "ecommerce" in text else "developer tools" if "developer" in text or "api" in text else "b2b services" if "b2b" in text else "general business"
model = "productized service" if "service" in proposal.business_model.lower() else "saas" if "saas" in proposal.business_model.lower() else "hybrid"
channel = "outbound/community" if any(word in text for word in ["outbound", "community", "posts", "network"]) else "marketplace" if "marketplace" in text or "fiverr" in text else "content/seo" if "seo" in text or "content" in text else "direct"
price = self._money(proposal.pricing_hypothesis)
price_band = "under_100" if price < 100 else "100_500" if price <= 500 else "over_500"
first_cash = "under_7_days" if any(token in proposal.time_to_first_dollar_estimate.lower() for token in ["3", "7", "week"]) else "under_30_days"
regulatory = "high" if any(word in text for word in ["legal", "compliance", "regulatory", "permit", "policy"]) else "medium" if "platform" in text else "low"
return {"industry": industry, "icp": proposal.target_customer[:500], "problem": proposal.problem[:500], "offer": proposal.proposed_solution[:500], "business_model": model, "primary_distribution_channel": channel, "price_band": price_band, "time_to_first_cash_band": first_cash, "required_capability_set": [item.category for item in proposal.capability_requirements.all()] or ["outbound sales", "CRM", "payments"], "geography_dependency": "local" if any(word in text for word in ["local", "city", "metro", "permit"]) else "none", "regulatory_dependency": regulatory, "online_offline": "online" if any(word in text for word in ["shopify", "saas", "api", "online", "web"]) else "mixed", "service_software_hybrid": model}
def _confidence(self, value: Any) -> float:
labels = {"low": 0.35, "medium": 0.55, "moderate": 0.55, "high": 0.75}
lowered = str(value).strip().lower()
if lowered in labels:
return labels[lowered]
match = re.search(r"\d+(?:\.\d+)?", lowered)
if match is None:
return 0.55
number = float(match.group(0))
return min(1.0, number / 100 if number > 1 else number)
def _evidence_scores(self, proposal: CompanyProposal) -> dict[str, int]:
research = proposal.metadata.get("research", {}) if isinstance(proposal.metadata, dict) else {}
coverage_ratio = float(research.get("coverage_ratio", 0.0) or 0.0)
unverified = set(research.get("unverified_categories", []))
evidence_count = len([item for item in self._as_list(proposal.market_evidence) if isinstance(item, dict) and item.get("url")])
low_build = str(proposal.build_complexity).upper() in {"LOW", "LOW-MEDIUM"}
service_model = "service" in proposal.business_model.lower()
margin_numbers = [int(value) for value in re.findall(r"\d+", proposal.expected_margin)]
margin = max(margin_numbers) if margin_numbers else 50
demand = min(85, 25 + int(coverage_ratio * 40) + min(evidence_count * 4, 20))
pricing = 70 if "pricing" not in unverified else 45
pain = 72 if "customer_pain" not in unverified else 35
competition = 68 if "competitors" not in unverified and "market_alternatives" not in unverified else 38
risk = 72 if "regulatory_platform_risks" not in unverified else 34
distribution = 60 if service_model else 42
build = 78 if low_build else 42
gross_margin = max(35, min(85, margin))
capital = 82 if proposal.capital_requested <= Decimal("50") else 30
validation = 78 if "5 credible" in proposal.validation_plan or "willingness" in proposal.validation_plan.lower() else 42
market_size = 62 if coverage_ratio >= 0.6 else 44
defensibility = 42 + (10 if service_model else 0) + (8 if coverage_ratio >= 0.8 else 0)
probability = round((demand * 0.22 + pricing * 0.12 + pain * 0.16 + distribution * 0.14 + build * 0.1 + capital * 0.1 + risk * 0.16), 0)
return {"Demand Evidence": demand, "Time-to-First-Dollar Attractiveness": 76 if service_model else 50, "Capital Efficiency": capital, "Validation Affordability": validation, "Gross Margin Potential": gross_margin, "Distribution Feasibility": distribution, "Build Simplicity": build, "Defensibility": min(75, defensibility), "Market Opportunity": market_size, "Competitive Position": competition, "Risk Manageability": risk, "Probability of Reaching $500": int(max(20, min(80, probability)))}
def _pitch(self, payload: dict[str, Any], *, fallback: bool) -> dict[str, Any]:
pitch = {"Company name": payload["title"], "One-line thesis": payload["one_line_thesis"], "Problem": payload["problem"], "ICP": payload["target_customer"], "Why now": "AI tooling lowers build cost, increasing the risk that founders overbuild before validating demand.", "Product / service": payload["proposed_solution"], "Business model": payload["business_model"], "Pricing": payload["pricing_hypothesis"], "Route to first customer": payload["acquisition_strategy"], "Validation plan": payload["validation_plan"], "$50 capital allocation proposal": {"initial": "$10 only after approval", "reserved": "$40 held until evidence gate", "v0_spend": "$0"}, "Time to first dollar": payload["time_to_first_dollar_estimate"], "Path to $500 net cash": "Sell 6-10 fixed-scope audits at $49-$99 while keeping delivery manual and using sunk Artifex compute.", "Competition": "Generic startup consultants, founder communities, AI business idea tools, and DIY validation templates. Public competitor research is unverified unless web research is configured.", "Differentiation": payload["differentiation"], "Build requirements": ["report template", "intake form", "manual analysis workflow", "optional landing page after validation approval"], "Distribution requirements": ["compliant outreach plan", "community/content channels", "CRM-lite tracking before first customers"], "Risks": payload["major_risks"], "What would falsify the thesis": "No willingness-to-pay signal at $49-$99 or fewer than 5 credible target-customer responses after approved compliant validation.", "Confidence": payload["confidence"], "Evidence caveat": "Deterministic fallback evidence only; not equivalent to Sol or public web research." if fallback else "Generated by Sol; public web research still only included if sources are present."}
return pitch
def _questions_from_pitch(self, pitch: dict[str, Any]) -> list[dict[str, str]]:
return [{"category": "demand", "question": f"What evidence supports demand for {pitch['Company name']} among the stated ICP?"}, {"category": "urgency", "question": "Why will this customer pay now instead of using free templates or advice?"}, {"category": "distribution", "question": "How do you reach the first 10 customers without spam or fake traction?"}, {"category": "validation", "question": "Can this be validated before building the full product?"}, {"category": "business_model", "question": "Why a productized service first instead of SaaS?"}, {"category": "falsification", "question": "What would falsify the thesis within the $50 and 30-day mandate?"}, {"category": "economics", "question": "What happens if acquisition cost or manual delivery time is 3x the estimate?"}, {"category": "competition", "question": "What is the main competitive threat and why is this worth funding over selling an existing Artifex capability?"}]
def _answer_question(self, question: ICQuestion) -> dict[str, Any]:
evidence = [{"source": "internal_reasoning", "summary": "No customer outreach, spend, or fabricated evidence used.", "fallback_evidence": True}]
if question.category in {"demand", "competition"} and not self.web_research_available:
evidence.append({"source": "capability_gap", "summary": "Public web research unavailable; demand/competition claims remain uncertain.", "fallback_evidence": True})
answers = {"demand": "Demand is not proven. The strongest V0 claim is that the problem is plausible and cheap to test, not that demand exists.", "urgency": "The buyer pays only if the report saves them build time or prevents wasted spend; urgency is weakest before a concrete launch decision.", "distribution": "After approval, use targeted compliant posts/conversations and track responses manually; V0 performs no outreach.", "validation": "Yes. The paid diagnostic can be validated with responses and willingness-to-pay before software build.", "business_model": "Service first reduces build risk and can reach first cash faster than SaaS; software should follow only if repeated demand appears.", "falsification": "Failure to collect credible responses or willingness-to-pay within the mandate falsifies near-term viability.", "economics": "If acquisition or delivery is 3x harder, the company should stop or raise price before building tooling.", "competition": "Main threat is generic consulting/free templates. The reason to fund this over selling raw Artifex capability is packaging a buyer-specific outcome."}
return {"answer": answers.get(question.category, "The assumption remains uncertain and must be tested before spend."), "evidence": evidence, "uncertainty": "High until public research and customer evidence are available.", "pitch_changes": {"confidence_adjustment": "reduced/held due missing external evidence"} if question.category in {"demand", "competition"} else {}}
def _capability_requirements(self, proposal: CompanyProposal) -> list[dict[str, Any]]:
research = proposal.metadata.get("research", {}) if isinstance(proposal.metadata, dict) else {}
web_status = CapabilityStatus.MISSING if not self.web_research_available else CapabilityStatus.AVAILABLE if not research.get("unverified_categories") else CapabilityStatus.PARTIAL
return [
{"category": "Company Brain", "status": CapabilityStatus.PARTIAL, "rationale": "Venture reasoning exists in V0 but is not a persistent operating brain.", "priority": CapabilityPriority.BEFORE_SCALING, "evidence": {}},
{"category": "Board", "status": CapabilityStatus.AVAILABLE, "rationale": "Structured CEO/CTO/CFO/CRO/Independent Director review exists for V0.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
{"category": "IC", "status": CapabilityStatus.AVAILABLE, "rationale": "Bounded IC diligence, questions, scoring, and decision vocabulary exist.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
{"category": "WEB_MARKET_RESEARCH", "status": web_status, "rationale": "Bounded source-linked web research exists only when configured and category coverage is complete.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {"web_research_available": self.web_research_available, **research}},
{"category": "software build", "status": CapabilityStatus.AVAILABLE, "rationale": "Task execution, coding, review, tests, and graph runtime exist.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
{"category": "frontend design", "status": CapabilityStatus.AVAILABLE, "rationale": "Frontend agents and Django UI path exist.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
{"category": "deployment", "status": CapabilityStatus.PARTIAL, "rationale": "Deployment planning exists, but company-specific production deployment workflow is not implemented.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
{"category": "outbound sales", "status": CapabilityStatus.MISSING, "rationale": "No compliant outreach/sequence/customer contact system exists and V0 forbids outreach.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
{"category": "CRM", "status": CapabilityStatus.MISSING, "rationale": "No customer pipeline/contact tracking exists.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
{"category": "payments", "status": CapabilityStatus.MISSING, "rationale": "No payment collection system exists.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
{"category": "invoicing", "status": CapabilityStatus.MISSING, "rationale": "No invoicing workflow exists.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
{"category": "customer support", "status": CapabilityStatus.MISSING, "rationale": "No support inbox or customer service workflow exists.", "priority": CapabilityPriority.BEFORE_SCALING, "evidence": {}},
{"category": "company budget management", "status": CapabilityStatus.MISSING, "rationale": "V0 blocks spend but future validation needs tranche/budget controls.", "priority": CapabilityPriority.BEFORE_VALIDATION, "evidence": {}},
{"category": "legal/compliance", "status": CapabilityStatus.MISSING, "rationale": "No contracts, terms, privacy, or compliance review workflow exists.", "priority": CapabilityPriority.BEFORE_FIRST_CUSTOMER, "evidence": {}},
]
def _portfolio_concentration(self, cohort: VentureCohort) -> dict[str, Any]:
fingerprints = [self.fingerprint_proposal(member.proposal) for member in cohort.members.select_related("proposal")]
data = {
"industry_distribution": dict(Counter(fp.industry for fp in fingerprints)),
"business_model_distribution": dict(Counter(fp.business_model for fp in fingerprints)),
"primary_channel_distribution": dict(Counter(fp.primary_distribution_channel for fp in fingerprints)),
"icp_distribution": dict(Counter(fp.icp[:80] for fp in fingerprints)),
"capability_dependency_distribution": dict(Counter(cap for fp in fingerprints for cap in fp.required_capability_set)),
}
flags = []
for key, counts in data.items():
if counts and max(counts.values()) >= max(4, int(cohort.cohort_size * 0.6)):
flags.append({"type": "PORTFOLIO_CONCENTRATION", "dimension": key, "value": max(counts, key=counts.get), "count": max(counts.values())})
data["flags"] = flags
return data
def _collision_summary(self, cohort: VentureCohort) -> dict[str, int]:
counts = Counter(cohort.collisions.values_list("classification", flat=True))
return {choice: counts.get(choice, 0) for choice in OverlapClassification.values}
def _readable_cohort_report(self, content: dict[str, Any]) -> str:
ranking = "\n".join(f"{row['rank']}. {row['company']} - score {row['ic_score']}, P($500) {row['probability']}%, {row['decision']}" for row in content["rankings"])
top = "\n".join(f"- {row['company']}: {row['thesis']}" for row in content["top_3"])
demand = "\n".join(f"- {row['capability']}: {row['count']} companies, earliest {row['earliest_stage']}" for row in content["capability_demand"][:15])
return f"# Venture Discovery Cohort Report\n\nCohort: {content['cohort_id']}\nMandate: {content['mandate']}\nTotal spend: $0\nCustomer outreach: none\n\n## Ranking\n{ranking}\n\n## Top 3 Finalists\n{top}\n\n## Collisions\n{json.dumps(content['collisions'], indent=2)}\n\n## Portfolio Concentration\n{json.dumps(content['portfolio_concentration'], indent=2)}\n\n## Capability Demand\n{demand}\n\n## Recommended Artifex Build Priorities\n" + "\n".join(f"- {item}" for item in content["recommended_build_priorities"])
def _artifact(self, proposal, mandate, artifact_type: str, name: str, content: dict[str, Any], readable: str, generated_by: str, *, graph_run=None) -> VentureArtifact:
return VentureArtifact.objects.create(proposal=proposal, mandate=mandate, graph_run=graph_run, artifact_type=artifact_type, name=name, content=content, readable=readable, generated_by=generated_by)
def readable_pitch(self, pitch: dict[str, Any]) -> str:
return "\n\n".join(f"{section}\n{pitch.get(section, '')}" for section in PITCH_SECTIONS)
def _readable_mandate(self, mandate: CompanyMandate) -> str:
return f"Objective\n{mandate.objective}\n\nConstraints\n{json.dumps(mandate.constraints, indent=2)}\n\nOptimization targets\n" + "\n".join(f"- {item}" for item in mandate.optimization_targets)
def _readable_board(self, review: CompanyBoardReview) -> str:
return f"Recommendation: {review.recommendation}\n\nStrengths\n" + "\n".join(f"- {item}" for item in review.strengths) + "\n\nWeaknesses\n" + "\n".join(f"- {item}" for item in review.weaknesses)
def _readable_responses(self, responses: list[ICResponse]) -> str:
return "\n\n".join(f"Q: {response.question.question}\nA: {response.answer}\nUncertainty: {response.uncertainty}" for response in responses)
def _readable_score(self, decision: ICDecision) -> str:
scores = "\n".join(f"- {key}: {value}/100" for key, value in decision.component_scores.items())
return f"Decision: {decision.decision}\nComposite: {decision.composite_score}/100\nP($500 within 30 days): {decision.probability_500_within_30_days}%\n\nScores\n{scores}\n\nCondition\n{decision.validation_condition}"
def _readable_research(self, research: dict[str, Any]) -> str:
sources = "\n".join(f"- {source.get('category')}: {source.get('title')} {source.get('url')}" for source in research.get("sources", []))
missing = ", ".join(research.get("unverified_categories", [])) or "none"
return f"Coverage\n{json.dumps(research.get('coverage', {}), indent=2)}\n\nSources\n{sources}\n\nUnverified categories\n{missing}"
def _readable_capability_gap(self, available: list[str], partial: list[str], missing: list[str], ranked: list[dict[str, str]]) -> str:
return "AVAILABLE\n" + "\n".join(f"- {item}" for item in available) + "\n\nPARTIAL\n" + "\n".join(f"- {item}" for item in partial) + "\n\nMISSING\n" + "\n".join(f"- {item}" for item in missing) + "\n\nNEXT ARTIFEX CAPABILITIES REQUIRED\n" + "\n".join(f"- {item['category']} ({item['priority']})" for item in ranked)
def _readable_memo(self, memo: dict[str, Any]) -> str:
return json.dumps(memo, indent=2)
def _fingerprint(self, payload: dict[str, Any]) -> str:
return hashlib.sha256((str(payload["title"]).lower() + str(payload["target_customer"]).lower() + str(payload["business_model"]).lower()).encode()).hexdigest()

View file

@ -21,6 +21,7 @@ INSTALLED_APPS = [
"control_plane.events",
"control_plane.agents",
"control_plane.resources",
"control_plane.ventures",
"control_plane.secrets",
"control_plane.knowledge",
"control_plane.verification",

View file

@ -3,3 +3,4 @@ from __future__ import annotations
from artifex.settings import * # noqa: F403
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}}
ALLOWED_HOSTS = ["testserver", "localhost", "127.0.0.1"]

View file

@ -3,10 +3,37 @@ from __future__ import annotations
from django.contrib import admin
from django.urls import path
from control_plane.projects.views import agent_control_room, dashboard
from control_plane.projects import views
urlpatterns = [
path("", dashboard, name="dashboard"),
path("agents/", agent_control_room, name="agent_control_room"),
path("", views.dashboard, name="dashboard"),
path("projects/", views.projects, name="projects"),
path("projects/<uuid:project_id>/", views.project_workspace, name="project_workspace"),
path("projects/<uuid:project_id>/brain/", views.project_brain, name="project_brain"),
path("projects/<uuid:project_id>/archaeologist/", views.project_archaeologist, name="project_archaeologist"),
path("projects/<uuid:project_id>/dag.json", views.project_dag_json, name="project_dag_json"),
path("projects/<uuid:project_id>/explore/run/", views.run_explore, name="run_explore"),
path("projects/<uuid:project_id>/explore/", views.explore, name="project_explore"),
path("projects/<uuid:project_id>/roadmap/", views.roadmap, name="project_roadmap"),
path("projects/<uuid:project_id>/scenarios/", views.scenarios, name="project_scenarios"),
path("projects/<uuid:project_id>/steward/", views.project_steward, name="project_steward"),
path("tasks/<uuid:task_id>/", views.task_detail, name="task_detail"),
path("graph-runs/<int:graph_run_id>/", views.graph_run_detail, name="graph_run_detail"),
path("graph-runs/<int:graph_run_id>/status.json", views.graph_run_json, name="graph_run_json"),
path("steward/", views.steward, name="steward"),
path("explore/", views.explore, name="explore"),
path("opportunities/<uuid:opportunity_id>/action/", views.opportunity_action, name="opportunity_action"),
path("roadmap/", views.roadmap, name="roadmap"),
path("roadmap/<uuid:item_id>/action/", views.roadmap_action, name="roadmap_action"),
path("scenario-lab/", views.scenarios, name="scenarios"),
path("scenario-findings/<uuid:finding_id>/action/", views.scenario_finding_action, name="scenario_finding_action"),
path("progeny/", views.progeny, name="progeny"),
path("agents/", views.agent_control_room, name="agent_control_room"),
path("agents/<uuid:version_id>/", views.agent_detail, name="agent_detail"),
path("agents/<uuid:version_id>/performance.json", views.agent_performance_json, name="agent_performance_json"),
path("resources/", views.resources, name="resources"),
path("approvals/", views.approvals, name="approvals"),
path("approvals/<int:approval_id>/action/", views.approval_action, name="approval_action"),
path("activity/", views.activity, name="activity"),
path("admin/", admin.site.urls),
]

View file

@ -0,0 +1,121 @@
from __future__ import annotations
from collections import Counter
from django.db.models import Count, Q
from agents.control_room import AgentControlRoomService
from agents.lifecycle import LifecycleInspectionService
from agents.roadmap import RoadmapService
from agents.scenario_lab import ScenarioLabService
from control_plane.agents.models import AgentRun, AgentVersion, ProgenyExperiment, ProgenyInvestigation, ProgenySignal, PromotionStatus
from control_plane.events.models import Event
from control_plane.projects.models import ExplorationOpportunity, Project, RoadmapHorizon, ScenarioFinding, ScenarioRun, ScenarioSuite, StewardFinding, StewardRun, Task, TaskStatus
from control_plane.resources.models import ModelRequest, Resource
from graph.models import ExecutionGraphVersionStatus, GraphApproval, GraphApprovalStatus, GraphRun, GraphRunStatus
class ControlPlaneUIService:
def dashboard(self) -> dict[str, object]:
projects = Project.objects.all()
graph_runs = GraphRun.objects.select_related("execution_graph_version__graph", "project").order_by("-created_at")
steward_findings = StewardFinding.objects.all()
progeny_signals = ProgenySignal.objects.all()
control_room = AgentControlRoomService()
agent_health = [control_room.get_agent_health(version.id)["status"] for version in AgentVersion.objects.filter(promotion_status=PromotionStatus.CHAMPION)]
return {
"project_summary": {"total": projects.count(), "active": projects.exclude(status__in=["FINISHED", "FAILED"]).count(), "blocked_failed": projects.filter(status__in=["BLOCKED", "FAILED"]).count(), "recent_completed": projects.filter(status="FINISHED").order_by("-updated_at")[:5]},
"execution_summary": {"active": graph_runs.filter(status__in=[GraphRunStatus.PENDING, GraphRunStatus.RUNNING, GraphRunStatus.PAUSED]).count(), "failed": graph_runs.filter(status=GraphRunStatus.FAILED).count(), "recent_success": graph_runs.filter(status=GraphRunStatus.COMPLETE)[:5], "champion_task_graph": self._champion_graph("task_execution")},
"steward_summary": {"enrolled_projects": Project.objects.filter(steward_enrollments__status="ACTIVE").distinct().count(), "open_findings": steward_findings.exclude(status__in=["RESOLVED", "DISMISSED"]).count(), "high_findings": steward_findings.filter(severity__in=["HIGH", "CRITICAL"]).exclude(status__in=["RESOLVED", "DISMISSED"]).count()},
"progeny_summary": {"unresolved_signals": progeny_signals.filter(status="OPEN").count(), "open_investigations": ProgenyInvestigation.objects.filter(status="OPEN").count(), "challengers": AgentVersion.objects.filter(promotion_status=PromotionStatus.CHALLENGER).count(), "pending_experiment_decisions": ProgenyExperiment.objects.filter(status__in=["DRAFT", "RUNNING"]).count()},
"agent_summary": {"agent_count": AgentVersion.objects.values("agent").distinct().count(), "watch_degraded": sum(1 for status in agent_health if status in ["WATCH", "DEGRADED"]), "active_runs": AgentRun.objects.filter(status__in=["QUEUED", "RUNNING"]).count()},
"approval_count": GraphApproval.objects.filter(status=GraphApprovalStatus.PENDING).count(),
"recent": {"events": Event.objects.order_by("-created_at")[:10], "tasks": Task.objects.order_by("-updated_at")[:10], "graph_runs": graph_runs[:10], "findings": steward_findings.order_by("-updated_at")[:10], "investigations": ProgenyInvestigation.objects.order_by("-updated_at")[:10]},
}
def project_list(self) -> list[dict[str, object]]:
rows = []
for project in Project.objects.order_by("name"):
rows.append({"project": project, "current_milestone": project.milestones.order_by("order", "created_at").last(), "task_total": project.tasks.count(), "task_complete": project.tasks.filter(status=TaskStatus.COMPLETE).count(), "steward_state": project.steward_enrollments.order_by("-created_at").first(), "open_findings": project.steward_findings.exclude(status__in=["RESOLVED", "DISMISSED"]).count(), "latest_graph_run": project.graph_runs.order_by("-created_at").first(), "warnings": self.project_warnings(project)})
return rows
def project_workspace(self, project: Project) -> dict[str, object]:
lifecycle = LifecycleInspectionService().project_lifecycle_view(project)
return {"project": project, "plan": project.plans.order_by("-version").first(), "milestones": project.milestones.prefetch_related("features__tasks", "tasks").order_by("order", "created_at"), "tasks": project.tasks.select_related("milestone", "feature").order_by("milestone__order", "priority", "created_at"), "graph_runs": project.graph_runs.select_related("execution_graph_version__graph", "task").order_by("-created_at")[:20], "commits": project.commits.order_by("-created_at")[:10], "roadmap": RoadmapService().project_roadmap_view(project), "lifecycle": lifecycle, "scenario_coverage": ScenarioLabService().coverage(project), "activity": Event.objects.filter(project=project).order_by("-created_at")[:20], "warnings": self.project_warnings(project)}
def graph_run_detail(self, graph_run: GraphRun) -> dict[str, object]:
nodes = list(graph_run.node_runs.select_related("agent_version__agent", "model_request").order_by("created_at", "visit_index"))
traversals = list(graph_run.edge_traversals.order_by("created_at"))
return {"graph_run": graph_run, "nodes": nodes, "traversals": traversals, "approvals": graph_run.approvals.order_by("-created_at"), "model_request_count": sum(1 for node in nodes if node.model_request_id)}
def task_detail(self, task: Task) -> dict[str, object]:
return {"task": task, "dependencies": [edge.depends_on for edge in task.dependency_edges.select_related("depends_on")], "attempts": task.attempts.select_related("coder").order_by("attempt_number"), "graph_runs": task.graph_runs.select_related("execution_graph_version__graph").order_by("-created_at"), "tests": task.test_runs.order_by("-created_at"), "reviews": task.reviews.order_by("-created_at"), "commits": task.commits.order_by("-created_at")}
def steward(self, project: Project | None = None) -> dict[str, object]:
findings = StewardFinding.objects.select_related("project").order_by("-updated_at")
runs = StewardRun.objects.select_related("project").order_by("-created_at")
if project:
findings = findings.filter(project=project)
runs = runs.filter(project=project)
return {"findings": findings[:100], "runs": runs[:50]}
def progeny(self) -> dict[str, object]:
signals = ProgenySignal.objects.select_related("project", "agent_version__agent", "execution_graph_version__graph", "graph_node_run").order_by("-created_at")
grouped = Counter(signals.filter(status="OPEN").values_list("grouping_key", flat=True))
return {"signals": signals[:100], "groups": grouped.most_common(50), "investigations": ProgenyInvestigation.objects.order_by("-created_at")[:50], "experiments": ProgenyExperiment.objects.order_by("-created_at")[:50]}
def roadmap_board(self, project: Project | None = None) -> dict[str, object]:
items = Project.objects.none()
qs = project.roadmap_items if project else None
board = {}
for horizon in RoadmapHorizon.values:
board[horizon] = (qs.filter(horizon=horizon) if qs else __import__("control_plane.projects.models", fromlist=["RoadmapItem"]).RoadmapItem.objects.filter(horizon=horizon)).select_related("project").order_by("-composite_score", "-priority")
return board
def scenario_lab(self, project: Project | None = None) -> dict[str, object]:
suites = ScenarioSuite.objects.select_related("project").order_by("-created_at")
runs = ScenarioRun.objects.select_related("project", "scenario").order_by("-created_at")
findings = ScenarioFinding.objects.select_related("project", "scenario").order_by("-created_at")
if project:
suites = suites.filter(project=project)
runs = runs.filter(project=project)
findings = findings.filter(project=project)
coverage = Counter(ScenarioRun.objects.filter(project=project).values_list("scenario__scenario_type", flat=True) if project else ScenarioRun.objects.values_list("scenario__scenario_type", flat=True))
return {"suites": suites[:50], "runs": runs[:100], "findings": findings[:100], "coverage": dict(coverage)}
def resources(self) -> dict[str, object]:
rows = []
for resource in Resource.objects.order_by("name"):
requests = resource.model_requests.order_by("-created_at")
latencies = [value for value in requests.exclude(latency_ms=None).values_list("latency_ms", flat=True)[:50]]
rows.append({"resource": resource, "recent_requests": requests[:10], "request_count": requests.count(), "median_latency": sorted(latencies)[len(latencies) // 2] if latencies else None})
return {"resources": rows}
def approvals(self) -> dict[str, object]:
return {"approvals": GraphApproval.objects.select_related("graph_run__project", "graph_run__execution_graph_version__graph", "node_run").filter(status=GraphApprovalStatus.PENDING).order_by("created_at")}
def activity(self, project: Project | None = None) -> dict[str, object]:
events = Event.objects.select_related("project", "task").order_by("-created_at")
if project:
events = events.filter(project=project)
return {"events": events[:200]}
def project_brain(self, project: Project) -> dict[str, object]:
return {"project": project, "decisions": project.decisions.order_by("-created_at"), "plans": project.plans.order_by("-version"), "artifacts": project.artifacts.filter(artifact_type__icontains="PLAN").order_by("-created_at")}
def archaeologist(self, project: Project) -> dict[str, object]:
archaeology = project.artifacts.filter(artifact_type__icontains="ARCH").order_by("-created_at")
return {"project": project, "observed": project.architecture_summary, "artifacts": archaeology, "findings": project.findings.order_by("-created_at")[:50]}
def project_warnings(self, project: Project) -> list[str]:
warnings = []
if project.graph_runs.filter(status=GraphRunStatus.FAILED).exists():
warnings.append("failed graph runs")
if project.tasks.filter(status__in=[TaskStatus.BLOCKED, TaskStatus.FAILED]).exists():
warnings.append("blocked or failed tasks")
if project.steward_findings.filter(severity__in=["HIGH", "CRITICAL"]).exclude(status__in=["RESOLVED", "DISMISSED"]).exists():
warnings.append("high severity findings")
return warnings
def _champion_graph(self, name: str):
return __import__("graph.models", fromlist=["ExecutionGraphVersion"]).ExecutionGraphVersion.objects.filter(graph__name=name, status=ExecutionGraphVersionStatus.CHAMPION).select_related("graph").first()

View file

@ -1,21 +1,164 @@
from __future__ import annotations
from django.shortcuts import render
import json
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.http import require_POST
from agents.control_room import AgentControlRoomService
from agents.lifecycle import ExplorerService
from agents.roadmap import RoadmapService
from agents.scenario_lab import ScenarioLabService
from control_plane.events.models import Event
from control_plane.projects.models import Project, TaskStatus
from control_plane.projects.models import Decision, ExplorationOpportunity, Project, RoadmapItem, ScenarioFinding, ScenarioSuite, StewardFinding, Task
from control_plane.projects.ui_services import ControlPlaneUIService
from graph.bootstrap import champion_project_exploration_graph_v1
from graph.langgraph_runtime import LangGraphRuntime
from graph.lifecycle import exploration_registry
from graph.models import GraphApproval, GraphApprovalStatus, GraphRun, GraphRunStatus
ui = ControlPlaneUIService()
def dashboard(request):
projects = Project.objects.order_by("name")
recent_events = Event.objects.select_related("project", "task").order_by("-created_at")[:25]
summary = {
"project_count": Project.objects.count(),
"ready_tasks": sum(project.tasks.filter(status=TaskStatus.READY).count() for project in projects),
"blocked_tasks": sum(project.tasks.filter(status=TaskStatus.BLOCKED).count() for project in projects),
}
return render(request, "projects/dashboard.html", {"projects": projects, "summary": summary, "recent_events": recent_events})
return render(request, "control_plane/dashboard.html", ui.dashboard())
def projects(request):
return render(request, "control_plane/projects.html", {"rows": ui.project_list()})
def project_workspace(request, project_id):
project = get_object_or_404(Project, id=project_id)
return render(request, "control_plane/project_workspace.html", ui.project_workspace(project))
def project_brain(request, project_id):
project = get_object_or_404(Project, id=project_id)
if request.method == "POST":
Decision.objects.create(project=project, decision_type="PROJECT_BRAIN_NOTE", decision=request.POST.get("message", ""), reason="User-authored Project Brain interaction from UI", actor="ui")
return redirect("project_brain", project_id=project.id)
return render(request, "control_plane/project_brain.html", ui.project_brain(project))
def project_archaeologist(request, project_id):
project = get_object_or_404(Project, id=project_id)
return render(request, "control_plane/archaeologist.html", ui.archaeologist(project))
def graph_run_detail(request, graph_run_id):
graph_run = get_object_or_404(GraphRun, id=graph_run_id)
template = "control_plane/partials/graph_run_status.html" if request.headers.get("HX-Request") else "control_plane/graph_run.html"
return render(request, template, ui.graph_run_detail(graph_run))
def graph_run_json(request, graph_run_id):
graph_run = get_object_or_404(GraphRun, id=graph_run_id)
return JsonResponse({"id": graph_run.id, "status": graph_run.status, "current_node": graph_run.current_node, "nodes": list(graph_run.node_runs.values("node_id", "visit_index", "status", "failure_evidence", "telemetry")), "edges": list(graph_run.edge_traversals.values("source_node", "target_node", "result", "condition"))})
def project_dag_json(request, project_id):
project = get_object_or_404(Project, id=project_id)
return JsonResponse({"project": str(project.id), "milestones": list(project.milestones.values("id", "key", "title", "status", "order")), "features": list(project.features.values("id", "milestone_id", "title", "status")), "tasks": list(project.tasks.values("id", "milestone_id", "feature_id", "goal", "status", "priority")), "dependencies": list(project.tasks.values("id", "dependency_edges__depends_on_id"))})
def task_detail(request, task_id):
task = get_object_or_404(Task, id=task_id)
return render(request, "control_plane/task.html", ui.task_detail(task))
def steward(request):
return render(request, "control_plane/steward.html", ui.steward())
def project_steward(request, project_id):
project = get_object_or_404(Project, id=project_id)
return render(request, "control_plane/steward.html", {"project": project, **ui.steward(project)})
def explore(request, project_id=None):
project = get_object_or_404(Project, id=project_id) if project_id else None
opportunities = ExplorationOpportunity.objects.select_related("project", "exploration").order_by("-composite_score", "-created_at")
if project:
opportunities = opportunities.filter(project=project)
return render(request, "control_plane/explore.html", {"project": project, "opportunities": opportunities[:100]})
@require_POST
def run_explore(request, project_id):
project = get_object_or_404(Project, id=project_id)
service = ExplorerService()
exploration = service.start_exploration(project, prompt="UI Explore run")
version = champion_project_exploration_graph_v1()
graph_run = GraphRun.objects.create(execution_graph_version=version, project=project, current_node=version.graph_spec["entry"], metadata={"exploration_id": str(exploration.id), "source": "ui"})
LangGraphRuntime(exploration_registry(service)).run_until_terminal_or_paused(graph_run)
return redirect("graph_run_detail", graph_run_id=graph_run.id)
@require_POST
def opportunity_action(request, opportunity_id):
opportunity = get_object_or_404(ExplorationOpportunity, id=opportunity_id)
action = request.POST.get("action")
service = ExplorerService()
if action == "extend":
service.convert_to_extension(opportunity)
elif action == "evolve":
service.convert_to_evolution(opportunity, baseline_measurement={"metric": "value", "value": 100})
elif action == "roadmap":
RoadmapService().upsert_item(opportunity.project, title=opportunity.title, description=opportunity.description, source="EXPLORE", source_ref={"exploration_opportunity_id": str(opportunity.id)}, rationale=opportunity.rationale, evidence=opportunity.evidence, horizon="NEXT", category=opportunity.opportunity_type, target_action=opportunity.recommended_action if opportunity.recommended_action in ["EXTEND", "EVOLVE", "REPAIR", "INVESTIGATE"] else "NONE", scores={"value": opportunity.value_score, "effort": opportunity.effort_score, "risk": opportunity.risk_score, "confidence": opportunity.confidence, "strategic_fit": opportunity.strategic_fit, "technical_fit": opportunity.technical_fit})
elif action == "defer":
service.defer(opportunity)
elif action == "reject":
service.reject(opportunity)
return redirect(request.META.get("HTTP_REFERER") or reverse("explore"))
def roadmap(request, project_id=None):
project = get_object_or_404(Project, id=project_id) if project_id else None
return render(request, "control_plane/roadmap.html", {"project": project, "board": ui.roadmap_board(project)})
@require_POST
def roadmap_action(request, item_id):
item = get_object_or_404(RoadmapItem, id=item_id)
action = request.POST.get("action")
service = RoadmapService()
if action in ["NOW", "NEXT", "LATER", "EXPLORING"]:
item.horizon = action
item.save(update_fields=["horizon", "updated_at"])
elif action == "defer":
item.status = "DEFERRED"
item.save(update_fields=["status", "updated_at"])
elif action == "reject":
item.status = "REJECTED"
item.save(update_fields=["status", "updated_at"])
elif action == "extend":
service.convert_to_extension(item)
elif action == "evolve":
service.convert_to_evolution(item, baseline_measurement={"metric": "value", "value": 100})
return redirect(request.META.get("HTTP_REFERER") or reverse("roadmap"))
def scenarios(request, project_id=None):
project = get_object_or_404(Project, id=project_id) if project_id else None
return render(request, "control_plane/scenarios.html", {"project": project, **ui.scenario_lab(project)})
@require_POST
def scenario_finding_action(request, finding_id):
finding = get_object_or_404(ScenarioFinding, id=finding_id)
action = request.POST.get("action")
if action == "roadmap":
ScenarioLabService().route_finding(finding)
return redirect(request.META.get("HTTP_REFERER") or reverse("scenarios"))
def progeny(request):
return render(request, "control_plane/progeny.html", ui.progeny())
def agent_control_room(request):
@ -24,4 +167,54 @@ def agent_control_room(request):
for agent in agents:
champion_id = agent.get("champion_version")
agent["health"] = service.get_agent_health(champion_id) if champion_id else {"status": "WATCH", "reasons": ["No champion version."]}
return render(request, "projects/agent_control_room.html", {"agents": agents, "teams": service.list_teams()})
agent["performance"] = service.get_agent_performance(champion_id) if champion_id else {}
return render(request, "control_plane/agents.html", {"agents": agents, "teams": service.list_teams()})
def agent_detail(request, version_id):
service = AgentControlRoomService()
return render(request, "control_plane/agent_detail.html", {"version": service.get_agent_version(version_id), "performance": service.get_agent_performance(version_id), "health": service.get_agent_health(version_id), "usage": service.get_agent_usage(version_id), "progeny": service.get_agent_progeny(version_id), "challengers": service.get_agent_challengers(service.get_agent_version(version_id)["agent_id"])})
def agent_performance_json(request, version_id):
return JsonResponse(AgentControlRoomService().get_agent_performance(version_id, window=request.GET.get("window", "lifetime")))
def resources(request):
return render(request, "control_plane/resources.html", ui.resources())
def approvals(request):
return render(request, "control_plane/approvals.html", ui.approvals())
@require_POST
def approval_action(request, approval_id):
approval = get_object_or_404(GraphApproval, id=approval_id)
action = request.POST.get("action")
approval.status = GraphApprovalStatus.APPROVED if action == "approve" else GraphApprovalStatus.REJECTED
approval.decided_by = "ui"
approval.decided_at = timezone.now()
approval.save(update_fields=["status", "decided_by", "decided_at", "updated_at"])
graph_run = approval.graph_run
if approval.status == GraphApprovalStatus.APPROVED:
graph_run.status = GraphRunStatus.RUNNING
graph_run.failure_reason = ""
graph_run.save(update_fields=["status", "failure_reason", "updated_at"])
self_resume_graph(graph_run)
return redirect("graph_run_detail", graph_run_id=graph_run.id)
def activity(request):
project = Project.objects.filter(id=request.GET.get("project")).first() if request.GET.get("project") else None
return render(request, "control_plane/activity.html", {"project": project, **ui.activity(project)})
def self_resume_graph(graph_run: GraphRun) -> None:
name = graph_run.execution_graph_version.graph.name
try:
if name == "project_exploration":
LangGraphRuntime(exploration_registry(ExplorerService())).run_until_terminal_or_paused(graph_run)
except Exception as exc:
graph_run.failure_reason = f"UI approval saved; automatic resume failed: {exc}"
graph_run.save(update_fields=["failure_reason", "updated_at"])

View file

View file

@ -0,0 +1,8 @@
from __future__ import annotations
from django.apps import AppConfig
class VenturesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "control_plane.ventures"

View file

@ -0,0 +1,239 @@
# Generated by Django 5.2.16 on 2026-08-15 13:50
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('graph', '0004_unique_champion_graph_version'),
]
operations = [
migrations.CreateModel(
name='CompanyMandate',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('objective', models.TextField()),
('constraints', models.JSONField(blank=True, default=dict)),
('optimization_targets', models.JSONField(blank=True, default=list)),
('max_validation_capital', models.DecimalField(decimal_places=2, default=50, max_digits=10)),
('target_net_new_cash', models.DecimalField(decimal_places=2, default=500, max_digits=10)),
('target_window_days', models.PositiveIntegerField(default=30)),
('metadata', models.JSONField(blank=True, default=dict)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='CompanyProposal',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('title', models.CharField(max_length=255)),
('description', models.TextField()),
('problem', models.TextField()),
('target_customer', models.TextField()),
('proposed_solution', models.TextField()),
('business_model', models.TextField()),
('pricing_hypothesis', models.TextField()),
('acquisition_strategy', models.TextField()),
('validation_plan', models.TextField()),
('capital_requested', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('time_to_first_dollar_estimate', models.CharField(max_length=120)),
('expected_margin', models.CharField(max_length=120)),
('build_complexity', models.CharField(max_length=80)),
('market_evidence', models.JSONField(blank=True, default=list)),
('differentiation', models.TextField()),
('major_risks', models.JSONField(blank=True, default=list)),
('confidence', models.FloatField(default=0.0)),
('status', models.CharField(choices=[('DRAFT', 'Draft'), ('SUBMITTED', 'Submitted'), ('UNDER_DILIGENCE', 'Under Diligence'), ('REVISE', 'Revise'), ('FUNDED_RECOMMENDED', 'Funded Recommended'), ('WATCHLIST', 'Watchlist'), ('REJECTED', 'Rejected')], default='DRAFT', max_length=32)),
('pitch', models.JSONField(blank=True, default=dict)),
('metadata', models.JSONField(blank=True, default=dict)),
('mandate', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='company_proposals', to='ventures.companymandate')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='CompanyCapabilityRequirement',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('category', models.CharField(max_length=120)),
('status', models.CharField(choices=[('AVAILABLE', 'Available'), ('PARTIAL', 'Partial'), ('MISSING', 'Missing')], max_length=32)),
('rationale', models.TextField(blank=True)),
('priority', models.CharField(choices=[('BEFORE_VALIDATION', 'Before Validation'), ('BEFORE_FIRST_CUSTOMER', 'Before First Customer'), ('BEFORE_SCALING', 'Before Scaling')], default='BEFORE_SCALING', max_length=32)),
('evidence', models.JSONField(blank=True, default=dict)),
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='capability_requirements', to='ventures.companyproposal')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='CompanyBoardReview',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('observations', models.JSONField(blank=True, default=dict)),
('strengths', models.JSONField(blank=True, default=list)),
('weaknesses', models.JSONField(blank=True, default=list)),
('key_assumptions', models.JSONField(blank=True, default=list)),
('required_revisions', models.JSONField(blank=True, default=list)),
('recommendation', models.CharField(max_length=80)),
('revised_pitch', models.JSONField(blank=True, default=dict)),
('metadata', models.JSONField(blank=True, default=dict)),
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='board_reviews', to='ventures.companyproposal')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ICDiligence',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('status', models.CharField(default='INITIAL_PITCH', max_length=32)),
('rounds', models.JSONField(blank=True, default=list)),
('red_team_challenge', models.JSONField(blank=True, default=dict)),
('final_response', models.JSONField(blank=True, default=dict)),
('metadata', models.JSONField(blank=True, default=dict)),
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ic_diligence', to='ventures.companyproposal')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ICDecision',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('decision', models.CharField(choices=[('FUND_RECOMMENDED', 'Fund Recommended'), ('CONDITIONAL_FUND', 'Conditional Fund'), ('REVISE_AND_RESUBMIT', 'Revise And Resubmit'), ('WATCHLIST', 'Watchlist'), ('PIVOT', 'Pivot'), ('REJECT', 'Reject')], max_length=32)),
('component_scores', models.JSONField(blank=True, default=dict)),
('composite_score', models.FloatField(default=0.0)),
('probability_500_within_30_days', models.FloatField(default=0.0)),
('initial_tranche', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('validation_condition', models.TextField(blank=True)),
('evidence_required', models.JSONField(blank=True, default=list)),
('recommended_allocation', models.JSONField(blank=True, default=dict)),
('kill_criteria', models.JSONField(blank=True, default=list)),
('next_decision_point', models.TextField(blank=True)),
('metadata', models.JSONField(blank=True, default=dict)),
('diligence', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='decision', to='ventures.icdiligence')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ICQuestion',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('question', models.TextField()),
('category', models.CharField(max_length=80)),
('evidence_required', models.BooleanField(default=True)),
('round_name', models.CharField(default='Diligence Round 1', max_length=80)),
('metadata', models.JSONField(blank=True, default=dict)),
('diligence', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='ventures.icdiligence')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ICResponse',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('answer', models.TextField()),
('evidence', models.JSONField(blank=True, default=list)),
('uncertainty', models.TextField(blank=True)),
('pitch_changes', models.JSONField(blank=True, default=dict)),
('metadata', models.JSONField(blank=True, default=dict)),
('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='ventures.icquestion')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='PortfolioCapabilityGap',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('available', models.JSONField(blank=True, default=list)),
('partial', models.JSONField(blank=True, default=list)),
('missing', models.JSONField(blank=True, default=list)),
('ranked_missing', models.JSONField(blank=True, default=list)),
('report', models.TextField(blank=True)),
('metadata', models.JSONField(blank=True, default=dict)),
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='capability_gap_reports', to='ventures.companyproposal')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='VentureArtifact',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('artifact_type', models.CharField(max_length=80)),
('name', models.CharField(max_length=255)),
('content', models.JSONField(blank=True, default=dict)),
('readable', models.TextField(blank=True)),
('generated_by', models.CharField(blank=True, max_length=120)),
('graph_run', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='venture_artifacts', to='graph.graphrun')),
('mandate', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='artifacts', to='ventures.companymandate')),
('proposal', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='artifacts', to='ventures.companyproposal')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='VentureThesis',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('title', models.CharField(max_length=255)),
('thesis', models.TextField()),
('similarity_fingerprint', models.CharField(blank=True, max_length=128)),
('related_theses', models.JSONField(blank=True, default=list)),
('competitive_theses', models.JSONField(blank=True, default=list)),
('portfolio_visibility', models.JSONField(blank=True, default=dict)),
('metadata', models.JSONField(blank=True, default=dict)),
('mandate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='theses', to='ventures.companymandate')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='companyproposal',
name='thesis',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='company_proposals', to='ventures.venturethesis'),
),
]

View file

@ -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')],
},
),
]

View file

@ -0,0 +1,271 @@
from __future__ import annotations
from django.db import models
from control_plane.common import TimestampedModel
class CompanyProposalStatus(models.TextChoices):
DRAFT = "DRAFT"
SUBMITTED = "SUBMITTED"
UNDER_DILIGENCE = "UNDER_DILIGENCE"
REVISE = "REVISE"
FUNDED_RECOMMENDED = "FUNDED_RECOMMENDED"
WATCHLIST = "WATCHLIST"
REJECTED = "REJECTED"
class ICDecisionType(models.TextChoices):
FUND_RECOMMENDED = "FUND_RECOMMENDED"
CONDITIONAL_FUND = "CONDITIONAL_FUND"
REVISE_AND_RESUBMIT = "REVISE_AND_RESUBMIT"
WATCHLIST = "WATCHLIST"
PIVOT = "PIVOT"
REJECT = "REJECT"
class CapabilityStatus(models.TextChoices):
AVAILABLE = "AVAILABLE"
PARTIAL = "PARTIAL"
MISSING = "MISSING"
class CapabilityPriority(models.TextChoices):
BEFORE_VALIDATION = "BEFORE_VALIDATION"
BEFORE_FIRST_CUSTOMER = "BEFORE_FIRST_CUSTOMER"
BEFORE_SCALING = "BEFORE_SCALING"
class EvidenceTier(models.TextChoices):
TIER_0_THESIS = "TIER_0_THESIS"
TIER_1_PUBLIC_EVIDENCE = "TIER_1_PUBLIC_EVIDENCE"
TIER_2_CUSTOMER_SIGNAL = "TIER_2_CUSTOMER_SIGNAL"
TIER_3_WILLINGNESS_TO_PAY = "TIER_3_WILLINGNESS_TO_PAY"
TIER_4_PAID_CUSTOMER = "TIER_4_PAID_CUSTOMER"
TIER_5_REPEATABLE_TRACTION = "TIER_5_REPEATABLE_TRACTION"
class OverlapClassification(models.TextChoices):
NONE = "NONE"
ADJACENT = "ADJACENT"
COMPETITIVE = "COMPETITIVE"
NEAR_DUPLICATE = "NEAR_DUPLICATE"
DUPLICATE = "DUPLICATE"
class CompanyMandate(TimestampedModel):
objective = models.TextField()
constraints = models.JSONField(default=dict, blank=True)
optimization_targets = models.JSONField(default=list, blank=True)
max_validation_capital = models.DecimalField(max_digits=10, decimal_places=2, default=50)
target_net_new_cash = models.DecimalField(max_digits=10, decimal_places=2, default=500)
target_window_days = models.PositiveIntegerField(default=30)
metadata = models.JSONField(default=dict, blank=True)
class VentureThesis(TimestampedModel):
mandate = models.ForeignKey(CompanyMandate, on_delete=models.CASCADE, related_name="theses")
title = models.CharField(max_length=255)
thesis = models.TextField()
similarity_fingerprint = models.CharField(max_length=128, blank=True)
related_theses = models.JSONField(default=list, blank=True)
competitive_theses = models.JSONField(default=list, blank=True)
portfolio_visibility = models.JSONField(default=dict, blank=True)
metadata = models.JSONField(default=dict, blank=True)
evidence_tier = models.CharField(max_length=40, choices=EvidenceTier.choices, default=EvidenceTier.TIER_0_THESIS)
class CompanyProposal(TimestampedModel):
mandate = models.ForeignKey(CompanyMandate, on_delete=models.PROTECT, related_name="company_proposals")
thesis = models.ForeignKey(VentureThesis, on_delete=models.SET_NULL, null=True, blank=True, related_name="company_proposals")
title = models.CharField(max_length=255)
description = models.TextField()
problem = models.TextField()
target_customer = models.TextField()
proposed_solution = models.TextField()
business_model = models.TextField()
pricing_hypothesis = models.TextField()
acquisition_strategy = models.TextField()
validation_plan = models.TextField()
capital_requested = models.DecimalField(max_digits=10, decimal_places=2, default=0)
time_to_first_dollar_estimate = models.CharField(max_length=120)
expected_margin = models.CharField(max_length=120)
build_complexity = models.CharField(max_length=80)
market_evidence = models.JSONField(default=list, blank=True)
differentiation = models.TextField()
major_risks = models.JSONField(default=list, blank=True)
confidence = models.FloatField(default=0.0)
status = models.CharField(max_length=32, choices=CompanyProposalStatus.choices, default=CompanyProposalStatus.DRAFT)
pitch = models.JSONField(default=dict, blank=True)
metadata = models.JSONField(default=dict, blank=True)
evidence_tier = models.CharField(max_length=40, choices=EvidenceTier.choices, default=EvidenceTier.TIER_0_THESIS)
class CompanyBoardReview(TimestampedModel):
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="board_reviews")
observations = models.JSONField(default=dict, blank=True)
strengths = models.JSONField(default=list, blank=True)
weaknesses = models.JSONField(default=list, blank=True)
key_assumptions = models.JSONField(default=list, blank=True)
required_revisions = models.JSONField(default=list, blank=True)
recommendation = models.CharField(max_length=80)
revised_pitch = models.JSONField(default=dict, blank=True)
metadata = models.JSONField(default=dict, blank=True)
class ICDiligence(TimestampedModel):
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="ic_diligence")
status = models.CharField(max_length=32, default="INITIAL_PITCH")
rounds = models.JSONField(default=list, blank=True)
red_team_challenge = models.JSONField(default=dict, blank=True)
final_response = models.JSONField(default=dict, blank=True)
metadata = models.JSONField(default=dict, blank=True)
class ICQuestion(TimestampedModel):
diligence = models.ForeignKey(ICDiligence, on_delete=models.CASCADE, related_name="questions")
question = models.TextField()
category = models.CharField(max_length=80)
evidence_required = models.BooleanField(default=True)
round_name = models.CharField(max_length=80, default="Diligence Round 1")
metadata = models.JSONField(default=dict, blank=True)
class ICResponse(TimestampedModel):
question = models.ForeignKey(ICQuestion, on_delete=models.CASCADE, related_name="responses")
answer = models.TextField()
evidence = models.JSONField(default=list, blank=True)
uncertainty = models.TextField(blank=True)
pitch_changes = models.JSONField(default=dict, blank=True)
metadata = models.JSONField(default=dict, blank=True)
class ICDecision(TimestampedModel):
diligence = models.OneToOneField(ICDiligence, on_delete=models.CASCADE, related_name="decision")
decision = models.CharField(max_length=32, choices=ICDecisionType.choices)
component_scores = models.JSONField(default=dict, blank=True)
composite_score = models.FloatField(default=0.0)
probability_500_within_30_days = models.FloatField(default=0.0)
initial_tranche = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
validation_condition = models.TextField(blank=True)
evidence_required = models.JSONField(default=list, blank=True)
recommended_allocation = models.JSONField(default=dict, blank=True)
kill_criteria = models.JSONField(default=list, blank=True)
next_decision_point = models.TextField(blank=True)
metadata = models.JSONField(default=dict, blank=True)
evidence_tier = models.CharField(max_length=40, choices=EvidenceTier.choices, default=EvidenceTier.TIER_0_THESIS)
raw_probability_500_within_30_days = models.FloatField(default=0.0)
evidence_ceiling = models.FloatField(default=35.0)
probability_explanation = models.TextField(blank=True)
score_definitions = models.JSONField(default=dict, blank=True)
class VentureThesisFingerprint(TimestampedModel):
proposal = models.OneToOneField(CompanyProposal, on_delete=models.CASCADE, related_name="fingerprint")
industry = models.CharField(max_length=160)
icp = models.TextField()
problem = models.TextField()
offer = models.TextField()
business_model = models.CharField(max_length=160)
primary_distribution_channel = models.CharField(max_length=160)
price_band = models.CharField(max_length=80)
time_to_first_cash_band = models.CharField(max_length=80)
required_capability_set = models.JSONField(default=list, blank=True)
geography_dependency = models.CharField(max_length=120, blank=True)
regulatory_dependency = models.CharField(max_length=120, blank=True)
online_offline = models.CharField(max_length=80, blank=True)
service_software_hybrid = models.CharField(max_length=80, blank=True)
fingerprint_hash = models.CharField(max_length=128)
metadata = models.JSONField(default=dict, blank=True)
class VentureCohort(TimestampedModel):
cohort_id = models.CharField(max_length=80, unique=True)
mandate = models.ForeignKey(CompanyMandate, on_delete=models.PROTECT, related_name="venture_cohorts")
cohort_size = models.PositiveIntegerField(default=10)
status = models.CharField(max_length=40, default="PREPARING")
graph_run = models.ForeignKey("graph.GraphRun", on_delete=models.SET_NULL, null=True, blank=True, related_name="venture_cohorts")
concurrency = models.PositiveIntegerField(default=1)
model_versions = models.JSONField(default=dict, blank=True)
graph_versions = models.JSONField(default=dict, blank=True)
research_policy = models.JSONField(default=dict, blank=True)
scoring_policy = models.JSONField(default=dict, blank=True)
evidence_calibration_policy = models.JSONField(default=dict, blank=True)
metrics = models.JSONField(default=dict, blank=True)
metadata = models.JSONField(default=dict, blank=True)
class VentureCohortMember(TimestampedModel):
cohort = models.ForeignKey(VentureCohort, on_delete=models.CASCADE, related_name="members")
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="cohort_memberships")
child_graph_run = models.ForeignKey("graph.GraphRun", on_delete=models.SET_NULL, null=True, blank=True, related_name="venture_cohort_members")
rank = models.PositiveIntegerField(null=True, blank=True)
is_top_3 = models.BooleanField(default=False)
portfolio_score = models.FloatField(default=0.0)
metadata = models.JSONField(default=dict, blank=True)
class Meta:
constraints = [models.UniqueConstraint(fields=["cohort", "proposal"], name="unique_venture_cohort_member")]
class VentureCollision(TimestampedModel):
cohort = models.ForeignKey(VentureCohort, on_delete=models.CASCADE, related_name="collisions")
company_a = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="collisions_as_a")
company_b = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="collisions_as_b")
classification = models.CharField(max_length=40, choices=OverlapClassification.choices)
similarity_score = models.FloatField(default=0.0)
explanation = models.TextField(blank=True)
overlapping_dimensions = models.JSONField(default=list, blank=True)
class PortfolioICReview(TimestampedModel):
cohort = models.OneToOneField(VentureCohort, on_delete=models.CASCADE, related_name="portfolio_review")
rankings = models.JSONField(default=list, blank=True)
top_3 = models.JSONField(default=list, blank=True)
concentration = models.JSONField(default=dict, blank=True)
capability_demand = models.JSONField(default=list, blank=True)
top_3_capability_gaps = models.JSONField(default=list, blank=True)
recommended_build_priorities = models.JSONField(default=list, blank=True)
metadata = models.JSONField(default=dict, blank=True)
class VentureCapabilityDemand(TimestampedModel):
cohort = models.ForeignKey(VentureCohort, on_delete=models.CASCADE, related_name="capability_demands")
capability = models.CharField(max_length=120)
count = models.PositiveIntegerField(default=0)
percentage = models.FloatField(default=0.0)
earliest_stage = models.CharField(max_length=32, choices=CapabilityPriority.choices)
companies = models.JSONField(default=list, blank=True)
status_distribution = models.JSONField(default=dict, blank=True)
top_3_count = models.PositiveIntegerField(default=0)
priority_score = models.FloatField(default=0.0)
class CompanyCapabilityRequirement(TimestampedModel):
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="capability_requirements")
category = models.CharField(max_length=120)
status = models.CharField(max_length=32, choices=CapabilityStatus.choices)
rationale = models.TextField(blank=True)
priority = models.CharField(max_length=32, choices=CapabilityPriority.choices, default=CapabilityPriority.BEFORE_SCALING)
evidence = models.JSONField(default=dict, blank=True)
class PortfolioCapabilityGap(TimestampedModel):
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, related_name="capability_gap_reports")
available = models.JSONField(default=list, blank=True)
partial = models.JSONField(default=list, blank=True)
missing = models.JSONField(default=list, blank=True)
ranked_missing = models.JSONField(default=list, blank=True)
report = models.TextField(blank=True)
metadata = models.JSONField(default=dict, blank=True)
class VentureArtifact(TimestampedModel):
proposal = models.ForeignKey(CompanyProposal, on_delete=models.CASCADE, null=True, blank=True, related_name="artifacts")
mandate = models.ForeignKey(CompanyMandate, on_delete=models.CASCADE, null=True, blank=True, related_name="artifacts")
graph_run = models.ForeignKey("graph.GraphRun", on_delete=models.SET_NULL, null=True, blank=True, related_name="venture_artifacts")
artifact_type = models.CharField(max_length=80)
name = models.CharField(max_length=255)
content = models.JSONField(default=dict, blank=True)
readable = models.TextField(blank=True)
generated_by = models.CharField(max_length=120, blank=True)

View 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.

View file

@ -9,6 +9,8 @@ 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
def champion_task_execution_graph_v1() -> ExecutionGraphVersion:
@ -96,3 +98,11 @@ def champion_scenario_lab_graph_v1() -> ExecutionGraphVersion:
def champion_agent_investigation_graph_v1() -> ExecutionGraphVersion:
return _champion_graph(agent_investigation_graph_v1())
def champion_venture_discovery_graph_v1() -> ExecutionGraphVersion:
return _champion_graph(venture_discovery_graph_v1())
def champion_venture_discovery_cohort_graph_v1() -> ExecutionGraphVersion:
return _champion_graph(venture_discovery_cohort_graph_v1())

107
graph/venture_cohort.py Normal file
View file

@ -0,0 +1,107 @@
from __future__ import annotations
from agents.venture_discovery import VentureDiscoveryService
from control_plane.ventures.models import VentureCohort
from graph.native_runtime import GraphExecutionContext
from graph.registry import NodeHandlerRegistry, NodeResult
from graph.spec import ExecutionGraphSpec, GraphEdgeSpec, GraphNodeSpec
def venture_discovery_cohort_graph_v1() -> ExecutionGraphSpec:
nodes = ["prepare_cohort", "generate_independent_proposals", "initial_research", "fingerprint_theses", "collision_analysis", "run_individual_diligence", "portfolio_compare", "portfolio_ic", "aggregate_capabilities", "produce_cohort_report", "complete"]
spec = ExecutionGraphSpec(name="venture_discovery_cohort", version=1, graph_type="VENTURE_DISCOVERY_COHORT", entry="prepare_cohort", nodes={node: GraphNodeSpec(node, node if node == "complete" else f"venture_cohort_{node}") for node in nodes}, edges=[GraphEdgeSpec(nodes[index], nodes[index + 1], "success") for index in range(len(nodes) - 1)], terminal_nodes=["complete"], metadata={"description": "Venture Discovery V0.2: 10-company independent cohort, portfolio IC, collisions, capability demand, and report."})
spec.validate()
return spec
class CohortNode:
idempotent = True
replay_safe = True
destructive = False
def __init__(self, service: VentureDiscoveryService, node_type: str, *, cohort_size: int = 10, concurrency: int = 1) -> None:
self.service = service
self.node_type = node_type
self.cohort_size = cohort_size
self.concurrency = concurrency
def cohort(self, context: GraphExecutionContext) -> VentureCohort:
return VentureCohort.objects.get(id=context.graph_run.metadata["cohort_id"])
class PrepareCohortNode(CohortNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
cohort = self.service.prepare_cohort(size=self.cohort_size, graph_run=context.graph_run, concurrency=self.concurrency)
metadata = dict(context.graph_run.metadata)
metadata["cohort_id"] = str(cohort.id)
metadata["cohort_stable_id"] = cohort.cohort_id
metadata["no_real_spend"] = True
metadata["no_real_customer_outreach"] = True
context.graph_run.metadata = metadata
context.graph_run.save(update_fields=["metadata", "updated_at"])
return NodeResult("COMPLETE", "success", {"cohort_id": cohort.cohort_id, "size": cohort.cohort_size})
class GenerateIndependentNode(CohortNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
proposals = self.service.generate_independent_proposals(self.cohort(context))
return NodeResult("COMPLETE", "success", {"proposal_count": len(proposals), "independent_generation": True})
class ResearchNode(CohortNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
cohort = self.cohort(context)
self.service.research_cohort(cohort)
cohort.refresh_from_db()
return NodeResult("COMPLETE", "success", cohort.metrics)
class FingerprintNode(CohortNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
fingerprints = self.service.fingerprint_cohort(self.cohort(context))
return NodeResult("COMPLETE", "success", {"fingerprint_count": len(fingerprints)})
class CollisionNode(CohortNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
collisions = self.service.analyze_collisions(self.cohort(context))
return NodeResult("COMPLETE", "success", {"pair_count": len(collisions)})
class DiligenceNode(CohortNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
cohort = self.cohort(context)
self.service.run_individual_diligence_for_cohort(cohort)
return NodeResult("COMPLETE", "success", {"diligence_count": cohort.members.count()})
class PortfolioNode(CohortNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
review = self.service.portfolio_ic(self.cohort(context))
return NodeResult("COMPLETE", "success", {"ranked_count": len(review.rankings), "top_3": review.top_3})
class CapabilityNode(CohortNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
cohort = self.cohort(context)
demand = self.service.aggregate_capability_demand(cohort)
top_3 = self.service.aggregate_capability_demand(cohort, top_3_only=True)
return NodeResult("COMPLETE", "success", {"capability_count": len(demand), "top_3_gap_count": len(top_3)})
class ReportNode(CohortNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
artifact = self.service.produce_cohort_report(self.cohort(context))
return NodeResult("COMPLETE", "success", {"artifact_id": str(artifact.id), "artifact_type": artifact.artifact_type})
class NoopNode(CohortNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
return NodeResult("COMPLETE", "success")
def venture_discovery_cohort_registry(service: VentureDiscoveryService, *, cohort_size: int = 10, concurrency: int = 1) -> NodeHandlerRegistry:
registry = NodeHandlerRegistry()
for handler in [PrepareCohortNode(service, "venture_cohort_prepare_cohort", cohort_size=cohort_size, concurrency=concurrency), GenerateIndependentNode(service, "venture_cohort_generate_independent_proposals"), ResearchNode(service, "venture_cohort_initial_research"), FingerprintNode(service, "venture_cohort_fingerprint_theses"), CollisionNode(service, "venture_cohort_collision_analysis"), DiligenceNode(service, "venture_cohort_run_individual_diligence"), NoopNode(service, "venture_cohort_portfolio_compare"), PortfolioNode(service, "venture_cohort_portfolio_ic"), CapabilityNode(service, "venture_cohort_aggregate_capabilities"), ReportNode(service, "venture_cohort_produce_cohort_report")]:
registry.register(handler)
return registry

163
graph/venture_discovery.py Normal file
View file

@ -0,0 +1,163 @@
from __future__ import annotations
from agents.venture_discovery import VentureDiscoveryService
from control_plane.ventures.models import CompanyMandate, CompanyProposal, ICDiligence, ICDecision, PortfolioCapabilityGap
from graph.native_runtime import GraphExecutionContext
from graph.registry import NodeHandlerRegistry, NodeResult
from graph.spec import ExecutionGraphSpec, GraphEdgeSpec, GraphNodeSpec
def venture_discovery_graph_v1() -> ExecutionGraphSpec:
nodes = ["prepare_mandate", "generate_company", "conduct_research", "board_review", "revise_pitch", "ic_first_pass", "generate_questions", "company_response", "red_team", "final_response", "score", "ic_decision", "capability_analysis", "produce_investment_memo", "complete"]
spec = ExecutionGraphSpec(
name="venture_discovery",
version=1,
graph_type="VENTURE_DISCOVERY",
entry="prepare_mandate",
nodes={node: GraphNodeSpec(node, node if node == "complete" else f"venture_{node}") for node in nodes},
edges=[GraphEdgeSpec(nodes[index], nodes[index + 1], "success") for index in range(len(nodes) - 1)],
terminal_nodes=["complete"],
metadata={"description": "Venture Discovery V0: exactly one startup proposal through Board, IC diligence, scoring, memo, and capability-gap analysis."},
)
spec.validate()
return spec
class VentureNode:
idempotent = True
replay_safe = True
destructive = False
def __init__(self, service: VentureDiscoveryService, node_type: str) -> None:
self.service = service
self.node_type = node_type
def mandate(self, context: GraphExecutionContext) -> CompanyMandate:
return CompanyMandate.objects.get(id=context.graph_run.metadata["mandate_id"])
def proposal(self, context: GraphExecutionContext) -> CompanyProposal:
return CompanyProposal.objects.get(id=context.graph_run.metadata["proposal_id"])
def diligence(self, context: GraphExecutionContext) -> ICDiligence:
return ICDiligence.objects.get(id=context.graph_run.metadata["diligence_id"])
def gap(self, context: GraphExecutionContext) -> PortfolioCapabilityGap:
return PortfolioCapabilityGap.objects.get(id=context.graph_run.metadata["capability_gap_id"])
class PrepareMandateNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
mandate = self.service.create_v0_mandate()
metadata = dict(context.graph_run.metadata)
metadata["mandate_id"] = str(mandate.id)
metadata["exactly_one_company_required"] = True
metadata["no_real_spend"] = True
metadata["no_real_customer_outreach"] = True
context.graph_run.metadata = metadata
context.graph_run.save(update_fields=["metadata", "updated_at"])
return NodeResult("COMPLETE", "success", {"mandate_id": str(mandate.id), "objective": mandate.objective})
class GenerateCompanyNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
proposal = self.service.generate_single_company(self.mandate(context), graph_run=context.graph_run)
metadata = dict(context.graph_run.metadata)
metadata["proposal_id"] = str(proposal.id)
context.graph_run.metadata = metadata
context.graph_run.save(update_fields=["metadata", "updated_at"])
return NodeResult("COMPLETE", "success", {"proposal_id": str(proposal.id), "company": proposal.title, "generation_source": proposal.metadata.get("generation_source")})
class BoardReviewNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
review = self.service.board_review(self.proposal(context), graph_run=context.graph_run)
return NodeResult("COMPLETE", "success", {"board_review_id": str(review.id), "recommendation": review.recommendation})
class ResearchNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
research = self.service.conduct_market_research(self.proposal(context), graph_run=context.graph_run)
return NodeResult("COMPLETE", "success", {"source_count": len(research.get("sources", [])), "unverified_categories": research.get("unverified_categories", [])})
class RevisePitchNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
proposal = self.proposal(context)
latest = proposal.board_reviews.order_by("-created_at").first()
if latest and latest.revised_pitch:
proposal.pitch = latest.revised_pitch
proposal.save(update_fields=["pitch", "updated_at"])
return NodeResult("COMPLETE", "success", {"proposal_id": str(proposal.id), "pitch_revised": latest is not None})
class ICFirstPassNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
diligence = self.service.start_ic_diligence(self.proposal(context), graph_run=context.graph_run)
metadata = dict(context.graph_run.metadata)
metadata["diligence_id"] = str(diligence.id)
context.graph_run.metadata = metadata
context.graph_run.save(update_fields=["metadata", "updated_at"])
return NodeResult("COMPLETE", "success", {"diligence_id": str(diligence.id), "status": diligence.status})
class ICQuestionsNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
questions = self.service.generate_ic_questions(self.diligence(context), graph_run=context.graph_run)
return NodeResult("COMPLETE", "success", {"question_count": len(questions), "question_ids": [str(q.id) for q in questions]})
class CompanyResponseNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
responses = self.service.answer_questions(self.diligence(context), graph_run=context.graph_run)
return NodeResult("COMPLETE", "success", {"response_count": len(responses)})
class RedTeamNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
challenge = self.service.red_team(self.diligence(context), graph_run=context.graph_run)
return NodeResult("COMPLETE", "success", {"concern_count": len(challenge.get("concerns", []))})
class FinalResponseNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
response = self.service.final_company_response(self.diligence(context), graph_run=context.graph_run)
return NodeResult("COMPLETE", "success", {"kill_criteria_count": len(response.get("kill_criteria", []))})
class ScoreNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
decision = self.service.score_and_decide(self.diligence(context), graph_run=context.graph_run)
metadata = dict(context.graph_run.metadata)
metadata["decision_id"] = str(decision.id)
context.graph_run.metadata = metadata
context.graph_run.save(update_fields=["metadata", "updated_at"])
return NodeResult("COMPLETE", "success", {"decision_id": str(decision.id), "score": decision.composite_score})
class ICDecisionNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
decision = ICDecision.objects.get(id=context.graph_run.metadata["decision_id"])
return NodeResult("COMPLETE", "success", {"decision": decision.decision, "initial_tranche": str(decision.initial_tranche or "")})
class CapabilityAnalysisNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
gap = self.service.capability_analysis(self.proposal(context), graph_run=context.graph_run)
metadata = dict(context.graph_run.metadata)
metadata["capability_gap_id"] = str(gap.id)
context.graph_run.metadata = metadata
context.graph_run.save(update_fields=["metadata", "updated_at"])
return NodeResult("COMPLETE", "success", {"capability_gap_id": str(gap.id), "missing_count": len(gap.missing)})
class InvestmentMemoNode(VentureNode):
def run(self, context: GraphExecutionContext) -> NodeResult:
memo = self.service.produce_investment_memo(self.diligence(context), self.gap(context), graph_run=context.graph_run)
return NodeResult("COMPLETE", "success", {"memo_artifact_id": str(memo.id), "artifact_type": memo.artifact_type})
def venture_discovery_registry(service: VentureDiscoveryService) -> NodeHandlerRegistry:
registry = NodeHandlerRegistry()
for handler in [PrepareMandateNode(service, "venture_prepare_mandate"), GenerateCompanyNode(service, "venture_generate_company"), ResearchNode(service, "venture_conduct_research"), BoardReviewNode(service, "venture_board_review"), RevisePitchNode(service, "venture_revise_pitch"), ICFirstPassNode(service, "venture_ic_first_pass"), ICQuestionsNode(service, "venture_generate_questions"), CompanyResponseNode(service, "venture_company_response"), RedTeamNode(service, "venture_red_team"), FinalResponseNode(service, "venture_final_response"), ScoreNode(service, "venture_score"), ICDecisionNode(service, "venture_ic_decision"), CapabilityAnalysisNode(service, "venture_capability_analysis"), InvestmentMemoNode(service, "venture_produce_investment_memo")]:
registry.register(handler)
return registry

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>Activity{% if project %} / {{ project.name }}{% endif %}</h2><p class="muted">Unified persisted event stream.</p></div></header><section class="panel">{% include "control_plane/partials/event_list.html" with events=events %}</section>{% endblock %}

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>{{ version.agent }} v{{ version.version }}</h2><p>{{ version.system_contract }}</p></div><span class="badge {{ health.status }}">{{ health.status }}</span></header><section class="grid"><div class="panel"><h3>Champion / Challenger</h3><p>Status: {{ version.promotion_status }}</p><p>Benchmark: {{ version.benchmark_status }}</p><p>Parent: {{ version.parent_version_id|default:"none" }}</p><h4>Challengers</h4>{% for challenger in challengers.challengers %}<p>{{ challenger.agent }} v{{ challenger.version }}</p>{% empty %}<p>No challengers.</p>{% endfor %}</div><div class="panel"><h3>Model / Policies</h3><p>{{ version.model }}</p><pre>{{ version.context_policy }}</pre><pre>{{ version.tool_policy }}</pre></div><div class="panel"><h3>Health Reasons</h3>{% for reason in health.reasons %}<p>{{ reason }}</p>{% endfor %}</div></section><section class="grid"><div class="panel"><h3>Competencies</h3>{% for c in version.competencies %}<p>{{ c.competency__key }} / proficiency {{ c.proficiency }} / confidence {{ c.confidence }}</p>{% empty %}<p>No competencies.</p>{% endfor %}</div><div class="panel"><h3>Performance</h3><pre>{{ performance }}</pre></div><div class="panel"><h3>Usage</h3><pre>{{ usage }}</pre></div></section><section class="panel"><h3>Progeny</h3><pre>{{ progeny }}</pre></section>{% endblock %}

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>Agent Control Room</h2><p class="muted">Governed workforce inventory, champion/challenger state, health, performance, and teams.</p></div></header><table><thead><tr><th>Agent</th><th>Role</th><th>Scope</th><th>Champion</th><th>Health</th><th>Performance</th></tr></thead><tbody>{% for agent in agents %}<tr><td>{{ agent.name }}<br><span class="muted">{{ agent.purpose }}</span></td><td>{{ agent.role }}</td><td>{{ agent.scope }}</td><td>{% if agent.champion_version %}<a href="{% url 'agent_detail' agent.champion_version %}">{{ agent.champion_version }}</a>{% else %}None{% endif %}</td><td><span class="badge {{ agent.health.status }}">{{ agent.health.status }}</span><br>{{ agent.health.reasons|join:", " }}</td><td>{% if agent.performance.quality %}Completion {{ agent.performance.quality.task_completion_rate|default:"n/a" }}<br>Test pass {{ agent.performance.quality.test_pass_rate|default:"n/a" }}{% else %}No data{% endif %}</td></tr>{% empty %}<tr><td colspan="6">No agents.</td></tr>{% endfor %}</tbody></table><section class="panel"><h3>Teams</h3>{% for team in teams %}<article class="card"><strong>{{ team.name }}</strong><p>{{ team.purpose }}</p><table><thead><tr><th>Role</th><th>AgentVersion</th><th>Status</th></tr></thead><tbody>{% for member in team.members %}<tr><td>{{ member.role }}</td><td>{{ member.agent_version__agent__name }} v{{ member.agent_version__version }}</td><td>{{ member.status }}</td></tr>{% endfor %}</tbody></table></article>{% empty %}<p>No teams.</p>{% endfor %}</section>{% endblock %}

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>Approvals</h2><p class="muted">Durable approval inbox for paused graph decisions.</p></div></header><table><thead><tr><th>Source</th><th>Project</th><th>Reason</th><th>Evidence</th><th>Requested</th><th>Actions</th></tr></thead><tbody>{% for approval in approvals %}<tr><td><a href="{% url 'graph_run_detail' approval.graph_run.id %}">{{ approval.graph_run.execution_graph_version.graph.name }} v{{ approval.graph_run.execution_graph_version.version }}</a></td><td>{{ approval.graph_run.project.name|default:"-" }}</td><td>{{ approval.reason }}</td><td><pre>{{ approval.payload }}</pre></td><td>{{ approval.created_at }}</td><td><form class="actions" method="post" action="{% url 'approval_action' approval.id %}">{% csrf_token %}<button name="action" value="approve">Approve</button><button class="secondary" name="action" value="reject">Reject</button></form></td></tr>{% empty %}<tr><td colspan="6">No pending approvals.</td></tr>{% endfor %}</tbody></table>{% endblock %}

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>Archaeologist / {{ project.name }}</h2><p class="muted">Observed system, inferred intent, and gap evidence.</p></div><a class="button secondary" href="{% url 'project_brain' project.id %}">Discuss with Project Brain</a></header><section class="grid"><div class="panel"><h3>Observed System</h3><p>{{ observed|default:"No architecture summary yet." }}</p></div><div class="panel"><h3>Evidence Artifacts</h3>{% for artifact in artifacts %}<p>{{ artifact.name }} <span class="badge">{{ artifact.artifact_type }}</span></p>{% empty %}<p>No archaeologist artifacts.</p>{% endfor %}</div><div class="panel"><h3>Gap Analysis</h3>{% for finding in findings %}<p><span class="badge {{ finding.severity }}">{{ finding.severity }}</span> {{ finding.title }}</p>{% empty %}<p>No findings.</p>{% endfor %}</div></section>{% endblock %}

View file

@ -0,0 +1,64 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Artifex{% endblock %}</title>
<style>
:root { --bg: #f6f2ea; --panel: #fffdfa; --line: #d8cfc2; --ink: #162033; --muted: #685f55; --accent: #284c7e; --danger: #9f2d2d; --warn: #9b681c; --ok: #23704a; }
* { box-sizing: border-box; }
body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: var(--ink); background: var(--bg); }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.shell { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh; }
nav { border-right: 1px solid var(--line); background: #ede6da; padding: 1.25rem; position: sticky; top: 0; height: 100vh; }
nav h1 { font-size: 1.15rem; letter-spacing: .12em; margin: 0 0 1.5rem; }
nav a { display: block; padding: .55rem .65rem; border-radius: 8px; color: var(--ink); }
nav a:hover { background: rgba(40, 76, 126, .09); text-decoration: none; }
main { padding: 1.5rem; max-width: 1480px; width: 100%; }
header.page { display: flex; justify-content: space-between; gap: 1rem; align-items: flex-start; margin-bottom: 1rem; }
h2, h3 { margin-bottom: .4rem; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }
.card, .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 1rem; margin-bottom: 1rem; box-shadow: 0 1px 0 rgba(0,0,0,.03); }
.metric { font-size: 2rem; font-weight: 760; line-height: 1; }
.muted { color: var(--muted); }
.badge { display: inline-block; padding: .18rem .45rem; border-radius: 999px; border: 1px solid var(--line); background: #f4efe6; font-size: .75rem; font-weight: 700; letter-spacing: .04em; }
.badge.COMPLETE, .badge.PASS, .badge.HEALTHY, .badge.APPROVED { color: var(--ok); border-color: rgba(35,112,74,.35); }
.badge.FAILED, .badge.FAIL, .badge.DEGRADED, .badge.REJECTED { color: var(--danger); border-color: rgba(159,45,45,.35); }
.badge.RUNNING, .badge.PAUSED, .badge.WATCH, .badge.PENDING { color: var(--warn); border-color: rgba(155,104,28,.35); }
table { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
th, td { text-align: left; padding: .65rem .75rem; border-bottom: 1px solid var(--line); vertical-align: top; }
th { font-size: .78rem; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); background: #f1eadf; }
tr:last-child td { border-bottom: 0; }
button, .button { border: 1px solid var(--accent); color: white; background: var(--accent); border-radius: 8px; padding: .45rem .7rem; cursor: pointer; font-weight: 650; }
button.secondary, .button.secondary { color: var(--ink); background: transparent; border-color: var(--line); }
.actions { display: flex; gap: .4rem; flex-wrap: wrap; }
pre { white-space: pre-wrap; word-break: break-word; background: #201d19; color: #f8f2e8; padding: .8rem; border-radius: 10px; max-height: 360px; overflow: auto; }
.timeline { border-left: 3px solid var(--line); margin-left: .5rem; padding-left: 1rem; }
.node { margin: .5rem 0; }
@media (max-width: 860px) { .shell { grid-template-columns: 1fr; } nav { position: static; height: auto; } nav a { display: inline-block; } main { padding: 1rem; } }
</style>
{% block head %}{% endblock %}
</head>
<body>
<div class="shell">
<nav aria-label="Primary navigation">
<h1>ARTIFEX</h1>
<a href="/">Dashboard</a>
<a href="/projects/">Projects</a>
<a href="/agents/">Agents</a>
<a href="/progeny/">Progeny</a>
<a href="/steward/">Steward</a>
<a href="/roadmap/">Roadmap</a>
<a href="/scenario-lab/">Scenario Lab</a>
<a href="/approvals/">Approvals</a>
<a href="/resources/">Resources</a>
<a href="/activity/">Activity</a>
<a href="/admin/">Settings</a>
</nav>
<main id="main">
{% block content %}{% endblock %}
</main>
</div>
</body>
</html>

View file

@ -0,0 +1,16 @@
{% extends "control_plane/base.html" %}
{% block title %}Dashboard / Artifex{% endblock %}
{% block content %}
<header class="page"><div><h2>Dashboard</h2><p class="muted">Operational control plane for projects, graphs, agents, approvals, and autonomous lifecycle work.</p></div><a class="button secondary" href="/approvals/">Approval Inbox: {{ approval_count }}</a></header>
<section class="grid">
<div class="card"><div class="metric">{{ project_summary.total }}</div><strong>Projects</strong><p>{{ project_summary.active }} active, {{ project_summary.blocked_failed }} blocked/failed</p></div>
<div class="card"><div class="metric">{{ execution_summary.active }}</div><strong>Active GraphRuns</strong><p>{{ execution_summary.failed }} failed. Champion task graph: {{ execution_summary.champion_task_graph|default:"not bootstrapped" }}</p></div>
<div class="card"><div class="metric">{{ steward_summary.open_findings }}</div><strong>Steward Findings</strong><p>{{ steward_summary.high_findings }} high/critical across {{ steward_summary.enrolled_projects }} enrolled projects</p></div>
<div class="card"><div class="metric">{{ progeny_summary.unresolved_signals }}</div><strong>Progeny Signals</strong><p>{{ progeny_summary.open_investigations }} investigations, {{ progeny_summary.challengers }} challengers</p></div>
<div class="card"><div class="metric">{{ agent_summary.agent_count }}</div><strong>Agents</strong><p>{{ agent_summary.watch_degraded }} watch/degraded, {{ agent_summary.active_runs }} active runs</p></div>
</section>
<section class="grid">
<div class="panel"><h3>Recent GraphRuns</h3>{% include "control_plane/partials/graph_run_table.html" with graph_runs=recent.graph_runs %}</div>
<div class="panel"><h3>Recent Activity</h3>{% include "control_plane/partials/event_list.html" with events=recent.events %}</div>
</section>
{% endblock %}

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>Explore{% if project %} / {{ project.name }}{% endif %}</h2><p class="muted">Ranked opportunities. Explorer results never directly invoke Coder.</p></div>{% if project %}<form method="post" action="{% url 'run_explore' project.id %}">{% csrf_token %}<button type="submit">Run Explore</button></form>{% endif %}</header><table><thead><tr><th>Opportunity</th><th>Project</th><th>Type</th><th>Scores</th><th>Status</th><th>Actions</th></tr></thead><tbody>{% for item in opportunities %}<tr><td>{{ item.title }}<br><span class="muted">{{ item.description }}</span></td><td>{{ item.project.name }}</td><td>{{ item.opportunity_type }}</td><td>V {{ item.value_score }} / E {{ item.effort_score }} / R {{ item.risk_score }} / C {{ item.confidence }} / T {{ item.technical_fit }} / S {{ item.strategic_fit }}</td><td><span class="badge {{ item.status }}">{{ item.status }}</span></td><td><form class="actions" method="post" action="{% url 'opportunity_action' item.id %}">{% csrf_token %}<button name="action" value="extend">Extend</button><button name="action" value="evolve">Evolve</button><button name="action" value="roadmap">Add to Roadmap</button><button class="secondary" name="action" value="defer">Defer</button><button class="secondary" name="action" value="reject">Reject</button></form></td></tr>{% empty %}<tr><td colspan="6">No opportunities.</td></tr>{% endfor %}</tbody></table>{% endblock %}

View file

@ -0,0 +1,3 @@
{% extends "control_plane/base.html" %}
{% block head %}<meta http-equiv="refresh" content="5">{% endblock %}
{% block content %}<header class="page"><div><h2>GraphRun {{ graph_run.id }}</h2><p class="muted">Live execution graph viewer. This page auto-refreshes every 5 seconds.</p></div><a class="button secondary" href="{% url 'graph_run_json' graph_run.id %}">JSON</a></header>{% include "control_plane/partials/graph_run_status.html" %}<section class="grid"><div class="panel"><h3>Edges</h3>{% for edge in traversals %}<p>{{ edge.source_node }} → {{ edge.target_node }} <span class="badge">{{ edge.result|default:edge.condition }}</span></p>{% empty %}<p>No traversed edges.</p>{% endfor %}</div><div class="panel"><h3>Approvals</h3>{% for approval in approvals %}<p><span class="badge {{ approval.status }}">{{ approval.status }}</span> {{ approval.reason }}</p>{% empty %}<p>No approvals.</p>{% endfor %}</div></section>{% endblock %}

View file

@ -0,0 +1 @@
{% for event in events %}<p><span class="badge">{{ event.event_type }}</span> {{ event.created_at }} {% if event.project %}<a href="{% url 'project_workspace' event.project.id %}">{{ event.project.name }}</a>{% endif %}<br><span class="muted">{{ event.actor }}</span></p>{% empty %}<p>No activity.</p>{% endfor %}

View file

@ -0,0 +1 @@
<section class="panel"><h3>{{ graph_run.execution_graph_version.graph.name }} v{{ graph_run.execution_graph_version.version }} <span class="badge {{ graph_run.status }}">{{ graph_run.status }}</span></h3><p>Runtime: LangGraphRuntime / Current node: {{ graph_run.current_node }}</p><div class="timeline">{% for node in nodes %}<div class="node"><span class="badge {{ node.status }}">{{ node.status }}</span> <strong>{{ node.node_id }}</strong> visit {{ node.visit_index }}{% if node.agent_version %}<br><span class="muted">{{ node.agent_version.agent.name }} v{{ node.agent_version.version }} / {{ node.agent_version.model }}</span>{% endif %}{% if node.failure_evidence %}<pre>{{ node.failure_evidence }}</pre>{% endif %}{% if node.output_metadata %}<pre>{{ node.output_metadata }}</pre>{% endif %}</div>{% empty %}<p>No node runs.</p>{% endfor %}</div></section>

View file

@ -0,0 +1 @@
<table><thead><tr><th>Graph</th><th>Project</th><th>Status</th><th>Current Node</th><th>Updated</th></tr></thead><tbody>{% for run in graph_runs %}<tr><td><a href="{% url 'graph_run_detail' run.id %}">{{ run.execution_graph_version.graph.name }} v{{ run.execution_graph_version.version }}</a></td><td>{{ run.project.name|default:"-" }}</td><td><span class="badge {{ run.status }}">{{ run.status }}</span></td><td>{{ run.current_node }}</td><td>{{ run.updated_at }}</td></tr>{% empty %}<tr><td colspan="5">No graph runs.</td></tr>{% endfor %}</tbody></table>

View file

@ -0,0 +1 @@
{% for milestone in milestones %}<article class="card"><h4>{{ milestone.key }} / {{ milestone.title }} <span class="badge {{ milestone.status }}">{{ milestone.status }}</span></h4><p>{{ milestone.goal }}</p>{% for feature in milestone.features.all %}<div class="card"><strong>{{ feature.title }}</strong> <span class="badge {{ feature.status }}">{{ feature.status }}</span><ul>{% for task in feature.tasks.all %}<li><a href="{% url 'task_detail' task.id %}">{{ task.goal|truncatechars:120 }}</a> <span class="badge {{ task.status }}">{{ task.status }}</span></li>{% endfor %}</ul></div>{% empty %}<ul>{% for task in milestone.tasks.all %}<li><a href="{% url 'task_detail' task.id %}">{{ task.goal|truncatechars:120 }}</a> <span class="badge {{ task.status }}">{{ task.status }}</span></li>{% empty %}<li>No tasks.</li>{% endfor %}</ul>{% endfor %}</article>{% empty %}<p>No DAG materialized.</p>{% endfor %}

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>Progeny</h2><p class="muted">Signals, investigations, experiments, and improvement candidates.</p></div></header><section class="grid"><div class="panel"><h3>Grouped Inbox</h3>{% for key,count in groups %}<p><span class="badge">{{ count }}</span> {{ key }}</p>{% empty %}<p>No open groups.</p>{% endfor %}</div><div class="panel"><h3>Investigations</h3>{% for inv in investigations %}<p><span class="badge {{ inv.status }}">{{ inv.status }}</span> {{ inv.recommended_target }} / {{ inv.recommended_route }} / confidence {{ inv.confidence }}</p>{% empty %}<p>No investigations.</p>{% endfor %}</div></section><section class="panel"><h3>Signals</h3><table><thead><tr><th>Signal</th><th>Source</th><th>Project</th><th>Agent</th><th>Severity</th><th>Status</th></tr></thead><tbody>{% for signal in signals %}<tr><td>{{ signal.summary }}<br><span class="muted">{{ signal.failure_category }} / {{ signal.grouping_key }}</span></td><td>{{ signal.source }}</td><td>{{ signal.project.name|default:"-" }}</td><td>{{ signal.agent_version.agent.name|default:"-" }}</td><td><span class="badge {{ signal.severity }}">{{ signal.severity }}</span></td><td><span class="badge {{ signal.status }}">{{ signal.status }}</span></td></tr>{% empty %}<tr><td colspan="6">No signals.</td></tr>{% endfor %}</tbody></table></section><section class="panel"><h3>Replay Arena Experiments</h3>{% for experiment in experiments %}<p>{{ experiment.hypothesis }} <span class="badge {{ experiment.status }}">{{ experiment.status }}</span></p>{% empty %}<p>No experiments.</p>{% endfor %}</section>{% endblock %}

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>Project Brain / {{ project.name }}</h2><p class="muted">Project-scoped strategic decisions, plans, and artifacts.</p></div></header><section class="panel"><form method="post">{% csrf_token %}<label for="message">Conversation note / proposed decision</label><textarea id="message" name="message" rows="4" style="width:100%"></textarea><p><button type="submit">Persist Project Brain Note</button></p></form></section><section class="grid"><div class="panel"><h3>Decisions</h3>{% for decision in decisions %}<p><span class="badge">{{ decision.decision_type }}</span> {{ decision.decision }}</p>{% empty %}<p>No decisions.</p>{% endfor %}</div><div class="panel"><h3>Plans</h3>{% for plan in plans %}<p>v{{ plan.version }} {{ plan.scope|default:plan.goal }}</p>{% empty %}<p>No plans.</p>{% endfor %}</div></section>{% endblock %}

View file

@ -0,0 +1,9 @@
{% extends "control_plane/base.html" %}
{% block title %}{{ project.name }} / Artifex{% endblock %}
{% block content %}
<header class="page"><div><h2>{{ project.name }}</h2><p>{{ project.goal }}</p><p class="muted">{{ project.repository_path|default:project.repository_url }}</p></div><div class="actions"><a class="button secondary" href="{% url 'project_brain' project.id %}">Project Brain</a><a class="button secondary" href="{% url 'project_archaeologist' project.id %}">Archaeologist</a><form method="post" action="{% url 'run_explore' project.id %}">{% csrf_token %}<button type="submit">Explore</button></form></div></header>
<section class="grid"><div class="card"><strong>Status</strong><br><span class="badge {{ project.status }}">{{ project.status }}</span><p>{{ warnings|join:", "|default:"No active warnings" }}</p></div><div class="card"><strong>Current Plan</strong><p>v{{ plan.version|default:"none" }} {{ plan.scope|default:"" }}</p></div><div class="card"><strong>Tasks</strong><p>{{ tasks|length }} total</p></div><div class="card"><strong>Scenario Coverage</strong><p>{% for key,value in scenario_coverage.items %}{{ key }}: {{ value }} {% empty %}None{% endfor %}</p></div></section>
<section class="panel"><h3>Project DAG</h3>{% include "control_plane/partials/project_dag.html" %}</section>
<section class="grid"><div class="panel"><h3>Graph Runs</h3>{% include "control_plane/partials/graph_run_table.html" with graph_runs=graph_runs %}</div><div class="panel"><h3>Roadmap NOW</h3>{% for item in roadmap.NOW %}<p><a href="{% url 'project_roadmap' project.id %}">{{ item.title }}</a> <span class="badge">{{ item.target_action }}</span></p>{% empty %}<p>No NOW items.</p>{% endfor %}</div></section>
<section class="grid"><div class="panel"><h3>Latest Commits</h3>{% for commit in commits %}<p><code>{{ commit.sha|slice:":10" }}</code> {{ commit.message|truncatechars:90 }}</p>{% empty %}<p>No commits.</p>{% endfor %}</div><div class="panel"><h3>Activity</h3>{% include "control_plane/partials/event_list.html" with events=activity %}</div></section>
{% endblock %}

View file

@ -0,0 +1,8 @@
{% extends "control_plane/base.html" %}
{% block title %}Projects / Artifex{% endblock %}
{% block content %}
<header class="page"><div><h2>Projects</h2><p class="muted">Create and inspect autonomous engineering workspaces.</p></div></header>
<table><thead><tr><th>Project</th><th>Status</th><th>Repository</th><th>Tasks</th><th>Steward</th><th>Latest GraphRun</th><th>Warnings</th><th>Actions</th></tr></thead><tbody>
{% for row in rows %}<tr><td><a href="{% url 'project_workspace' row.project.id %}">{{ row.project.name }}</a><br><span class="muted">{{ row.project.goal|truncatechars:100 }}</span></td><td><span class="badge {{ row.project.status }}">{{ row.project.status }}</span></td><td>{{ row.project.repository_path|default:row.project.repository_url }}</td><td>{{ row.task_complete }}/{{ row.task_total }}</td><td>{{ row.steward_state.status|default:"not enrolled" }}<br>{{ row.open_findings }} findings</td><td>{% if row.latest_graph_run %}<a href="{% url 'graph_run_detail' row.latest_graph_run.id %}">{{ row.latest_graph_run.execution_graph_version.graph.name }} / {{ row.latest_graph_run.status }}</a>{% else %}None{% endif %}</td><td>{{ row.warnings|join:", "|default:"none" }}</td><td class="actions"><a class="button secondary" href="{% url 'project_workspace' row.project.id %}">Open</a><form method="post" action="{% url 'run_explore' row.project.id %}">{% csrf_token %}<button type="submit">Explore</button></form></td></tr>{% empty %}<tr><td colspan="8">No projects yet.</td></tr>{% endfor %}
</tbody></table>
{% endblock %}

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>Resources</h2><p class="muted">Compute/provider visibility without credentials.</p></div></header><table><thead><tr><th>Resource</th><th>Kind</th><th>Provider</th><th>Health</th><th>Roles</th><th>Recent Requests</th><th>Median Latency</th></tr></thead><tbody>{% for row in resources %}<tr><td>{{ row.resource.name }}</td><td>{{ row.resource.kind }}</td><td>{{ row.resource.provider }} {{ row.resource.config.model|default:"" }}</td><td><span class="badge {{ row.resource.health_status }}">{{ row.resource.health_status }}</span></td><td>{{ row.resource.roles }}</td><td>{{ row.request_count }}</td><td>{{ row.median_latency|default:"n/a" }}</td></tr>{% empty %}<tr><td colspan="7">No resources.</td></tr>{% endfor %}</tbody></table>{% endblock %}

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>Roadmap{% if project %} / {{ project.name }}{% endif %}</h2><p class="muted">Persistent future intent. Conversion creates lifecycle candidates, not immediate coding work.</p></div></header><section class="grid">{% for horizon,items in board.items %}<div class="panel"><h3>{{ horizon }}</h3>{% for item in items %}<article class="card"><strong>{{ item.title }}</strong><p>{{ item.description }}</p><p><span class="badge">{{ item.source }}</span> <span class="badge">{{ item.target_action }}</span> <span class="badge {{ item.status }}">{{ item.status }}</span></p><p>Composite {{ item.composite_score }} / Value {{ item.value_score }} / Effort {{ item.effort_score }} / Risk {{ item.risk_score }}</p><form class="actions" method="post" action="{% url 'roadmap_action' item.id %}">{% csrf_token %}<button name="action" value="NOW">NOW</button><button name="action" value="NEXT">NEXT</button><button name="action" value="LATER">LATER</button><button name="action" value="EXPLORING">EXPLORING</button><button name="action" value="extend">Extend</button><button name="action" value="evolve">Evolve</button><button class="secondary" name="action" value="defer">Defer</button><button class="secondary" name="action" value="reject">Reject</button></form></article>{% empty %}<p>No items.</p>{% endfor %}</div>{% endfor %}</section>{% endblock %}

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>Scenario Lab{% if project %} / {{ project.name }}{% endif %}</h2><p class="muted">Unusual, degraded, adversarial, and high-load scenario portfolio.</p></div></header><section class="grid"><div class="panel"><h3>Coverage</h3>{% for key,value in coverage.items %}<p>{{ key }}: {{ value }}</p>{% empty %}<p>No coverage yet.</p>{% endfor %}</div><div class="panel"><h3>Suites</h3>{% for suite in suites %}<p>{{ suite.name }} v{{ suite.version }} <span class="badge {{ suite.status }}">{{ suite.status }}</span> / {{ suite.project.name }}</p>{% empty %}<p>No suites.</p>{% endfor %}</div></section><section class="panel"><h3>Recent Runs</h3><table><thead><tr><th>Scenario</th><th>Project</th><th>Result</th><th>Evidence</th></tr></thead><tbody>{% for run in runs %}<tr><td>{{ run.scenario.title }}</td><td>{{ run.project.name }}</td><td><span class="badge {{ run.result }}">{{ run.result }}</span></td><td><pre>{{ run.failure_evidence }}</pre></td></tr>{% empty %}<tr><td colspan="4">No runs.</td></tr>{% endfor %}</tbody></table></section><section class="panel"><h3>Findings</h3><table><thead><tr><th>Finding</th><th>Route</th><th>Status</th><th>Action</th></tr></thead><tbody>{% for finding in findings %}<tr><td>{{ finding.title }}<br>{{ finding.summary }}</td><td>{{ finding.recommended_action }} / {{ finding.recommended_route }}</td><td><span class="badge {{ finding.status }}">{{ finding.status }}</span></td><td><form method="post" action="{% url 'scenario_finding_action' finding.id %}">{% csrf_token %}<button name="action" value="roadmap">Route / Add Roadmap</button></form></td></tr>{% empty %}<tr><td colspan="4">No findings.</td></tr>{% endfor %}</tbody></table></section>{% endblock %}

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>Steward{% if project %} / {{ project.name }}{% endif %}</h2><p class="muted">Enrollment runs, classified findings, evidence, and routed lifecycle actions.</p></div></header><table><thead><tr><th>Finding</th><th>Project</th><th>Severity</th><th>Route</th><th>Occurrences</th><th>Seen</th><th>Evidence</th></tr></thead><tbody>{% for finding in findings %}<tr><td>{{ finding.title }}<br><span class="muted">{{ finding.summary }}</span></td><td>{{ finding.project.name }}</td><td><span class="badge {{ finding.severity }}">{{ finding.severity }}</span></td><td>{{ finding.recommended_action }} / {{ finding.recommended_route }}</td><td>{{ finding.occurrence_count }}</td><td>{{ finding.first_seen|default:finding.created_at }} → {{ finding.last_seen|default:finding.updated_at }}</td><td><pre>{{ finding.evidence }}</pre></td></tr>{% empty %}<tr><td colspan="7">No Steward findings.</td></tr>{% endfor %}</tbody></table>{% endblock %}

View file

@ -0,0 +1,2 @@
{% extends "control_plane/base.html" %}
{% block content %}<header class="page"><div><h2>Task</h2><p>{{ task.goal }}</p></div><span class="badge {{ task.status }}">{{ task.status }}</span></header><section class="grid"><div class="panel"><h3>Acceptance</h3><pre>{{ task.acceptance_criteria }}</pre><h3>Dependencies</h3>{% for dep in dependencies %}<p>{{ dep.goal }}</p>{% empty %}<p>No dependencies.</p>{% endfor %}</div><div class="panel"><h3>Attempts</h3>{% for attempt in attempts %}<p>#{{ attempt.attempt_number }} <span class="badge {{ attempt.status }}">{{ attempt.status }}</span> {{ attempt.coder.agent.name }} v{{ attempt.coder.version }}</p>{% empty %}<p>No attempts.</p>{% endfor %}</div></section><section class="grid"><div class="panel"><h3>GraphRuns</h3>{% include "control_plane/partials/graph_run_table.html" with graph_runs=graph_runs %}</div><div class="panel"><h3>Tests / Reviews / Commits</h3>{% for test in tests %}<p>Test: <span class="badge {{ test.status }}">{{ test.status }}</span> {{ test.command }}</p>{% endfor %}{% for review in reviews %}<p>Review: <span class="badge {{ review.status }}">{{ review.status }}</span></p>{% endfor %}{% for commit in commits %}<p>Commit <code>{{ commit.sha|slice:":10" }}</code> {{ commit.message }}</p>{% endfor %}</div></section>{% endblock %}

View file

@ -0,0 +1,123 @@
from __future__ import annotations
from django.test import Client
from django.urls import reverse
from agents.control_room import AgentControlRoomService
from control_plane.agents.management.commands.seed_core_agents import Command as SeedAgentsCommand
from control_plane.agents.models import AgentRole, AgentVersion, PromotionStatus, ProgenySignal
from control_plane.events.bus import EventBus
from control_plane.events.models import EventType
from control_plane.projects.models import Exploration, ExplorationOpportunity, Project, ProjectPlan, Milestone, RoadmapItem, Scenario, ScenarioFinding, ScenarioRun, ScenarioSuite, StewardFinding, Task, TaskAttempt, TaskStatus
from control_plane.resources.models import Resource, ResourceKind
from control_plane.verification.models import Review, TestRun, Verification, VerificationLevel, VerificationResult
from graph.bootstrap import champion_project_exploration_graph_v1, champion_task_execution_graph_v1
from graph.models import GraphApproval, GraphRun, GraphRunStatus
def fixture() -> dict[str, object]:
SeedAgentsCommand().handle()
AgentControlRoomService().bootstrap_frontend_agents()
team = AgentControlRoomService().create_software_feature_team()
Resource.objects.create(name="Qwen", kind=ResourceKind.MODEL, provider="local_inference", roles=["CODING"], health_status="AVAILABLE", config={"model": "qwen38"})
project = Project.objects.create(name="UI Dogfood", goal="Operate Artifex through the UI", repository_path="/tmp/ui")
plan = ProjectPlan.objects.create(project=project, version=1, goal=project.goal, scope="UI V1")
milestone = Milestone.objects.create(project=project, plan=plan, key="UI", title="UI", goal="Build UI")
task = Task.objects.create(project=project, milestone=milestone, task_type="implementation", status=TaskStatus.COMPLETE, goal="Render dashboard", acceptance_criteria=["loads"])
coder = AgentVersion.objects.get(agent__role=AgentRole.CODER, promotion_status=PromotionStatus.CHAMPION)
reviewer = AgentVersion.objects.get(agent__role=AgentRole.REVIEWER, promotion_status=PromotionStatus.CHAMPION)
judge = AgentVersion.objects.get(agent__role=AgentRole.PROJECT_JUDGE, promotion_status=PromotionStatus.CHAMPION)
TaskAttempt.objects.create(task=task, attempt_number=1, coder=coder, status="COMPLETE")
TestRun.objects.create(project=project, task=task, command="pytest", status="PASS")
Review.objects.create(task=task, reviewer=reviewer, status="PASS")
Verification.objects.create(project=project, task=task, judge=judge, level=VerificationLevel.TASK, result=VerificationResult.PASS)
graph_version = champion_task_execution_graph_v1()
graph_run = GraphRun.objects.create(project=project, task=task, execution_graph_version=graph_version, current_node="review", status=GraphRunStatus.PAUSED, metadata={"telemetry": {}})
node_run = graph_run.node_runs.create(node_id="review", node_type="review", status="PAUSED", visit_index=1, agent_version=reviewer, output_metadata={"review_status": "RUNNING"})
approval = GraphApproval.objects.create(graph_run=graph_run, node_run=node_run, reason="AWAITING_UI_APPROVAL")
exploration = Exploration.objects.create(project=project, status="COMPLETE")
opportunity = ExplorationOpportunity.objects.create(exploration=exploration, project=project, title="Add status filter", description="Filter dashboard cards", opportunity_type="UX", recommended_action="EXTEND", value_score=0.8, effort_score=0.2, risk_score=0.2, confidence=0.8, technical_fit=0.8, strategic_fit=0.8, composite_score=0.75, grouping_key="ui-opp")
roadmap_item = RoadmapItem.objects.create(project=project, title="Improve graph viewer", description="Expose node details", horizon="NOW", target_action="EXTEND", grouping_key="ui-roadmap")
suite = ScenarioSuite.objects.create(project=project, name="UI Suite")
scenario = Scenario.objects.create(project=project, suite=suite, name="Permission denial", title="Permission denial", scenario_type="PERMISSION", target_type="PROJECT", target_id=str(project.id), injected_condition={"mechanism": "permission_denial"}, expected_invariants=["safe"], success_criteria=["pass"], resource_budget={"max_seconds": 5})
run = ScenarioRun.objects.create(project=project, scenario=scenario, result="FAIL", status="COMPLETE", failure_evidence={"summary": "denied"})
finding = ScenarioFinding.objects.create(project=project, scenario=scenario, scenario_run=run, title="Scenario failed", summary="permission denied", failure_category="PERMISSION", recommended_action="EXTEND", recommended_route="RoadmapItem", grouping_key="ui-scenario")
StewardFinding.objects.create(project=project, finding_type="RELIABILITY", title="Open issue", summary="Needs attention", severity="HIGH", confidence=0.8, recommended_action="REPAIR", grouping_key="ui-steward")
ProgenySignal.objects.create(project=project, agent_version=coder, source="ui", severity="MEDIUM", failure_category="MALFORMED", summary="Signal", grouping_key="ui-signal")
EventBus().publish(EventType.TASK_COMPLETED, project=project, task=task, actor="test")
return {"project": project, "task": task, "graph_run": graph_run, "approval": approval, "opportunity": opportunity, "roadmap_item": roadmap_item, "finding": finding, "coder": coder, "team": team}
def assert_ok(client: Client, url: str, text: str) -> None:
response = client.get(url)
assert response.status_code == 200
assert text.encode() in response.content
def test_control_plane_core_pages_render_and_link() -> None:
data = fixture()
client = Client()
project = data["project"]
task = data["task"]
graph_run = data["graph_run"]
coder = data["coder"]
assert_ok(client, reverse("dashboard"), "Dashboard")
assert_ok(client, reverse("projects"), "UI Dogfood")
assert_ok(client, reverse("project_workspace", args=[project.id]), "Project DAG")
assert_ok(client, reverse("project_brain", args=[project.id]), "Project Brain")
assert_ok(client, reverse("project_archaeologist", args=[project.id]), "Archaeologist")
assert_ok(client, reverse("graph_run_detail", args=[graph_run.id]), "Live execution graph viewer")
assert_ok(client, reverse("task_detail", args=[task.id]), "Acceptance")
assert_ok(client, reverse("steward"), "Open issue")
assert_ok(client, reverse("explore"), "Add status filter")
assert_ok(client, reverse("roadmap"), "Improve graph viewer")
assert_ok(client, reverse("scenarios"), "Permission denial")
assert_ok(client, reverse("progeny"), "Signal")
assert_ok(client, reverse("agent_control_room"), "Agent Control Room")
assert_ok(client, reverse("agent_detail", args=[coder.id]), "Competencies")
assert_ok(client, reverse("resources"), "Qwen")
assert_ok(client, reverse("approvals"), "AWAITING_UI_APPROVAL")
assert_ok(client, reverse("activity"), "TASK_COMPLETED")
def test_json_endpoints_and_ui_actions_work() -> None:
data = fixture()
client = Client()
project = data["project"]
graph_run = data["graph_run"]
approval = data["approval"]
opportunity = data["opportunity"]
roadmap_item = data["roadmap_item"]
finding = data["finding"]
assert client.get(reverse("graph_run_json", args=[graph_run.id])).json()["status"] == "PAUSED"
assert client.get(reverse("project_dag_json", args=[project.id])).json()["tasks"]
assert client.get(reverse("agent_performance_json", args=[data["coder"].id])).json()["quality"]
response = client.post(reverse("opportunity_action", args=[opportunity.id]), {"action": "roadmap"})
assert response.status_code == 302
assert project.roadmap_items.filter(title="Add status filter").exists()
response = client.post(reverse("roadmap_action", args=[roadmap_item.id]), {"action": "NEXT"})
assert response.status_code == 302
roadmap_item.refresh_from_db()
assert roadmap_item.horizon == "NEXT"
response = client.post(reverse("scenario_finding_action", args=[finding.id]), {"action": "roadmap"})
assert response.status_code == 302
response = client.post(reverse("approval_action", args=[approval.id]), {"action": "approve"})
assert response.status_code == 302
approval.refresh_from_db()
graph_run.refresh_from_db()
assert approval.status == "APPROVED"
assert graph_run.status == "RUNNING"
def test_ui_can_start_safe_explore_graph() -> None:
data = fixture()
client = Client()
response = client.post(reverse("run_explore", args=[data["project"].id]))
assert response.status_code == 302
assert GraphRun.objects.filter(execution_graph_version__graph__name="project_exploration", project=data["project"]).exists()

View 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

View file

@ -0,0 +1,217 @@
from __future__ import annotations
import json
from decimal import Decimal
from agents.venture_discovery import PITCH_SECTIONS, SCORE_DIMENSIONS, VentureDiscoveryService
from control_plane.ventures.models import CapabilityPriority, CapabilityStatus, CompanyProposal, CompanyProposalStatus, ICDecisionType, VentureArtifact
from graph.bootstrap import champion_venture_discovery_graph_v1
from graph.langgraph_runtime import LangGraphRuntime
from graph.models import GraphRun, GraphRunStatus
from graph.venture_discovery import venture_discovery_registry
from model_router.router import ModelProvider, ModelRequestContract, ModelResponseContract, ModelRouter
class SolOneCompanyProvider(ModelProvider):
provider_name = "sol-test"
def complete(self, request: ModelRequestContract) -> ModelResponseContract:
return ModelResponseContract(
model="sol",
content=json.dumps(
{
"title": "LaunchLens",
"one_line_thesis": "A paid validation audit helps solo technical founders avoid wasting weeks on unvalidated AI microbusinesses.",
"description": "A fixed-scope validation and launch-readiness report for one startup idea.",
"problem": "Builders overbuild before proving demand.",
"target_customer": "Solo technical founders and small service operators considering an AI-assisted microbusiness.",
"proposed_solution": "A productized audit covering ICP, first-dollar path, validation gates, build plan, risks, and capability gaps.",
"business_model": "Productized service first, optional software later.",
"pricing_hypothesis": "$49-$99 per audit.",
"acquisition_strategy": "Compliant founder community posts and personal network conversations after approval.",
"validation_plan": "Collect 5 credible target-customer responses or 1 willingness-to-pay signal before build/spend.",
"capital_requested": "50",
"time_to_first_dollar_estimate": "3-10 days after outreach approval",
"expected_margin": "70-85% gross margin",
"build_complexity": "LOW",
"market_evidence": [{"type": "reasoning", "source": "sol", "summary": "Service-led validation minimizes build risk."}],
"differentiation": "IC-style diligence plus Artifex execution/capability-gap awareness.",
"major_risks": ["Demand unproven", "Distribution may fail", "Generic consulting competition"],
"confidence": 0.66,
}
),
metadata={"usage": {"prompt_tokens": 1, "completion_tokens": 1}},
)
def health(self) -> str:
return "AVAILABLE"
class ResearchProvider(ModelProvider):
provider_name = "luna-test"
def __init__(self, complete: bool = True) -> None:
self.complete_coverage = complete
def complete(self, request: ModelRequestContract) -> ModelResponseContract:
categories = ["competitors", "pricing", "customer_pain", "market_alternatives", "regulatory_platform_risks"] if self.complete_coverage else ["competitors"]
return ModelResponseContract(
model="luna",
content=json.dumps(
{
"sources": [{"url": f"https://example.com/{category}", "title": category.replace("_", " ").title(), "category": category, "summary": f"Source-linked evidence for {category}."} for category in categories],
"findings": {category: f"Finding for {category}." for category in categories},
"coverage": {category: category in categories for category in ["competitors", "pricing", "customer_pain", "market_alternatives", "regulatory_platform_risks"]},
}
),
metadata={},
)
def health(self) -> str:
return "AVAILABLE"
def service(*, web_research_available: bool = False, complete_research: bool = True) -> VentureDiscoveryService:
return VentureDiscoveryService(ModelRouter({"sol": SolOneCompanyProvider(), "luna": ResearchProvider(complete_research)}), web_research_available=web_research_available)
def test_company_proposal_lifecycle_mandate_limits_and_pitch_schema() -> None:
svc = service()
mandate = svc.create_v0_mandate()
proposal = svc.generate_single_company(mandate)
assert mandate.max_validation_capital == Decimal("50")
assert mandate.target_net_new_cash == Decimal("500")
assert mandate.target_window_days == 30
assert mandate.constraints["no_real_spend_in_v0"] is True
assert mandate.constraints["no_real_customer_outreach_in_v0"] is True
assert CompanyProposal.objects.count() == 1
assert proposal.status == CompanyProposalStatus.SUBMITTED
assert proposal.capital_requested <= Decimal("50")
assert all(section in proposal.pitch for section in PITCH_SECTIONS)
assert proposal.metadata["generation_source"] == "sol"
assert proposal.metadata["real_spend"] == 0
assert proposal.metadata["real_customer_outreach"] is False
def test_board_review_ic_questions_responses_and_bounded_diligence() -> None:
svc = service()
proposal = svc.generate_single_company(svc.create_v0_mandate())
board = svc.board_review(proposal)
diligence = svc.start_ic_diligence(proposal)
questions = svc.generate_ic_questions(diligence)
responses = svc.answer_questions(diligence)
challenge = svc.red_team(diligence)
final = svc.final_company_response(diligence)
assert set(board.observations) == {"CEO", "CTO", "CFO", "CRO", "Independent Director"}
assert board.metadata["company_does_not_grade_itself"] is True
assert diligence.rounds == ["Initial Pitch", "Diligence Round 1", "Final Challenge", "Decision"]
assert diligence.metadata["bounded_rounds"] is True
assert len(questions) == 8
assert all(q.evidence_required for q in questions)
assert len(responses) == len(questions)
assert all(r.metadata["no_customer_outreach"] and r.metadata["no_spend"] for r in responses)
assert challenge["recommendation"] == "continue_to_final_response"
assert final["kill_criteria"]
def test_ic_scoring_decision_capability_gap_and_memo_artifact() -> None:
svc = service(web_research_available=True)
proposal = svc.generate_single_company(svc.create_v0_mandate())
research = svc.conduct_market_research(proposal)
svc.board_review(proposal)
diligence = svc.start_ic_diligence(proposal)
svc.generate_ic_questions(diligence)
svc.answer_questions(diligence)
svc.red_team(diligence)
svc.final_company_response(diligence)
decision = svc.score_and_decide(diligence)
gap = svc.capability_analysis(proposal)
memo = svc.produce_investment_memo(diligence, gap)
assert decision.decision in ICDecisionType.values
assert set(SCORE_DIMENSIONS) == set(decision.component_scores)
assert decision.decision == ICDecisionType.CONDITIONAL_FUND
assert decision.initial_tranche in {Decimal("10.00"), Decimal("20.00")}
assert "5 credible target-customer responses" in decision.validation_condition
assert decision.metadata["no_actual_funding"] is True
assert len(research["sources"]) == 5
assert "WEB_MARKET_RESEARCH" in gap.available
assert gap.metadata["web_market_research_status"] == "AVAILABLE"
assert {item["priority"] for item in gap.ranked_missing}.issuperset({CapabilityPriority.BEFORE_VALIDATION, CapabilityPriority.BEFORE_FIRST_CUSTOMER})
assert memo.artifact_type == "FINAL_INVESTMENT_MEMO"
assert memo.content["Company"] == proposal.title
assert "Capability gap" in memo.content
def test_fallback_is_marked_and_reduces_confidence_when_research_unavailable() -> None:
svc = VentureDiscoveryService(web_research_available=False)
proposal = svc.generate_single_company(svc.create_v0_mandate())
gap = svc.capability_analysis(proposal)
assert proposal.metadata["generation_source"] == "deterministic_fallback"
assert proposal.metadata["fallback_evidence"] is True
assert proposal.confidence <= 0.58
assert any(item.get("fallback_evidence") for item in proposal.market_evidence)
assert "WEB_MARKET_RESEARCH" in gap.missing
def test_venture_discovery_v1_graph_lineage_and_no_automatic_execution() -> None:
version = champion_venture_discovery_graph_v1()
graph_run = GraphRun.objects.create(execution_graph_version=version, current_node=version.graph_spec["entry"])
LangGraphRuntime(venture_discovery_registry(service())).run_until_terminal_or_paused(graph_run)
graph_run.refresh_from_db()
assert graph_run.status == GraphRunStatus.COMPLETE
assert graph_run.execution_graph_version.graph.name == "venture_discovery"
assert graph_run.execution_graph_version.version == 1
assert CompanyProposal.objects.count() == 1
proposal = CompanyProposal.objects.get()
assert proposal.metadata["real_spend"] == 0
assert proposal.metadata["real_customer_outreach"] is False
assert graph_run.node_runs.count() == 14
assert graph_run.edge_traversals.count() == 14
artifact_types = set(VentureArtifact.objects.values_list("artifact_type", flat=True))
assert {"STANDARDIZED_COMPANY_PITCH", "MARKET_RESEARCH", "COMPANY_BOARD_REVIEW", "IC_QUESTIONS", "IC_RESPONSES", "IC_RED_TEAM", "IC_FINAL_RESPONSE", "IC_FINAL_SCORE", "CAPABILITY_GAP_REPORT", "FINAL_INVESTMENT_MEMO"}.issubset(artifact_types)
decision = proposal.ic_diligence.get().decision
svc = service()
svc.request_human_approval(decision, "approve_for_validation")
decision.refresh_from_db()
assert decision.metadata["real_spend_still_blocked"] is True
def test_materially_different_pitches_receive_different_scores_and_tranches() -> None:
strong = service(web_research_available=True, complete_research=True)
strong_proposal = strong.generate_single_company(strong.create_v0_mandate())
strong.conduct_market_research(strong_proposal)
strong_diligence = strong.start_ic_diligence(strong_proposal)
strong.generate_ic_questions(strong_diligence)
strong.answer_questions(strong_diligence)
strong.red_team(strong_diligence)
strong.final_company_response(strong_diligence)
strong_decision = strong.score_and_decide(strong_diligence)
weak = service(web_research_available=True, complete_research=False)
weak_proposal = weak.generate_single_company(weak.create_v0_mandate())
weak_proposal.title = "Enterprise RegTech Platform"
weak_proposal.business_model = "SaaS platform requiring integrations and long enterprise sales cycles."
weak_proposal.build_complexity = "HIGH"
weak_proposal.expected_margin = "35-45% gross margin before support burden"
weak_proposal.validation_plan = "Build a prototype and then seek feedback."
weak_proposal.pitch = {**weak_proposal.pitch, "Company name": weak_proposal.title, "Business model": weak_proposal.business_model, "Validation plan": weak_proposal.validation_plan}
weak_proposal.save(update_fields=["title", "business_model", "build_complexity", "expected_margin", "validation_plan", "pitch", "updated_at"])
weak.conduct_market_research(weak_proposal)
weak_diligence = weak.start_ic_diligence(weak_proposal)
weak.generate_ic_questions(weak_diligence)
weak.answer_questions(weak_diligence)
weak.red_team(weak_diligence)
weak.final_company_response(weak_diligence)
weak_decision = weak.score_and_decide(weak_diligence)
assert strong_decision.component_scores["Demand Evidence"] > weak_decision.component_scores["Demand Evidence"]
assert strong_decision.component_scores["Build Simplicity"] > weak_decision.component_scores["Build Simplicity"]
assert strong_decision.composite_score - weak_decision.composite_score >= 10
assert strong_decision.probability_500_within_30_days > weak_decision.probability_500_within_30_days
assert strong_decision.initial_tranche != weak_decision.initial_tranche