generated from coulomb/repo-seed
271 lines
9.9 KiB
Python
271 lines
9.9 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import jsonschema
|
|
|
|
from railiance_fabric.discovery import (
|
|
attribute_stable_key,
|
|
discovery_stable_key,
|
|
relationship_stable_key,
|
|
replacement_scope_id,
|
|
source_fingerprint,
|
|
)
|
|
from railiance_fabric.schema_validation import draft202012_validator
|
|
|
|
|
|
def test_discovery_identity_helpers_are_stable_and_scoped() -> None:
|
|
service_key = discovery_stable_key("Repo Scoping", "ServiceDeclaration", "Scope Generator")
|
|
duplicate_key = discovery_stable_key("repo-scoping", "service declaration", "scope-generator")
|
|
readme_key = discovery_stable_key(
|
|
"repo-scoping",
|
|
"ServiceDeclaration",
|
|
"Scope Generator",
|
|
source_anchor={"path": "README.md", "line_start": 12},
|
|
)
|
|
pyproject_key = discovery_stable_key(
|
|
"repo-scoping",
|
|
"ServiceDeclaration",
|
|
"Scope Generator",
|
|
source_anchor={"path": "pyproject.toml", "line_start": 1},
|
|
)
|
|
|
|
assert service_key == duplicate_key
|
|
assert service_key == "discovery:repo-scoping:service-declaration:scope-generator"
|
|
assert readme_key != pyproject_key
|
|
assert readme_key.startswith(service_key)
|
|
|
|
edge_key = relationship_stable_key(
|
|
service_key,
|
|
"provides",
|
|
discovery_stable_key("repo-scoping", "CapabilityDeclaration", "Scope Generation"),
|
|
evidence_scope="fabric/declarations",
|
|
)
|
|
assert edge_key.startswith("edge:")
|
|
assert edge_key == relationship_stable_key(
|
|
service_key,
|
|
"provides",
|
|
discovery_stable_key("repo-scoping", "CapabilityDeclaration", "Scope Generation"),
|
|
evidence_scope="fabric/declarations",
|
|
)
|
|
|
|
attribute_key = attribute_stable_key(service_key, "runtime")
|
|
assert attribute_key == "attribute:discovery:repo-scoping:service-declaration:scope-generator:runtime"
|
|
|
|
assert replacement_scope_id(
|
|
"Repo Scoping",
|
|
"python-package",
|
|
"package_manifest",
|
|
source_path="pyproject.toml",
|
|
) != replacement_scope_id(
|
|
"Repo Scoping",
|
|
"python-package",
|
|
"package_manifest",
|
|
source_path="package.json",
|
|
)
|
|
|
|
|
|
def test_source_fingerprint_ignores_review_snippet_noise() -> None:
|
|
base = {
|
|
"source_kind": "file",
|
|
"path": "README.md",
|
|
"line_start": 10,
|
|
"line_end": 18,
|
|
"snippet": "first wording",
|
|
}
|
|
changed_snippet = {**base, "snippet": "edited wording"}
|
|
|
|
assert source_fingerprint(base) == source_fingerprint(changed_snippet)
|
|
assert source_fingerprint(base) != source_fingerprint({**base, "line_start": 11})
|
|
|
|
|
|
def test_discovery_snapshot_schema_accepts_candidate_graph() -> None:
|
|
service_key = discovery_stable_key("repo-scoping", "ServiceDeclaration", "Scope Generator")
|
|
capability_key = discovery_stable_key("repo-scoping", "CapabilityDeclaration", "Scope Generation")
|
|
edge_key = relationship_stable_key(service_key, "provides", capability_key)
|
|
scope_id = replacement_scope_id(
|
|
"repo-scoping",
|
|
"python-package",
|
|
"package_manifest",
|
|
source_path="pyproject.toml",
|
|
)
|
|
anchor = {
|
|
"source_kind": "package_manifest",
|
|
"path": "pyproject.toml",
|
|
"json_pointer": "/project/name",
|
|
"fingerprint": source_fingerprint(
|
|
{
|
|
"source_kind": "package_manifest",
|
|
"path": "pyproject.toml",
|
|
"json_pointer": "/project/name",
|
|
}
|
|
),
|
|
}
|
|
provenance = {
|
|
"extractor_id": "python-package",
|
|
"extractor_version": "0.1.0",
|
|
"method": "deterministic",
|
|
"origin": "deterministic",
|
|
}
|
|
payload = {
|
|
"apiVersion": "railiance.fabric/v1alpha1",
|
|
"kind": "FabricDiscoverySnapshot",
|
|
"generated_at": "2026-05-19T00:00:00Z",
|
|
"source": {
|
|
"repo_slug": "repo-scoping",
|
|
"repo_name": "repo-scoping",
|
|
"domain": "capabilities",
|
|
"commit": "abc123",
|
|
"path": "/home/worsch/repo-scoping",
|
|
},
|
|
"scan": {
|
|
"run_id": "scan:repo-scoping:abc123",
|
|
"profile": "deterministic",
|
|
"deterministic_only": True,
|
|
"llm_enabled": False,
|
|
"started_at": "2026-05-19T00:00:00Z",
|
|
"completed_at": "2026-05-19T00:00:01Z",
|
|
},
|
|
"replacement_scopes": [
|
|
{
|
|
"id": scope_id,
|
|
"extractor_id": "python-package",
|
|
"source_kind": "package_manifest",
|
|
"source_path": "pyproject.toml",
|
|
"mode": "replacement",
|
|
}
|
|
],
|
|
"candidates": {
|
|
"nodes": [
|
|
{
|
|
"stable_key": service_key,
|
|
"kind": "ServiceDeclaration",
|
|
"label": "Scope Generator",
|
|
"repo": "repo-scoping",
|
|
"domain": "capabilities",
|
|
"lifecycle": "active",
|
|
"aliases": ["repo-scoping", "scope-generator"],
|
|
"attributes": {"language": "python"},
|
|
"origin": "deterministic",
|
|
"review_state": "candidate",
|
|
"status": "active",
|
|
"confidence": 0.85,
|
|
"replacement_scope": scope_id,
|
|
"provenance": [provenance],
|
|
"source_anchors": [anchor],
|
|
},
|
|
{
|
|
"stable_key": capability_key,
|
|
"kind": "CapabilityDeclaration",
|
|
"label": "Scope Generation",
|
|
"repo": "repo-scoping",
|
|
"domain": "capabilities",
|
|
"aliases": ["scope-generation"],
|
|
"attributes": {"capability_type": "scope-generation"},
|
|
"origin": "llm",
|
|
"review_state": "needs_review",
|
|
"status": "active",
|
|
"confidence": 0.62,
|
|
"replacement_scope": scope_id,
|
|
"provenance": [
|
|
{
|
|
"extractor_id": "readme-llm",
|
|
"method": "llm",
|
|
"origin": "llm",
|
|
"prompt_version": "repo-summary-v1",
|
|
"provider": "mock",
|
|
"model": "mock",
|
|
"usage": {"total_tokens": 0},
|
|
"rationale": "Fixture output for schema coverage.",
|
|
}
|
|
],
|
|
"source_anchors": [anchor],
|
|
},
|
|
],
|
|
"edges": [
|
|
{
|
|
"stable_key": edge_key,
|
|
"edge_type": "provides",
|
|
"source_key": service_key,
|
|
"target_key": capability_key,
|
|
"origin": "deterministic",
|
|
"review_state": "candidate",
|
|
"status": "active",
|
|
"confidence": 0.8,
|
|
"replacement_scope": scope_id,
|
|
"provenance": [provenance],
|
|
"source_anchors": [anchor],
|
|
}
|
|
],
|
|
"attributes": [
|
|
{
|
|
"stable_key": attribute_stable_key(service_key, "language"),
|
|
"entity_key": service_key,
|
|
"name": "language",
|
|
"value": "python",
|
|
"origin": "deterministic",
|
|
"review_state": "candidate",
|
|
"confidence": 0.95,
|
|
"replacement_scope": scope_id,
|
|
"provenance": [provenance],
|
|
"source_anchors": [anchor],
|
|
}
|
|
],
|
|
},
|
|
"tombstones": [
|
|
{
|
|
"stable_key": discovery_stable_key("repo-scoping", "ServiceDeclaration", "Old Scope Tool"),
|
|
"entity_kind": "node",
|
|
"replacement_scope": scope_id,
|
|
"retired_at": "2026-05-19T00:00:01Z",
|
|
"reason": "source_missing",
|
|
}
|
|
],
|
|
"reconciliation": {
|
|
"precedence": ["repo_declaration", "deterministic", "catalog", "registry", "llm", "manual"],
|
|
"duplicate_policy": "stable-key matches merge automatically; alias-only matches require review",
|
|
"retirement_policy": "missing candidates retire only inside their replacement scope",
|
|
},
|
|
}
|
|
|
|
_validate_schema("discovery-snapshot.schema.yaml", payload)
|
|
|
|
|
|
def test_discovery_snapshot_schema_rejects_unscoped_tombstone() -> None:
|
|
payload = {
|
|
"apiVersion": "railiance.fabric/v1alpha1",
|
|
"kind": "FabricDiscoverySnapshot",
|
|
"source": {"repo_slug": "repo-scoping", "commit": "abc123"},
|
|
"scan": {
|
|
"run_id": "scan:repo-scoping:abc123",
|
|
"profile": "deterministic",
|
|
"deterministic_only": True,
|
|
"llm_enabled": False,
|
|
},
|
|
"replacement_scopes": [],
|
|
"candidates": {"nodes": [], "edges": [], "attributes": []},
|
|
"tombstones": [
|
|
{
|
|
"stable_key": "discovery:repo-scoping:service:old",
|
|
"entity_kind": "node",
|
|
"retired_at": "2026-05-19T00:00:01Z",
|
|
"reason": "source_missing",
|
|
}
|
|
],
|
|
"reconciliation": {
|
|
"precedence": ["repo_declaration", "deterministic", "catalog", "registry", "llm", "manual"],
|
|
"duplicate_policy": "stable-key matches merge automatically",
|
|
"retirement_policy": "missing candidates retire only inside their replacement scope",
|
|
},
|
|
}
|
|
|
|
validator = draft202012_validator(Path("schemas") / "discovery-snapshot.schema.yaml")
|
|
with pytest.raises(jsonschema.ValidationError):
|
|
validator.validate(payload)
|
|
|
|
|
|
def _validate_schema(schema_name: str, payload: dict[str, object]) -> None:
|
|
validator = draft202012_validator(Path("schemas") / schema_name)
|
|
validator.validate(payload)
|