Skip to content
Merged
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
66 changes: 66 additions & 0 deletions diffgraph/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,72 @@ def value(self) -> dict:
return deepcopy(self._value)


def enrich_with_prose(
artifact: ValidatedArtifact,
text: str,
*,
provider: str,
model: str,
confidence: float | None = None,
prompt_ref: str | None = None,
) -> ValidatedArtifact:
"""Return a separately validated artifact with optional AI prose only.

The supplied prose is deliberately limited to ``summary`` and its own
provenance. Files, symbols, and relationships are copied from the frozen
deterministic artifact, so a provider cannot add, remove, or edit graph
topology. This helper performs no provider call and never receives a
prompt or API credential.
"""
if not isinstance(artifact, ValidatedArtifact):
raise TypeError("enrich_with_prose requires a ValidatedArtifact")
if not isinstance(text, str) or not text.strip():
raise ValueError("summary text must be a non-empty string")
if not isinstance(provider, str) or not provider.strip():
raise ValueError("provider must be a non-empty string")
if not isinstance(model, str) or not model.strip():
raise ValueError("model must be a non-empty string")
if confidence is not None and (
isinstance(confidence, bool)
or not isinstance(confidence, (int, float))
or not 0 <= confidence <= 1
):
raise ValueError("confidence must be a number from 0 through 1")
if prompt_ref is not None and (not isinstance(prompt_ref, str) or not prompt_ref.strip()):
raise ValueError("prompt_ref must be a non-empty string when provided")

enriched = artifact.value
llm_evidence = {"kind": "llm_inference", "model": model}
if prompt_ref is not None:
llm_evidence["prompt_ref"] = prompt_ref
enriched["summary"] = {
"text": text,
"analysis_source": "inferred",
"confidence": confidence,
"evidence": [
llm_evidence,
{
"kind": "structural_basis",
"file_ids": [item["id"] for item in enriched["files"]],
"symbol_ids": [item["id"] for item in enriched["symbols"]],
},
],
}
metadata = enriched["metadata"]
# The v2 contract has no combined tier. Retain a pre-existing backend
# classification so enrichment never hides that the artifact's data also
# left the machine through WildestAI's backend.
if metadata.get("privacy_tier") != "cloud_backend":
metadata["privacy_tier"] = "cloud_llm"
metadata["cloud_providers_used"] = sorted(
set(metadata.get("cloud_providers_used", [])) | {provider}
)
metadata["llm_calls"] = (metadata.get("llm_calls") or 0) + 1
metadata["llm_model"] = model
metadata["tiers_used"] = sorted(set(metadata.get("tiers_used", [])) | {"inferred"})
return ValidatedArtifact.from_value(enriched)


def schema_version(value: object) -> Tuple[int, int]:
"""Parse and compatibility-check a DiffGraph ``MAJOR.MINOR`` version."""
if not isinstance(value, str):
Expand Down
78 changes: 78 additions & 0 deletions tests/test_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
SUPPORTED_SCHEMA_MAJOR,
DiffGraphContractError,
ValidatedArtifact,
enrich_with_prose,
load_schema,
schema_version,
validate_artifact,
Expand Down Expand Up @@ -65,6 +66,83 @@ def test_golden_contains_only_local_structural_claims(golden_artifact):
)


def test_optional_prose_enrichment_cannot_mutate_frozen_topology(golden_artifact):
frozen = ValidatedArtifact.from_value(golden_artifact)
topology_before = {
field: frozen.value[field]
for field in ("files", "symbols", "relationships")
}

enriched = enrich_with_prose(
frozen,
"The deterministic graph shows one modified Python symbol.",
provider="byok-openai",
model="gpt-test",
confidence=0.75,
prompt_ref="summary-v1",
)

validate_artifact(enriched.value)
assert frozen.value["summary"] is None
assert {
field: frozen.value[field]
for field in ("files", "symbols", "relationships")
} == topology_before
assert {
field: enriched.value[field]
for field in ("files", "symbols", "relationships")
} == topology_before
assert enriched.value["summary"] == {
"text": "The deterministic graph shows one modified Python symbol.",
"analysis_source": "inferred",
"confidence": 0.75,
"evidence": [
{"kind": "llm_inference", "model": "gpt-test", "prompt_ref": "summary-v1"},
{
"kind": "structural_basis",
"file_ids": [item["id"] for item in topology_before["files"]],
"symbol_ids": [item["id"] for item in topology_before["symbols"]],
},
],
}
assert enriched.value["metadata"]["privacy_tier"] == "cloud_llm"
assert enriched.value["metadata"]["cloud_providers_used"] == ["byok-openai"]
assert enriched.value["metadata"]["llm_calls"] == 1
assert enriched.value["metadata"]["llm_model"] == "gpt-test"
assert enriched.value["metadata"]["tiers_used"] == ["inferred", "structural"]


def test_optional_prose_enrichment_preserves_cloud_backend_privacy(golden_artifact):
golden_artifact["metadata"]["privacy_tier"] = "cloud_backend"
frozen = ValidatedArtifact.from_value(golden_artifact)

enriched = enrich_with_prose(
frozen,
"The deterministic graph has backend provenance.",
provider="byok-openai",
model="gpt-test",
)

assert enriched.value["metadata"]["privacy_tier"] == "cloud_backend"
assert enriched.value["metadata"]["cloud_providers_used"] == ["byok-openai"]


@pytest.mark.parametrize(
("kwargs", "message"),
[
({"text": "", "provider": "provider", "model": "model"}, "summary text"),
({"text": "summary", "provider": "", "model": "model"}, "provider"),
({"text": "summary", "provider": "provider", "model": ""}, "model"),
({"text": "summary", "provider": "provider", "model": "model", "confidence": 2}, "confidence"),
],
)
def test_optional_prose_enrichment_rejects_ambiguous_provenance(golden_artifact, kwargs, message):
frozen = ValidatedArtifact.from_value(golden_artifact)

with pytest.raises(ValueError, match=message):
enrich_with_prose(frozen, **kwargs)


@pytest.mark.parametrize("value", [None, 2, "2", "v2", "2.0.0", "02.0", "2.-1"])
def test_schema_version_rejects_malformed_values(value):
with pytest.raises(DiffGraphContractError, match=r"MAJOR\.MINOR"):
Expand Down
Loading