T09: crystallization
A stable agentic realization becomes deterministic code. All four exit criteria met; 163 tests pass. - crystallization.py: trajectory capture, stability assessment requiring the same path across several runs, CrystallizedDriver, pytest codegen - crystallized/test_grant_access.py: generated, runs with no model, carries its lineage in the docstring - descendant preserves the ancestor's oracle set, agrees with it across five lab versions, and still catches a seeded defect - reversibility shown both ways via new M24 (grant endpoint renamed): the frozen descendant fails loudly rather than searching, and the agentic ancestor recovers from the same mutation F-0007 (open): the 54% cost reduction must not be quoted in support of the thesis. The T07 runtime is token-free, so the measured saving is one page fetch, one parse and a two-candidate scoring pass. The saving the concept actually claims - tokens, latency, retry variance - is unmeasured. Together with F-0005 this makes a bounded live-model experiment the highest-value next investment. Assertions in the generated test are imported rather than restated, so it is not fully standalone. Deliberate: paraphrased claims would be a second unverified statement of intent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 1629012@bnt-lap001 Assistant-Session: 78d4fb13-8a1e-474b-87a3-9b9261c49a39
This commit is contained in:
parent
b9ad259468
commit
eee7722714
28 changed files with 794 additions and 10 deletions
272
src/testdriver/crystallization.py
Normal file
272
src/testdriver/crystallization.py
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
"""Turning a stable agentic realization into deterministic code.
|
||||
|
||||
The thesis in one module: once an agent has found the same path enough times, the
|
||||
finding itself is the valuable part, and repeating the search is waste. What is
|
||||
crystallized is the *realization* — how to perform a semantic action on this
|
||||
surface. What is never crystallized, and never re-authored, is the judgment.
|
||||
|
||||
The descendant imports its ancestor's claim predicates rather than restating
|
||||
them. That is deliberate: a generated test that paraphrases its assertions has
|
||||
introduced a second, unverified statement of intent, and any drift between the
|
||||
two is silent. Importing makes oracle preservation a fact rather than a hope.
|
||||
|
||||
Reversibility is part of the design (`INTENT.md`: "Crystallization is
|
||||
reversible"). A descendant that stops matching its ancestor is evidence the
|
||||
surface moved again, and the asset returns to its agentic form rather than being
|
||||
patched.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Mapping, Sequence
|
||||
|
||||
from .actions import SemanticAction
|
||||
from .drivers import Realization
|
||||
from .world import Actor
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Trajectory:
|
||||
"""How one semantic action was actually performed, reduced to what replays."""
|
||||
|
||||
step_id: str
|
||||
action_name: str
|
||||
surface_id: str
|
||||
method: str
|
||||
target: str
|
||||
fields: tuple[str, ...]
|
||||
|
||||
def key(self) -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"step": self.step_id, "action": self.action_name,
|
||||
"surface": self.surface_id, "method": self.method,
|
||||
"target": self.target, "fields": sorted(self.fields),
|
||||
},
|
||||
sort_keys=True,
|
||||
)
|
||||
|
||||
|
||||
def capture(pack: Mapping[str, Any]) -> tuple[Trajectory, ...]:
|
||||
"""Extract the realization path from one Evidence Pack."""
|
||||
out: list[Trajectory] = []
|
||||
for obs in pack["observations"]:
|
||||
if obs["kind"] != "realization" or obs["data"].get("raised"):
|
||||
continue
|
||||
mechanics = obs["data"].get("mechanics", {})
|
||||
target = mechanics.get("target") or mechanics.get("operation") or ""
|
||||
fields = mechanics.get("fields") or sorted(mechanics.get("arguments") or {})
|
||||
out.append(Trajectory(
|
||||
step_id=obs["step_id"],
|
||||
action_name=str(mechanics.get("action", "")).split("(")[0],
|
||||
surface_id=obs["data"].get("surface", ""),
|
||||
method=("POST" if obs["data"].get("surface") == "browser" else "CALL"),
|
||||
target=target,
|
||||
fields=tuple(sorted(fields)),
|
||||
))
|
||||
return tuple(out)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class StabilityReport:
|
||||
stable: bool
|
||||
observations: int
|
||||
distinct_paths: int
|
||||
reason: str
|
||||
trajectories: tuple[Trajectory, ...] = ()
|
||||
|
||||
|
||||
def assess_stability(packs: Sequence[Mapping[str, Any]], minimum: int = 3) -> StabilityReport:
|
||||
"""Is this realization settled enough to freeze?
|
||||
|
||||
Requires the *same* path across several runs. One successful run proves the
|
||||
agent can find a way; it does not show the surface has stopped moving, and
|
||||
freezing on a single observation is how a crystallized test becomes flaky
|
||||
the first time a page renders differently.
|
||||
"""
|
||||
if len(packs) < minimum:
|
||||
return StabilityReport(
|
||||
False, len(packs), 0,
|
||||
f"need at least {minimum} runs to judge stability, have {len(packs)}",
|
||||
)
|
||||
captured = [capture(pack) for pack in packs]
|
||||
keys = {tuple(t.key() for t in trajectory) for trajectory in captured}
|
||||
if len(keys) != 1:
|
||||
return StabilityReport(
|
||||
False, len(packs), len(keys),
|
||||
f"realization varied across runs ({len(keys)} distinct paths); "
|
||||
"the surface is still moving",
|
||||
)
|
||||
if not captured[0]:
|
||||
return StabilityReport(False, len(packs), 0, "no successful realization to freeze")
|
||||
return StabilityReport(
|
||||
True, len(packs), 1,
|
||||
f"identical realization across {len(packs)} runs", captured[0],
|
||||
)
|
||||
|
||||
|
||||
# --- the deterministic descendant ----------------------------------------
|
||||
|
||||
|
||||
class CrystallizedDriver:
|
||||
"""Replays a frozen trajectory. No discovery, no runtime, no model.
|
||||
|
||||
Deliberately fails rather than searching when the recorded path no longer
|
||||
works. A driver that fell back to discovery would quietly turn a T5 asset
|
||||
back into a T1 one and hide the fact that the surface had moved — which is
|
||||
exactly the signal crystallization is supposed to surface.
|
||||
"""
|
||||
|
||||
name = "crystallized-driver"
|
||||
|
||||
def __init__(self, trajectories: Sequence[Trajectory], session_factory) -> None:
|
||||
self._by_step = {t.step_id: t for t in trajectories}
|
||||
self._by_action = {t.action_name: t for t in trajectories}
|
||||
self._session_factory = session_factory
|
||||
self.surface = None # set per action; a frozen path may span surfaces
|
||||
|
||||
def realize(self, actor: Actor, action: SemanticAction) -> Realization:
|
||||
trajectory = self._by_action.get(action.name)
|
||||
if trajectory is None:
|
||||
return Realization(
|
||||
"crystallized", {"action": action.name},
|
||||
raised="NotCrystallized: no frozen path for this action",
|
||||
)
|
||||
action.check_surface(trajectory.surface_id)
|
||||
args = dict(action.args)
|
||||
fields = {name: str(args[name]) for name in trajectory.fields if name in args}
|
||||
|
||||
session = self._session_factory(actor)
|
||||
mechanics: dict[str, Any] = {
|
||||
"action": action.describe(),
|
||||
"runtime": self.name,
|
||||
"target": trajectory.target,
|
||||
"fields": sorted(fields),
|
||||
"metrics": {
|
||||
"runtime": self.name, "wall_time_ms": 0.0,
|
||||
"candidates_considered": 0, "attempts": 1, "retries": 0,
|
||||
"tokens_in": 0, "tokens_out": 0, "model": None,
|
||||
},
|
||||
"actor": actor.id,
|
||||
}
|
||||
status, body = session.post_form(trajectory.target, fields)
|
||||
mechanics["status"] = status
|
||||
if status >= 400:
|
||||
return Realization(trajectory.surface_id, mechanics,
|
||||
raised=f"Refused: {status} {body[:120]}")
|
||||
return Realization(trajectory.surface_id, mechanics)
|
||||
|
||||
|
||||
# --- code generation ------------------------------------------------------
|
||||
|
||||
_TEMPLATE = '''"""Crystallized regression test — generated, do not edit by hand.
|
||||
|
||||
Lineage
|
||||
-------
|
||||
ancestor asset : {ancestor_id}
|
||||
ancestor maturity: {ancestor_maturity}
|
||||
descendant : {descendant_id} (T5 Deterministic)
|
||||
frozen from : {runs} identical realizations
|
||||
surface version: {sut_version}
|
||||
generated : {generated_at}
|
||||
|
||||
Why this file exists
|
||||
--------------------
|
||||
An agent discovered this path {runs} times running and it did not change. The
|
||||
search is now waste, so it has been frozen. **No model is involved in running
|
||||
this test.**
|
||||
|
||||
The realization below is plain HTTP with no framework dependency. The assertions
|
||||
are imported from the originating use case rather than restated — a generated
|
||||
test that paraphrases its assertions creates a second, unverified statement of
|
||||
intent, and drift between the two would be silent. See F-0007 for what that
|
||||
costs.
|
||||
|
||||
If this test starts failing, the correct first response is **not** to update the
|
||||
selectors. Re-run the agentic ancestor: if it recovers, the surface moved and
|
||||
this file should be regenerated; if it does not, the behaviour changed and that
|
||||
is a finding.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
from {claims_module} import {claim_imports}
|
||||
|
||||
TARGET = {target!r}
|
||||
FIELDS = {fields!r}
|
||||
|
||||
|
||||
def _post(base_url: str, token: str, path: str, fields: dict) -> int:
|
||||
request = urllib.request.Request(
|
||||
urllib.parse.urljoin(base_url, path),
|
||||
data=urllib.parse.urlencode(fields).encode(),
|
||||
method="POST",
|
||||
headers={{
|
||||
"Authorization": f"Bearer {{token}}",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=10) as response:
|
||||
return response.status
|
||||
except urllib.error.HTTPError as error:
|
||||
return error.code
|
||||
|
||||
|
||||
def realize(base_url: str, token: str) -> int:
|
||||
"""Perform {action_name} deterministically, exactly as the agent learned to."""
|
||||
return _post(base_url, token, TARGET, FIELDS)
|
||||
|
||||
|
||||
def test_{test_name}(crystallized_world):
|
||||
"""{action_name} still works, and the claims it protects still hold."""
|
||||
base_url, token, observe = crystallized_world
|
||||
|
||||
assert realize(base_url, token) < 400, "the frozen realization no longer works"
|
||||
|
||||
snapshot = observe()
|
||||
{assertions}
|
||||
'''
|
||||
|
||||
|
||||
def generate_test_module(
|
||||
*,
|
||||
trajectory: Trajectory,
|
||||
action_args: Mapping[str, Any],
|
||||
ancestor_id: str,
|
||||
ancestor_maturity: str,
|
||||
descendant_id: str,
|
||||
runs: int,
|
||||
sut_version: str,
|
||||
claims: Sequence[Any],
|
||||
claims_module: str,
|
||||
) -> str:
|
||||
"""Emit an ordinary pytest module for one crystallized semantic action."""
|
||||
fields = {name: str(action_args[name]) for name in trajectory.fields
|
||||
if name in action_args}
|
||||
predicate_names = [c.predicate.__name__ for c in claims]
|
||||
assertions = "\n".join(
|
||||
f" assert {c.predicate.__name__}(snapshot), {c.text!r}" for c in claims
|
||||
)
|
||||
return _TEMPLATE.format(
|
||||
ancestor_id=ancestor_id,
|
||||
ancestor_maturity=ancestor_maturity,
|
||||
descendant_id=descendant_id,
|
||||
runs=runs,
|
||||
sut_version=sut_version,
|
||||
generated_at=datetime.now(timezone.utc).date().isoformat(),
|
||||
claims_module=claims_module,
|
||||
claim_imports=", ".join(sorted(predicate_names)),
|
||||
target=trajectory.target,
|
||||
fields=fields,
|
||||
action_name=trajectory.action_name,
|
||||
test_name=trajectory.action_name,
|
||||
assertions=assertions,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue