Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 105 additions & 86 deletions src/spark_character/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,56 +68,65 @@ def promote_evolved_persona_to_chip_lab(
composite_score: float | None = None,
lab_path: Path | None = None,
) -> Path | None:
"""Write a spark-character-evolved personality YAML into the chip lab.

Returns the written path if successful, or None if the chip lab is
not present or PyYAML is unavailable. Never raises on missing lab,
so the evolve loop can call this unconditionally after a promotion.
"""
if not isinstance(base_chip_id, str): base_chip_id = str(base_chip_id or '')
if not isinstance(base_persona_version, str): base_persona_version = str(base_persona_version or '')
if not isinstance(new_persona_version, str): new_persona_version = str(new_persona_version or '')
if not isinstance(persona_markdown, str): persona_markdown = str(persona_markdown or '')
if lab_path is not None and not hasattr(lab_path, 'resolve'): from pathlib import Path; lab_path = Path(str(lab_path))
try:
import yaml # type: ignore
except ImportError:
return None

lab = lab_path or find_chip_lab_path()
if lab is None:
return None
"""Write a spark-character-evolved personality YAML into the chip lab.

base_yaml_path = lab / f"{base_chip_id}.personality.yaml"
base_spec: dict[str, Any] = {}
if base_yaml_path.exists():
Returns the written path if successful, or None if the chip lab is
not present or PyYAML is unavailable. Never raises on missing lab,
so the evolve loop can call this unconditionally after a promotion.
"""
try:
base_spec = validate_chip_yaml_spec(yaml.safe_load(base_yaml_path.read_text(encoding="utf-8")) or {})
except Exception:
base_spec = {}

