fix(skill-versioning): consume divergence_tolerance, replace vacuous catalog-isolation guard
- 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
This commit is contained in:
@@ -3,6 +3,7 @@ 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
|
||||
|
||||
@@ -13,6 +14,68 @@ class VersionDiff:
|
||||
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)
|
||||
@@ -21,41 +84,17 @@ class VersioningResult:
|
||||
created_new_version: bool
|
||||
|
||||
|
||||
def diff_flow_versions(
|
||||
stored_steps: list[FlowStep],
|
||||
executed_steps: list[FlowStep],
|
||||
) -> VersionDiff:
|
||||
stored_sequence = [step.tool_name for step in stored_steps]
|
||||
executed_sequence = [step.tool_name for step in executed_steps]
|
||||
structural = stored_sequence != executed_sequence
|
||||
differences: list[tuple[int, str, Any, Any]] = []
|
||||
if not structural:
|
||||
for index, (stored_step, executed_step) in enumerate(
|
||||
zip(stored_steps, executed_steps, strict=True)
|
||||
):
|
||||
keys = set(stored_step.args) | set(executed_step.args)
|
||||
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))
|
||||
return VersionDiff(
|
||||
structural_divergence=structural,
|
||||
stored_sequence=stored_sequence,
|
||||
executed_sequence=executed_sequence,
|
||||
argument_differences=differences,
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
diff = diff_flow_versions(latest.steps, candidate.steps, config=config)
|
||||
if diff.structural_divergence:
|
||||
return VersioningResult(store.create_version(candidate, parent=latest), True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user