- divergence_tolerance was loaded from config but diff_flow_versions never consulted it, so structural_divergence was always plain sequence-equality regardless of the configured tolerance. Now actually applied per design.md D4. - The skill-catalog-subscription isolation guard test asserted 'skills.catalog' not in sys.modules, which is vacuously true since that module doesn't exist anywhere yet. Replaced with a real check against store.py's actual imports. openspec: skill-versioning capability, archived change skill-learning-runtime
112 lines
4.0 KiB
Python
112 lines
4.0 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from skills_learning.config import SkillAuthoringConfig
|
|
from skills_learning.models import FlowStep, FlowTemplateSkill
|
|
from skills_learning.store import SkillStore
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class VersionDiff:
|
|
structural_divergence: bool
|
|
stored_sequence: list[str]
|
|
executed_sequence: list[str]
|
|
argument_differences: list[tuple[int, str, Any, Any]]
|
|
argument_divergence_fraction: float
|
|
|
|
|
|
def diff_flow_versions(
|
|
stored_steps: list[FlowStep],
|
|
executed_steps: list[FlowStep],
|
|
*,
|
|
config: SkillAuthoringConfig | None = None,
|
|
) -> VersionDiff:
|
|
"""Compare a stored skill's steps against a newly executed run.
|
|
|
|
Per design.md D4: a change to the tool-name *sequence* itself
|
|
(insertion/deletion/reorder) always triggers a new version, with no
|
|
tolerance applied. When the sequence matches (same skeleton), the
|
|
fraction of step positions whose arguments differ is compared against
|
|
``config.divergence_tolerance`` (a fraction in ``[0, 1]``): a
|
|
"materially different parameter set" that exceeds that tolerance is
|
|
also treated as divergence worth a new version, per proposal.md's
|
|
"differ beyond a configured tolerance ... or a materially different
|
|
parameter set" trigger. When no ``config`` is supplied, tolerance is
|
|
treated as unlimited (matching the pre-existing, backward-compatible
|
|
behavior of never bumping a version for argument-only differences).
|
|
"""
|
|
stored_sequence = [step.tool_name for step in stored_steps]
|
|
executed_sequence = [step.tool_name for step in executed_steps]
|
|
sequence_diverged = stored_sequence != executed_sequence
|
|
|
|
differences: list[tuple[int, str, Any, Any]] = []
|
|
diverged_positions = 0
|
|
total_positions = 0
|
|
if not sequence_diverged:
|
|
for index, (stored_step, executed_step) in enumerate(
|
|
zip(stored_steps, executed_steps, strict=True)
|
|
):
|
|
total_positions += 1
|
|
keys = set(stored_step.args) | set(executed_step.args)
|
|
position_diverged = False
|
|
for key in sorted(keys):
|
|
stored_value = stored_step.args.get(key)
|
|
executed_value = executed_step.args.get(key)
|
|
if stored_value != executed_value:
|
|
differences.append((index, key, stored_value, executed_value))
|
|
position_diverged = True
|
|
if position_diverged:
|
|
diverged_positions += 1
|
|
|
|
argument_divergence_fraction = (
|
|
diverged_positions / total_positions if total_positions else 0.0
|
|
)
|
|
beyond_tolerance = (
|
|
not sequence_diverged
|
|
and config is not None
|
|
and argument_divergence_fraction > config.divergence_tolerance
|
|
)
|
|
|
|
return VersionDiff(
|
|
structural_divergence=sequence_diverged or beyond_tolerance,
|
|
stored_sequence=stored_sequence,
|
|
executed_sequence=executed_sequence,
|
|
argument_differences=differences,
|
|
argument_divergence_fraction=argument_divergence_fraction,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class VersioningResult:
|
|
skill: FlowTemplateSkill
|
|
created_new_version: bool
|
|
|
|
|
|
def store_synthesized_skill(
|
|
store: SkillStore,
|
|
candidate: FlowTemplateSkill,
|
|
*,
|
|
config: SkillAuthoringConfig | None = None,
|
|
) -> VersioningResult:
|
|
latest = store.get_latest_by_name(candidate.name)
|
|
if latest is None:
|
|
return VersioningResult(store.create_version(candidate), True)
|
|
|
|
diff = diff_flow_versions(latest.steps, candidate.steps, config=config)
|
|
if diff.structural_divergence:
|
|
return VersioningResult(store.create_version(candidate, parent=latest), True)
|
|
|
|
merged_parameters = {
|
|
**latest.parameters,
|
|
**candidate.parameters,
|
|
}
|
|
updated = latest.with_updates(
|
|
steps=candidate.steps,
|
|
parameters=merged_parameters,
|
|
description=candidate.description,
|
|
originating_goal=candidate.originating_goal,
|
|
)
|
|
return VersioningResult(store.update_skill(updated), False)
|