Codex
Deploy submission links to Hugging Face Space
200cb0b
Raw
History Blame Contribute Delete
47.4 kB
"""Core pressure and distillation functions for Iris."""
from __future__ import annotations
from dataclasses import dataclass
from difflib import SequenceMatcher
import re
from typing import Protocol
from iris.config import IrisConfig
from iris.errors import IrisResponseError
from iris.http_client import ChatCompletionsClient
from iris.parser import parse_json_object, parse_json_object_with_key, require_string
from iris.prompts import (
DISTILL_SYSTEM,
DIRECTION_PROFILES,
DIRECTION_STYLES,
DIRECTION_SET_PRESSURE_SYSTEM,
PRESSURE_REPAIR_SYSTEM,
PRESSURE_SYSTEM,
REFINE_SYSTEM,
RING_PROFILES,
DIRECTION_PRESSURE_SYSTEM,
WHY_BITE_SYSTEM,
direction_set_pressure_user_prompt,
direction_pressure_user_prompt,
distill_user_prompt,
pressure_repair_user_prompt,
pressure_user_prompt,
refine_idea_user_prompt,
why_bite_user_prompt,
)
MAX_MODEL_ATTEMPTS = 4
# The live canvas soft-accepts the best-effort card, so extra retries mostly add
# latency. Cap direction retries lower in soft mode to keep Proceed responsive.
DIRECTION_SOFT_ATTEMPTS = 2
# The live Space uses one combined generation for all four direction cards.
# Retrying that whole payload on CPU Basic costs minutes; if the first answer is
# imperfect, soft mode shows the best clean result instead of stalling the demo.
DIRECTION_SET_SOFT_ATTEMPTS = 1
PRESSURE_REPEAT_THRESHOLD = 0.68
DIRECTION_NAMES = tuple(DIRECTION_PROFILES.keys())
PRESSURE_ALIASES = (
"constraint",
"question",
"pressure_question",
"pressure question",
)
WHY_IT_BITES_ALIASES = (
"why it bites",
"why_it_bits",
"why it bits",
"why this bites",
"why it matters",
"why",
"reason",
"rationale",
"stakes",
"risk",
"bite",
)
WEAK_ALTERNATIVE_FILLS = (
"none",
"nothing",
"unknown",
"n/a",
"the app",
"app",
"the tool",
"tool",
"platform",
"marketplace",
"failure",
"risk",
"problem",
)
# Small local models sometimes echo the prompt's fill-in template instead of an
# answer, e.g. "What hard [Constraint] does [this] collide with?". Square/angle
# bracketed placeholders are the clean signal; real pressures don't use them.
TEMPLATE_LEAK_RE = re.compile(r"\[[^\]]*[A-Za-z][^\]]*\]|<[^>]*[A-Za-z][^>]*>")
def _has_template_leak(*texts: str | None) -> bool:
return any(bool(text) and bool(TEMPLATE_LEAK_RE.search(text)) for text in texts)
BANNED_PRESSURE_PHRASES = (
"the app needs",
"the app must",
"the tool needs",
"the tool must",
"user adoption",
"market fit",
"user-friendly",
"seamless user experience",
"seamless experience",
"existing workarounds",
"low adoption",
"implement user adoption",
"adoption strategies",
"why does the app fail",
"why does the tool fail",
"solution",
"solutions",
"feature",
"features",
"strategy",
"strategies",
"practical or safe",
)
WHY_ADVICE_PHRASES = (
"should",
"need for",
"need to",
"needs to",
"must",
"recommend",
"recommends",
"recommendation",
"incorporate",
"feature",
"features",
"solution",
"solutions",
"guidance",
"strategy",
"strategies",
"implement",
"develop",
"design",
)
BANNED_CENTER_WORDS = (
"implement",
"design",
"build",
"add",
"integrate",
"develop",
"launch",
"create",
)
WEAK_CENTER_FILLS = (
"interview",
"call",
"ask",
"watch",
"observe",
"test",
"visit",
"find",
"send",
"user",
"users",
"people",
"person",
"someone",
"customer",
"customers",
"stakeholder",
"stakeholders",
)
class CompletionClient(Protocol):
def complete(
self, messages: list[dict[str, str]], temperature: float | None = None
) -> str:
...
# Local small models can get stuck echoing the prompt at temperature 0, so each
# retry nudges the temperature up to break the deterministic loop.
RETRY_TEMPERATURES = (0.0, 0.5, 0.8, 1.0)
def _retry_temperature(attempt: int) -> float | None:
if attempt <= 0:
return None
index = min(attempt, len(RETRY_TEMPERATURES) - 1)
return RETRY_TEMPERATURES[index]
@dataclass(frozen=True)
class PressureResult:
pressure: str
why_it_bites: str
raw: str
alternative: str | None = None
def as_constraint(self) -> str:
if self.alternative:
return (
f"{self.pressure} Alternative: {self.alternative}. "
f"Why it bites: {self.why_it_bites}"
)
return f"{self.pressure} Why it bites: {self.why_it_bites}"
@dataclass(frozen=True)
class DirectionPressureResult:
direction: str
pressure: str
why_it_bites: str
raw: str
def as_constraint(self) -> str:
return (
f"{self.direction}: {self.pressure} "
f"Why it bites: {self.why_it_bites}"
)
@dataclass(frozen=True)
class DistillResult:
actor: str
situation: str
assumption_to_test: str
next_step: str
raw: str
@dataclass(frozen=True)
class FinalBrief:
refined_idea: str
center: "DistillResult | None"
class IrisEngine:
def __init__(
self,
client: CompletionClient | None = None,
config: IrisConfig | None = None,
):
self.config = config or IrisConfig.from_env()
self.client = client or ChatCompletionsClient(self.config)
def pressure(
self, idea: str, prior_constraints: list[str], depth: int, total: int
) -> PressureResult:
if depth < 1 or total < 1 or depth > total:
raise ValueError("depth must be between 1 and total")
feedback: str | None = None
last_result: PressureResult | None = None
last_error: IrisResponseError | None = None
for _attempt in range(MAX_MODEL_ATTEMPTS):
try:
result = self._pressure_once(
idea=idea,
prior_constraints=prior_constraints,
depth=depth,
total=total,
rejection_feedback=feedback,
)
except IrisResponseError as exc:
last_error = exc
feedback = str(exc)
continue
quality_feedback = _pressure_quality_feedback(
result, prior_constraints, idea, depth
)
if quality_feedback is None:
return result
last_result = result
feedback = quality_feedback
if last_result is not None:
return last_result
if last_error is not None:
raise last_error
raise IrisResponseError("Model did not return pressure output")
def pressure_directions(
self,
idea: str,
prior_constraints: list[str],
depth: int,
total: int,
*,
allow_soft_failures: bool = False,
conversation: list[dict[str, str]] | None = None,
) -> list[DirectionPressureResult]:
if depth < 1 or total < 1 or depth > total:
raise ValueError("depth must be between 1 and total")
if allow_soft_failures and isinstance(self.client, ChatCompletionsClient):
return self._pressure_direction_set(
idea=idea,
prior_constraints=prior_constraints,
depth=depth,
total=total,
conversation=conversation,
)
results: list[DirectionPressureResult] = []
for direction in DIRECTION_NAMES:
direction_constraints = prior_constraints + [
result.as_constraint() for result in results
]
results.append(
self._pressure_direction(
idea=idea,
prior_constraints=direction_constraints,
depth=depth,
total=total,
direction=direction,
allow_soft_failure=allow_soft_failures,
conversation=conversation,
)
)
return results
def _pressure_once(
self,
idea: str,
prior_constraints: list[str],
depth: int,
total: int,
rejection_feedback: str | None,
) -> PressureResult:
raw = self.client.complete(
[
{"role": "system", "content": PRESSURE_SYSTEM},
{
"role": "user",
"content": pressure_user_prompt(
idea=idea,
prior_constraints=prior_constraints,
depth=depth,
total=total,
enable_thinking=self.config.enable_thinking,
rejection_feedback=rejection_feedback,
),
},
]
)
data = parse_json_object_with_key(
raw, "pressure", aliases=PRESSURE_ALIASES
)
try:
pressure_text = require_string(data, "pressure", aliases=PRESSURE_ALIASES)
try:
why_text = require_string(
data, "why_it_bites", aliases=WHY_IT_BITES_ALIASES
)
except IrisResponseError:
pressure_text, why_text = _split_inline_why(pressure_text)
else:
if re.search(
r"\b(?:why\s+it\s+bites\s*:|this\s+bites\s+because)\s*",
pressure_text,
re.IGNORECASE,
):
pressure_text, inline_why = _split_inline_why(pressure_text)
why_text = inline_why or why_text
alternative_text = _optional_string(
data,
"alternative",
aliases=(
"current_alternative",
"existing_alternative",
"workaround",
"fallback",
"today_alternative",
),
)
return PressureResult(
pressure=pressure_text,
why_it_bites=why_text,
raw=raw,
alternative=alternative_text,
)
except IrisResponseError as exc:
raise IrisResponseError(f"{exc}; raw response: {raw[:500]}") from exc
def _pressure_direction(
self,
idea: str,
prior_constraints: list[str],
depth: int,
total: int,
direction: str,
allow_soft_failure: bool = False,
conversation: list[dict[str, str]] | None = None,
) -> DirectionPressureResult:
feedback: str | None = None
last_result: DirectionPressureResult | None = None
last_error: IrisResponseError | None = None
attempts = DIRECTION_SOFT_ATTEMPTS if allow_soft_failure else MAX_MODEL_ATTEMPTS
for _attempt in range(attempts):
try:
result = self._pressure_direction_once(
idea=idea,
prior_constraints=prior_constraints,
depth=depth,
total=total,
direction=direction,
rejection_feedback=feedback,
temperature=_retry_temperature(_attempt),
conversation=conversation,
)
except IrisResponseError as exc:
last_error = exc
feedback = str(exc)
continue
quality_feedback = _single_direction_pressure_quality_feedback(
result, prior_constraints, idea
)
if quality_feedback is None:
return result
last_result = result
feedback = quality_feedback
if last_result is not None and feedback is not None:
# In soft mode prefer a best-effort card over throwing away the whole
# turn, BUT only if the card is clean. A result that still leaks a
# bracketed template placeholder is worse than the honest fallback.
if allow_soft_failure:
if _has_template_leak(
last_result.pressure, last_result.why_it_bites
):
return _direction_fallback(direction, last_error)
return last_result
raise IrisResponseError(
f"{direction} direction failed quality gate: {feedback}"
)
if last_result is not None:
return last_result
if allow_soft_failure:
return _direction_fallback(direction, last_error)
if last_error is not None:
raise last_error
raise IrisResponseError(
f"Model did not return {direction} direction pressure output"
)
def _pressure_direction_set(
self,
idea: str,
prior_constraints: list[str],
depth: int,
total: int,
conversation: list[dict[str, str]] | None = None,
) -> list[DirectionPressureResult]:
feedback: str | None = None
last_results: list[DirectionPressureResult] | None = None
last_error: IrisResponseError | None = None
for attempt in range(DIRECTION_SET_SOFT_ATTEMPTS):
try:
results = self._pressure_direction_set_once(
idea=idea,
prior_constraints=prior_constraints,
depth=depth,
total=total,
rejection_feedback=feedback,
temperature=_retry_temperature(attempt),
conversation=conversation,
)
except IrisResponseError as exc:
last_error = exc
feedback = str(exc)
continue
quality_feedback = [
item
for result in results
for item in [
_single_direction_pressure_quality_feedback(
result, prior_constraints, idea
)
]
if item is not None
]
if not quality_feedback:
return results
last_results = results
feedback = "; ".join(quality_feedback[:4])
if last_results is not None:
return [
result
if not _has_template_leak(result.pressure, result.why_it_bites)
else _direction_fallback(result.direction, last_error)
for result in last_results
]
return [
_direction_fallback(direction, last_error)
for direction in DIRECTION_NAMES
]
def _pressure_direction_set_once(
self,
idea: str,
prior_constraints: list[str],
depth: int,
total: int,
rejection_feedback: str | None,
temperature: float | None = None,
conversation: list[dict[str, str]] | None = None,
) -> list[DirectionPressureResult]:
messages = [{"role": "system", "content": DIRECTION_SET_PRESSURE_SYSTEM}]
if conversation:
messages.extend(conversation)
messages.append(
{
"role": "user",
"content": direction_set_pressure_user_prompt(
idea=idea,
prior_constraints=prior_constraints,
depth=depth,
total=total,
enable_thinking=self.config.enable_thinking,
rejection_feedback=rejection_feedback,
),
}
)
raw = self.client.complete(messages, temperature=temperature)
data = parse_json_object(raw)
cards = data.get("cards")
if not isinstance(cards, list):
raise IrisResponseError(f"Expected cards list; raw response: {raw[:500]}")
by_direction: dict[str, DirectionPressureResult] = {}
for card in cards:
if not isinstance(card, dict):
raise IrisResponseError(f"Expected card objects; raw response: {raw[:500]}")
try:
direction = require_string(card, "direction")
if direction not in DIRECTION_NAMES:
raise IrisResponseError(f"Unexpected direction: {direction}")
pressure_text, why_text = _extract_direction_pressure_fields(card)
if pressure_text is None or why_text is None:
raise IrisResponseError(
f"Expected pressure and why_it_bites for {direction}"
)
by_direction[direction] = DirectionPressureResult(
direction=direction,
pressure=pressure_text,
why_it_bites=why_text,
raw=raw,
)
except IrisResponseError as exc:
raise IrisResponseError(f"{exc}; raw response: {raw[:500]}") from exc
missing = [direction for direction in DIRECTION_NAMES if direction not in by_direction]
if missing:
raise IrisResponseError(
f"Missing direction cards: {', '.join(missing)}; raw response: {raw[:500]}"
)
return [by_direction[direction] for direction in DIRECTION_NAMES]
def _pressure_direction_once(
self,
idea: str,
prior_constraints: list[str],
depth: int,
total: int,
direction: str,
rejection_feedback: str | None,
temperature: float | None = None,
conversation: list[dict[str, str]] | None = None,
) -> DirectionPressureResult:
messages = [{"role": "system", "content": DIRECTION_PRESSURE_SYSTEM}]
if conversation:
messages.extend(conversation)
messages.append(
{
"role": "user",
"content": direction_pressure_user_prompt(
idea=idea,
prior_constraints=prior_constraints,
depth=depth,
total=total,
direction=direction,
enable_thinking=self.config.enable_thinking,
rejection_feedback=rejection_feedback,
),
}
)
raw = self.client.complete(messages, temperature=temperature)
data = parse_json_object_with_key(
raw, "pressure", aliases=PRESSURE_ALIASES
)
try:
pressure_text, why_text = _extract_direction_pressure_fields(data)
if pressure_text is None:
pressure_text = self._repair_direction_pressure_question(
idea=idea,
prior_constraints=prior_constraints,
direction=direction,
why_it_bites=why_text,
)
if why_text is None:
why_text = self._repair_direction_why_bite(
idea=idea,
direction=direction,
pressure=pressure_text,
)
return DirectionPressureResult(
direction=direction,
pressure=pressure_text,
why_it_bites=why_text,
raw=raw,
)
except IrisResponseError as exc:
raise IrisResponseError(f"{exc}; raw response: {raw[:500]}") from exc
def _repair_direction_pressure_question(
self,
idea: str,
prior_constraints: list[str],
direction: str,
why_it_bites: str | None,
) -> str:
raw = self.client.complete(
[
{"role": "system", "content": PRESSURE_REPAIR_SYSTEM},
{
"role": "user",
"content": pressure_repair_user_prompt(
idea=idea,
prior_constraints=prior_constraints,
direction=direction,
why_it_bites=why_it_bites,
enable_thinking=self.config.enable_thinking,
),
},
]
)
data = parse_json_object_with_key(
raw, "pressure", aliases=PRESSURE_ALIASES
)
try:
return require_string(data, "pressure", aliases=PRESSURE_ALIASES)
except IrisResponseError as exc:
raise IrisResponseError(f"{exc}; raw response: {raw[:500]}") from exc
def _repair_direction_why_bite(
self,
idea: str,
direction: str,
pressure: str,
) -> str:
raw = self.client.complete(
[
{"role": "system", "content": WHY_BITE_SYSTEM},
{
"role": "user",
"content": why_bite_user_prompt(
idea=idea,
direction=direction,
pressure=pressure,
enable_thinking=self.config.enable_thinking,
),
},
]
)
data = parse_json_object_with_key(
raw, "why_it_bites", aliases=WHY_IT_BITES_ALIASES
)
try:
return _require_why_it_bites(data)
except IrisResponseError as exc:
raise IrisResponseError(f"{exc}; raw response: {raw[:500]}") from exc
def distill(self, idea: str, all_constraints: list[str]) -> DistillResult:
feedback: str | None = None
last_result: DistillResult | None = None
last_error: IrisResponseError | None = None
for _attempt in range(MAX_MODEL_ATTEMPTS):
try:
result = self._distill_once(
idea=idea,
all_constraints=all_constraints,
rejection_feedback=feedback,
)
except IrisResponseError as exc:
last_error = exc
feedback = str(exc)
continue
quality_feedback = _distill_quality_feedback(result, idea)
if quality_feedback is None:
return result
last_result = result
feedback = quality_feedback
if last_result is not None:
return last_result
if last_error is not None:
raise last_error
raise IrisResponseError("Model did not return distillation output")
def finalize(
self,
idea: str,
all_constraints: list[str],
*,
conversation: list[dict[str, str]] | None = None,
) -> FinalBrief:
"""Synthesize the final brief. Always returns; never crashes the turn."""
refined = self._refine_idea(idea, all_constraints, conversation)
try:
center: DistillResult | None = self.distill(idea, all_constraints)
except IrisResponseError:
center = None
return FinalBrief(refined_idea=refined, center=center)
def _refine_idea(
self,
idea: str,
all_constraints: list[str],
conversation: list[dict[str, str]] | None,
) -> str:
fallback = _current_iteration_text(idea) or idea
for attempt in range(DIRECTION_SOFT_ATTEMPTS):
messages = [{"role": "system", "content": REFINE_SYSTEM}]
if conversation:
messages.extend(conversation)
messages.append(
{
"role": "user",
"content": refine_idea_user_prompt(
idea=idea,
all_constraints=all_constraints,
enable_thinking=self.config.enable_thinking,
),
}
)
try:
raw = self.client.complete(
messages, temperature=_retry_temperature(attempt)
)
data = parse_json_object_with_key(
raw, "refined_idea", aliases=("refined", "idea", "summary")
)
return require_string(
data, "refined_idea", aliases=("refined", "summary")
)
except IrisResponseError:
continue
return fallback
def _distill_once(
self,
idea: str,
all_constraints: list[str],
rejection_feedback: str | None,
) -> DistillResult:
raw = self.client.complete(
[
{"role": "system", "content": DISTILL_SYSTEM},
{
"role": "user",
"content": distill_user_prompt(
idea=idea,
all_constraints=all_constraints,
enable_thinking=self.config.enable_thinking,
rejection_feedback=rejection_feedback,
),
},
]
)
data = parse_json_object(raw)
try:
actor = require_string(
data,
"actor",
aliases=("person", "role", "real_actor", "participant"),
)
situation = require_string(
data,
"situation",
aliases=("moment", "scenario", "behavior", "context"),
)
assumption_to_test = require_string(
data,
"assumption_to_test",
aliases=(
"assumption",
"load_bearing_assumption",
"test_assumption",
"risk_to_test",
),
)
return DistillResult(
actor=actor,
situation=situation,
assumption_to_test=assumption_to_test,
next_step=_format_center_action(
actor=actor,
situation=situation,
assumption_to_test=assumption_to_test,
),
raw=raw,
)
except IrisResponseError as exc:
raise IrisResponseError(f"{exc}; raw response: {raw[:500]}") from exc
def pressure(
idea: str, prior_constraints: list[str], depth: int, total: int
) -> PressureResult:
return IrisEngine().pressure(idea, prior_constraints, depth, total)
def pressure_directions(
idea: str, prior_constraints: list[str], depth: int, total: int
) -> list[DirectionPressureResult]:
return IrisEngine().pressure_directions(idea, prior_constraints, depth, total)
def distill(idea: str, all_constraints: list[str]) -> DistillResult:
return IrisEngine().distill(idea, all_constraints)
def _split_inline_why(pressure_text: str) -> tuple[str, str]:
match = re.search(
r"\b(?:why\s+it\s+bites\s*:|this\s+bites\s+because|because)\s*",
pressure_text,
re.IGNORECASE,
)
if not match:
raise IrisResponseError("Expected non-empty string field: why_it_bites")
pressure = pressure_text[: match.start()].strip()
why = pressure_text[match.end() :].strip()
if not pressure or not why:
raise IrisResponseError("Expected non-empty string field: why_it_bites")
return pressure, why
def _direction_fallback(
direction: str, last_error: IrisResponseError | None = None
) -> DirectionPressureResult:
# Soft mode (live canvas): never crash the whole turn, and never show
# leaked/garbage text. When the local model cannot return a clean answer,
# surface an honest, readable card so the rest of the stack can render.
return DirectionPressureResult(
direction=direction,
pressure=(
f"What is the sharpest {direction.lower()} pressure on this "
"idea right now?"
),
why_it_bites=(
"The local model could not return a clean answer for this "
"phrasing. Rephrase the idea a little and proceed again."
),
raw=str(last_error) if last_error else "",
)
def _pressure_quality_feedback(
result: PressureResult, prior_constraints: list[str], idea: str, depth: int
) -> str | None:
pressure = result.pressure.strip()
normalized = _normalize(pressure)
idea_keywords = _idea_grounding_keywords(idea)
profile = RING_PROFILES.get(depth)
if "?" not in pressure:
return "Pressure must be a hard question ending with a question mark."
if _has_template_leak(result.pressure, result.why_it_bites):
return (
"Output leaked a fill-in placeholder like [Constraint] or [this]. "
"Write the actual question and bite with no bracketed placeholders."
)
if profile:
required_opening = str(profile["required_opening"])
if not normalized.startswith(required_opening.lower()):
return (
f'Ring {depth} pressure must start exactly with "{required_opening}" '
f'to satisfy the {profile["name"]} angle.'
)
if idea_keywords and not _has_keyword_match(normalized, idea_keywords):
return (
"Pressure invented an unrelated situation. It must use at least one "
f"concrete word from the idea: {', '.join(sorted(idea_keywords)[:6])}."
)
for phrase in BANNED_PRESSURE_PHRASES:
if phrase in normalized:
return f'Pressure used banned generic or solution phrase: "{phrase}".'
if any(word in normalized for word in ("implement ", "design ", "build ", "add ")):
return "Pressure proposed implementation instead of applying pressure."
alternative_feedback = _alternative_quality_feedback(
result=result,
prior_constraints=prior_constraints,
idea=idea,
depth=depth,
)
if alternative_feedback is not None:
return alternative_feedback
why_advice = advice_language_phrase(result.why_it_bites)
if why_advice is not None:
return (
f'why_it_bites used recommendation language: "{why_advice}". '
"Return a risk-only bite that explains the stakes without saying "
"what to build, add, change, or recommend."
)
for prior in prior_constraints:
prior_pressure = _constraint_pressure_text(prior)
if (
SequenceMatcher(None, normalized, _normalize(prior_pressure)).ratio()
>= PRESSURE_REPEAT_THRESHOLD
):
if profile and profile.get("requires_alternative"):
return (
"Existing Alternative pressure repeats a prior failure frame. "
f'It is too close to this earlier pressure: "{prior_pressure}". '
"Rewrite Ring 3 around the underlying job people need done, "
"not the earlier concrete failure scene. Use the "
"model-chosen alternative to attack what people already do today."
)
return "Pressure repeats a prior ring instead of escalating."
return None
def _alternative_quality_feedback(
result: PressureResult,
prior_constraints: list[str],
idea: str,
depth: int,
) -> str | None:
profile = RING_PROFILES.get(depth, {})
if not profile.get("requires_alternative"):
return None
if not result.alternative:
return (
"Existing Alternative ring must include an alternative field. "
"The model must choose the current workaround, behavior, tool, "
"place, or social fallback people use today."
)
alternative = result.alternative.strip()
normalized_alternative = _normalize(alternative)
if normalized_alternative in WEAK_ALTERNATIVE_FILLS:
return (
f'Alternative is too weak or product-shaped: "{alternative}". '
"Name the concrete current workaround, behavior, tool, place, or "
"social fallback instead."
)
if advice_language_phrase(alternative) is not None:
return (
"Alternative used recommendation language. Name what people already "
"use today, not what they should do."
)
if re.search(r"\bbecause\b", _normalize(result.pressure)):
return (
"Existing Alternative pressure must not explain a failure cause with "
'"because". Ask about the underlying job people need done and the '
"current workaround they use today."
)
proposed_product = _proposed_product_reference(result.pressure)
if proposed_product is not None:
return (
f'Existing Alternative pressure referenced the proposed product as "{proposed_product}". '
"Ask what people do today without relying on the proposed product."
)
if _alternative_copied_prior_failure(alternative, prior_constraints):
return (
f'Alternative "{alternative}" repeats a prior failure object or frame. '
"Choose the current workaround/tool/behavior people use today."
)
idea_keywords = _keywords(idea)
if normalized_alternative in idea_keywords:
return (
f'Alternative "{alternative}" is only an idea keyword. Name the '
"actual current workaround, tool, behavior, place, or social fallback."
)
return None
def _single_direction_pressure_quality_feedback(
result: DirectionPressureResult, prior_constraints: list[str], idea: str
) -> str | None:
pressure = result.pressure.strip()
normalized = _normalize(pressure)
idea_keywords = _keywords(idea)
current_iteration_keywords = _current_iteration_keywords(idea)
if "?" not in pressure or not pressure.rstrip().endswith("?"):
return (
f"{result.direction} pressure must be one hard question ending with "
"a question mark."
)
if _has_template_leak(result.pressure, result.why_it_bites):
return (
f"{result.direction} output leaked a fill-in placeholder like "
"[Constraint] or [this]. Write the actual question and bite with no "
"bracketed placeholders."
)
direction_opening_feedback = _direction_opening_feedback(
result.direction, normalized
)
if direction_opening_feedback is not None:
return direction_opening_feedback
if idea_keywords and not _has_keyword_match(normalized, idea_keywords):
return (
f"{result.direction} pressure is not grounded in the idea. Use at "
"least one concrete word from the idea: "
f"{', '.join(sorted(idea_keywords)[:6])}."
)
if current_iteration_keywords and not _has_keyword_match(
normalized, current_iteration_keywords
):
return (
f"{result.direction} pressure ignored the current iteration. Use at "
"least one concrete word from Current iteration: "
f"{', '.join(sorted(current_iteration_keywords)[:6])}."
)
for phrase in BANNED_PRESSURE_PHRASES:
if phrase in normalized:
return (
f'{result.direction} pressure used banned generic or solution '
f'phrase: "{phrase}".'
)
if any(word in normalized for word in ("implement ", "design ", "build ", "add ")):
return (
f"{result.direction} pressure proposed implementation instead of "
"applying pressure."
)
why_advice = advice_language_phrase(result.why_it_bites)
if why_advice is not None:
return (
f'{result.direction} why_it_bites used recommendation language: '
f'"{why_advice}". Return a risk-only bite that explains the stakes '
"without saying what to build, add, change, or recommend."
)
for prior in prior_constraints:
prior_pressure = _constraint_pressure_text(prior)
if (
SequenceMatcher(None, normalized, _normalize(prior_pressure)).ratio()
>= PRESSURE_REPEAT_THRESHOLD
):
return (
f"{result.direction} pressure repeats a prior pressure instead "
"of opening a new direction."
)
return None
def _direction_opening_feedback(direction: str, normalized_pressure: str) -> str | None:
opening = DIRECTION_STYLES[direction]["opening"].lower()
if normalized_pressure.startswith(opening):
return None
return (
f'{direction} pressure must start with "{DIRECTION_STYLES[direction]["opening"]}" '
"so each card stays in its assigned direction."
)
def _distill_quality_feedback(result: DistillResult, idea: str) -> str | None:
fields = {
"actor": result.actor,
"situation": result.situation,
"assumption_to_test": result.assumption_to_test,
}
for field_name, value in fields.items():
normalized = _normalize(value)
if normalized in WEAK_CENTER_FILLS:
return (
f'Center field "{field_name}" is too weak: "{value}". '
"The model must choose a concrete actor, situation, and assumption."
)
for word in BANNED_CENTER_WORDS:
if re.search(rf"\b{re.escape(word)}\b", normalized):
return (
f'Center field "{field_name}" used banned implementation '
f'verb: "{word}".'
)
advice = advice_language_phrase(value)
if advice is not None:
return (
f'Center field "{field_name}" used recommendation language: '
f'"{advice}". Choose a validation assumption, not a solution.'
)
if len(result.situation.split()) < 4:
return (
"Center situation is too short; name the real moment or behavior "
"the human can validate this week."
)
if len(result.assumption_to_test.split()) < 5:
return (
"Center assumption_to_test is too short; name the assumption whose "
"failure would weaken the idea."
)
idea_keywords = _keywords(idea)
combined_fields = _normalize(
" ".join(
(
result.actor,
result.situation,
result.assumption_to_test,
result.next_step,
)
)
)
if idea_keywords and not _has_keyword_match(combined_fields, idea_keywords):
return (
"Center fields are not grounded in the idea. Use at least one "
f"concrete word from the idea: {', '.join(sorted(idea_keywords)[:6])}."
)
return None
def advice_language_phrase(text: str) -> str | None:
normalized = _normalize(text)
for phrase in WHY_ADVICE_PHRASES:
if phrase in {"need to", "needs to"}:
if _has_recommendation_need(normalized):
return phrase
continue
if phrase == "must":
if _has_recommendation_must(normalized):
return phrase
continue
if re.search(rf"\b{re.escape(phrase)}\b", normalized):
return phrase
return None
def _proposed_product_reference(text: str) -> str | None:
normalized = _normalize(text)
for phrase in ("this app", "the app", "the platform", "this platform", "the product", "this product"):
if phrase in normalized:
return phrase
return None
def _has_recommendation_need(normalized: str) -> bool:
return bool(
re.search(
r"\b(?:app|tool|platform|product|service|builder|team|we|you|they)\s+needs?\s+to\b",
normalized,
)
or re.search(
r"\bneed(?:s)?\s+to\s+(?:add|build|include|incorporate|implement|design|develop|create|provide|change|improve)\b",
normalized,
)
)
def _has_recommendation_must(normalized: str) -> bool:
return bool(
re.search(
r"\b(?:app|tool|platform|product|service|builder|team|we|you|they)\s+must\b",
normalized,
)
or re.search(
r"\bmust\s+(?:add|build|include|incorporate|implement|design|develop|create|provide|change|improve)\b",
normalized,
)
)
def _format_center_action(
actor: str,
situation: str,
assumption_to_test: str,
) -> str:
actor_phrase = _single_actor_phrase(_strip_terminal_punctuation(actor))
situation_phrase = _strip_terminal_punctuation(situation)
assumption_clause = _assumption_clause(
_strip_terminal_punctuation(assumption_to_test)
)
return (
f"Ask {actor_phrase} to walk through this situation: {situation_phrase}, "
f"so you can test whether {assumption_clause}."
)
def _single_actor_phrase(actor: str) -> str:
normalized = _normalize(actor)
if re.match(r"^(a|an|one|the|two|three)\b", normalized):
return actor
return f"one {actor}"
def _assumption_clause(assumption: str) -> str:
return re.sub(r"^(whether|if|that)\s+", "", assumption, flags=re.IGNORECASE)
def _strip_terminal_punctuation(text: str) -> str:
return text.strip().rstrip(".?!;:")
def _optional_string(
data: dict[str, object], key: str, aliases: tuple[str, ...] = ()
) -> str | None:
try:
return require_string(data, key, aliases=aliases)
except IrisResponseError:
return None
def _require_why_it_bites(data: dict[str, object]) -> str:
return require_string(
data,
"why_it_bites",
aliases=WHY_IT_BITES_ALIASES,
)
def _extract_direction_pressure_fields(
data: dict[str, object],
) -> tuple[str | None, str | None]:
pressure_value = _field_value(data, "pressure", aliases=PRESSURE_ALIASES)
pressure_text = _pressure_text_from_value(pressure_value)
why_text = _optional_why_it_bites(data)
if isinstance(pressure_value, dict):
why_text = why_text or _optional_why_it_bites(pressure_value)
if pressure_text:
try:
split_pressure, inline_why = _split_inline_why(pressure_text)
except IrisResponseError:
pass
else:
pressure_text = split_pressure
why_text = why_text or inline_why
return pressure_text, why_text
def _pressure_text_from_value(value: object) -> str | None:
if isinstance(value, str) and value.strip():
return value.strip()
if isinstance(value, dict):
try:
return require_string(value, "pressure", aliases=PRESSURE_ALIASES)
except IrisResponseError:
return None
return None
def _optional_why_it_bites(data: dict[str, object]) -> str | None:
try:
return _require_why_it_bites(data)
except IrisResponseError:
return None
def _field_value(
data: dict[str, object], key: str, aliases: tuple[str, ...] = ()
) -> object | None:
if key in data:
return data[key]
normalized = {
re.sub(r"[^a-z0-9]", "", str(item_key).lower()): value
for item_key, value in data.items()
}
for candidate in (key, *aliases):
value = normalized.get(re.sub(r"[^a-z0-9]", "", candidate.lower()))
if value is not None:
return value
return None
def _alternative_copied_prior_failure(
alternative: str, prior_constraints: list[str]
) -> bool:
normalized_alternative = _normalize(alternative)
return any(
normalized_alternative in _normalize(_constraint_pressure_text(prior))
for prior in prior_constraints
)
def _constraint_pressure_text(constraint: str) -> str:
pressure = constraint.split(" Why it bites:", 1)[0]
return pressure.split(" Alternative:", 1)[0]
def _normalize(text: str) -> str:
return re.sub(r"\s+", " ", text.lower()).strip()
def _keywords(text: str, *, min_length: int = 5) -> set[str]:
stop_words = {
"about",
"actually",
"after",
"against",
"already",
"allow",
"allows",
"also",
"amazing",
"approach",
"between",
"before",
"bites",
"build",
"canvas",
"cards",
"capabilities",
"constraints",
"contact",
"could",
"current",
"direction",
"does",
"first",
"focus",
"frame",
"from",
"have",
"history",
"idea",
"into",
"iteration",
"limitations",
"model",
"original",
"pressure",
"prior",
"previous",
"reality",
"that",
"their",
"them",
"they",
"this",
"tool",
"trail",
"turns",
"user",
"well",
"what",
"when",
"where",
"which",
"while",
"with",
}
minimum = max(min_length - 1, 1)
words = set(re.findall(rf"[a-z][a-z0-9]{{{minimum},}}", text.lower()))
return {word for word in words if word not in stop_words}
def _idea_grounding_keywords(text: str) -> set[str]:
current = _frame_context_section(text, "Current iteration:")
original = _frame_context_section(text, "Original idea:")
history = _frame_context_section(text, "Iteration history:")
if current or original or history:
return _keywords(
" ".join(item for item in (current, original, history) if item),
min_length=4,
)
return _keywords(text)
def _current_iteration_keywords(text: str) -> set[str]:
current = _frame_context_section(text, "Current iteration:")
if not current:
return set()
original = _frame_context_section(text, "Original idea:")
current_keywords = _keywords(current, min_length=4)
original_keywords = _keywords(original)
original_variants = {
variant
for keyword in original_keywords
for variant in _keyword_variants(keyword)
}
current_only = {
keyword
for keyword in current_keywords
if not (_keyword_variants(keyword) & original_variants)
}
return current_only or current_keywords
def _current_iteration_text(text: str) -> str:
return _frame_context_section(text, "Current iteration:")
def _frame_context_section(text: str, heading: str) -> str:
marker = f"{heading}\n"
start = text.find(marker)
if start < 0:
return ""
start += len(marker)
end = text.find("\n\n", start)
if end < 0:
end = len(text)
return text[start:end].strip()
def _has_keyword_match(normalized_text: str, keywords: set[str]) -> bool:
for keyword in keywords:
variants = _keyword_variants(keyword)
if any(variant and variant in normalized_text for variant in variants):
return True
return False
def _keyword_variants(keyword: str) -> set[str]:
variants = {keyword}
if keyword.endswith("s"):
variants.add(keyword[:-1])
else:
variants.add(f"{keyword}s")
if keyword.endswith("ies"):
variants.add(f"{keyword[:-3]}y")
elif keyword.endswith("y"):
variants.add(f"{keyword[:-1]}ies")
return variants