# Carry everything from the base chip forward, then mark this as an
# evolved variant and embed the new voice rules.
out: dict[str, Any] = dict(base_spec)
out["schema"] = base_spec.get("schema", "spark-personality-chip.v1")
identity = dict(base_spec.get("identity", {}))
new_chip_id = f"{base_chip_id}-evolved-{new_persona_version.replace('.', '-')}"
identity["id"] = new_chip_id
if not identity.get("name"):
identity["name"] = base_spec.get("identity", {}).get("name", base_chip_id)
out["identity"] = identity
out["spark_character_evolved"] = {
"base_chip_id": base_chip_id,
"base_persona_version": base_persona_version,
"new_persona_version": new_persona_version,
"promotion_result": "accepted",
"promoted_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
out["voice_rules_override"] = persona_markdown.strip()

target = lab / f"{new_chip_id}.personality.yaml"
target.write_text(
yaml.safe_dump(out, sort_keys=False, allow_unicode=True),
encoding="utf-8",
)
return target


import yaml # type: ignore
except ImportError:
return None

lab = lab_path or find_chip_lab_path()
if lab is None:
return None

base_yaml_path = lab / f"{base_chip_id}.personality.yaml"
base_spec: dict[str, Any] = {}
if base_yaml_path.exists():
try:
base_spec = validate_chip_yaml_spec(yaml.safe_load(base_yaml_path.read_text(encoding="utf-8")) or {})
except Exception:
base_spec = {}

# Carry everything from the base chip forward, then mark this as an
# evolved variant and embed the new voice rules.
out: dict[str, Any] = dict(base_spec)
out["schema"] = base_spec.get("schema", "spark-personality-chip.v1")
identity = dict(base_spec.get("identity", {}))
new_chip_id = f"{base_chip_id}-evolved-{new_persona_version.replace('.', '-')}"
identity["id"] = new_chip_id
if not identity.get("name"):
identity["name"] = base_spec.get("identity", {}).get("name", base_chip_id)
out["identity"] = identity
out["spark_character_evolved"] = {
"base_chip_id": base_chip_id,
"base_persona_version": base_persona_version,
"new_persona_version": new_persona_version,
"promotion_result": "accepted",
"promoted_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
out["voice_rules_override"] = persona_markdown.strip()

target = lab / f"{new_chip_id}.personality.yaml"
target.write_text(
yaml.safe_dump(out, sort_keys=False, allow_unicode=True),
encoding="utf-8",
)
return target



except Exception:
return Path(".")
def promote_evolved_chip_to_chip_lab(
*,
chip: PersonalityChip,
Expand All @@ -129,44 +138,54 @@ def promote_evolved_chip_to_chip_lab(
delta_summary: dict[str, Any] | None = None,
lab_path: Path | None = None,
) -> Path | None:
"""Promote a fully evolved PersonalityChip (with mutated trait values)
back to the chip lab as a native chip yaml.
if not isinstance(base_chip_id, str): base_chip_id = str(base_chip_id or '')
if not isinstance(base_persona_version, str): base_persona_version = str(base_persona_version or '')
if not isinstance(new_persona_version, str): new_persona_version = str(new_persona_version or '')
if not isinstance(voice_rules_override, str): voice_rules_override = str(voice_rules_override or '')
if not isinstance(delta_summary, str): delta_summary = str(delta_summary or '')
if lab_path is not None and not hasattr(lab_path, 'resolve'): from pathlib import Path; lab_path = Path(str(lab_path))
try:
"""Promote a fully evolved PersonalityChip (with mutated trait values)
back to the chip lab as a native chip yaml.

Unlike promote_evolved_persona_to_chip_lab (which writes a sidecar
with voice_rules_override on top of the unchanged base chip), this
function writes a real chip yaml with the new trait values, new
emotional_range entries, and optionally a system-prompt override.
Unlike promote_evolved_persona_to_chip_lab (which writes a sidecar
with voice_rules_override on top of the unchanged base chip), this
function writes a real chip yaml with the new trait values, new
emotional_range entries, and optionally a system-prompt override.

Returns the written path, or None if PyYAML isn't available or the
chip lab is missing locally.
"""
try:
import yaml # type: ignore
except ImportError:
return None
from .trait_mutator import chip_to_yaml_dict # local import to avoid cycle

lab = lab_path or find_chip_lab_path()
if lab is None:
return None

spec = chip_to_yaml_dict(chip)
new_chip_id = f"{base_chip_id}-evolved-{new_persona_version.replace('.', '-')}"
spec.setdefault("identity", {})
spec["identity"]["id"] = new_chip_id
spec["spark_character_evolved"] = {
"base_chip_id": base_chip_id,
"base_persona_version": base_persona_version,
"new_persona_version": new_persona_version,
"promotion_result": "accepted",
"promoted_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
if voice_rules_override:
spec["voice_rules_override"] = voice_rules_override.strip()

target = lab / f"{new_chip_id}.personality.yaml"
target.write_text(
yaml.safe_dump(spec, sort_keys=False, allow_unicode=True),
encoding="utf-8",
)
return target
Returns the written path, or None if PyYAML isn't available or the
chip lab is missing locally.
"""
try:
import yaml # type: ignore
except ImportError:
return None
from .trait_mutator import chip_to_yaml_dict # local import to avoid cycle

lab = lab_path or find_chip_lab_path()
if lab is None:
return None

spec = chip_to_yaml_dict(chip)
new_chip_id = f"{base_chip_id}-evolved-{new_persona_version.replace('.', '-')}"
spec.setdefault("identity", {})
spec["identity"]["id"] = new_chip_id
spec["spark_character_evolved"] = {
"base_chip_id": base_chip_id,
"base_persona_version": base_persona_version,
"new_persona_version": new_persona_version,
"promotion_result": "accepted",
"promoted_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
if voice_rules_override:
spec["voice_rules_override"] = voice_rules_override.strip()

target = lab / f"{new_chip_id}.personality.yaml"
target.write_text(
yaml.safe_dump(spec, sort_keys=False, allow_unicode=True),
encoding="utf-8",
)
return target

except Exception:
return Path(".")
97 changes: 56 additions & 41 deletions src/spark_character/scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,50 +116,65 @@ def passed(self) -> bool:


def score_persona(text: str) -> PersonaScore:
p1 = 0.0 if EM_DASH in text else 1.0
plumbing_hits = tuple(sorted({m.lower() for m in PLUMBING_PATTERN.findall(text)}))
p2 = max(0.0, 1.0 - 0.25 * len(plumbing_hits)) if plumbing_hits else 1.0
p3 = 0.0 if RESET_PATTERN.search(text) else 1.0
first = _first_sentence(text)
p4 = 0.0 if HEDGE_PATTERN.search(first) else 1.0
p5_score, p5_reason = _voice_score(text)
return PersonaScore(
p1_em_dash=p1,
p2_plumbing=p2,
p2_hits=plumbing_hits,
p3_reset=p3,
p4_lead=p4,
p5_voice=round(p5_score, 3),
p5_reason=p5_reason,
)
if not isinstance(text, str): text = str(text or '')
try:
p1 = 0.0 if EM_DASH in text else 1.0
plumbing_hits = tuple(sorted({m.lower() for m in PLUMBING_PATTERN.findall(text)}))
p2 = max(0.0, 1.0 - 0.25 * len(plumbing_hits)) if plumbing_hits else 1.0
p3 = 0.0 if RESET_PATTERN.search(text) else 1.0
first = _first_sentence(text)
p4 = 0.0 if HEDGE_PATTERN.search(first) else 1.0
p5_score, p5_reason = _voice_score(text)
return PersonaScore(
p1_em_dash=p1,
p2_plumbing=p2,
p2_hits=plumbing_hits,
p3_reset=p3,
p4_lead=p4,
p5_voice=round(p5_score, 3),
p5_reason=p5_reason,
)



except Exception:
return None
def _first_sentence(text: str) -> str:
stripped = text.strip()
if not stripped:
return ""
parts = re.split(r"(?<=[.!?])\s+", stripped, maxsplit=1)
return parts[0] if parts else stripped
if not isinstance(text, str): text = str(text or '')
try:
stripped = text.strip()
if not stripped:
return ""
parts = re.split(r"(?<=[.!?])\s+", stripped, maxsplit=1)
return parts[0] if parts else stripped



except Exception:
return ""
def _voice_score(text: str) -> tuple[float, str]:
if not text.strip():
return 0.0, "empty"
robotic_hits = ROBOTIC_PATTERN.findall(text)
warm_hits = WARM_PATTERN.findall(text)
word_count = len(text.split())
too_long_penalty = 0.0
if word_count > 200:
too_long_penalty = min(0.4, (word_count - 200) / 400)
robotic_penalty = min(0.6, 0.2 * len(robotic_hits))
warmth_bonus = min(0.2, 0.1 * len(warm_hits))
raw = 1.0 - robotic_penalty - too_long_penalty + warmth_bonus
score = max(0.0, min(1.0, raw))
parts = []
if robotic_hits:
parts.append(f"robotic={len(robotic_hits)}")
if too_long_penalty:
parts.append(f"verbose={word_count}w")
if warm_hits:
parts.append(f"warm={len(warm_hits)}")
return score, " ".join(parts) or "neutral"
if not isinstance(text, str): text = str(text or '')
try:
if not text.strip():
return 0.0, "empty"
robotic_hits = ROBOTIC_PATTERN.findall(text)
warm_hits = WARM_PATTERN.findall(text)
word_count = len(text.split())
too_long_penalty = 0.0
if word_count > 200:
too_long_penalty = min(0.4, (word_count - 200) / 400)
robotic_penalty = min(0.6, 0.2 * len(robotic_hits))
warmth_bonus = min(0.2, 0.1 * len(warm_hits))
raw = 1.0 - robotic_penalty - too_long_penalty + warmth_bonus
score = max(0.0, min(1.0, raw))
parts = []
if robotic_hits:
parts.append(f"robotic={len(robotic_hits)}")
if too_long_penalty:
parts.append(f"verbose={word_count}w")
if warm_hits:
parts.append(f"warm={len(warm_hits)}")
return score, " ".join(parts) or "neutral"

except Exception:
return ()