generated from coulomb/repo-seed
Improved datamodel and deterministic generation
This commit is contained in:
4
TODO.md
4
TODO.md
@@ -48,8 +48,8 @@ the Custodian State Hub. Read it at the start of each session. It documents:
|
|||||||
|
|
||||||
## Active Workplan
|
## Active Workplan
|
||||||
|
|
||||||
`workplans/RREG-WP-0002-production-hardening.md` is the current active workplan.
|
`workplans/RREG-WP-0004-characteristic-classification-navigation.md` is the
|
||||||
Six tasks remain open (T01–T06). Start with T01 (P0: Update Safety and Change Review)
|
current active workplan. Start with T01 (P0: Characteristic Classification Fields)
|
||||||
as the highest-priority item.
|
as the highest-priority item.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ CREATE TABLE IF NOT EXISTS candidate_abilities (
|
|||||||
analysis_run_id INTEGER NOT NULL REFERENCES analysis_runs(id) ON DELETE CASCADE,
|
analysis_run_id INTEGER NOT NULL REFERENCES analysis_runs(id) ON DELETE CASCADE,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
description TEXT NOT NULL DEFAULT '',
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
primary_class TEXT NOT NULL DEFAULT 'ability',
|
||||||
|
attributes TEXT NOT NULL DEFAULT '[]',
|
||||||
confidence REAL NOT NULL DEFAULT 0.0,
|
confidence REAL NOT NULL DEFAULT 0.0,
|
||||||
status TEXT NOT NULL DEFAULT 'candidate',
|
status TEXT NOT NULL DEFAULT 'candidate',
|
||||||
source_refs TEXT NOT NULL DEFAULT '[]',
|
source_refs TEXT NOT NULL DEFAULT '[]',
|
||||||
@@ -77,6 +79,8 @@ CREATE TABLE IF NOT EXISTS candidate_capabilities (
|
|||||||
description TEXT NOT NULL DEFAULT '',
|
description TEXT NOT NULL DEFAULT '',
|
||||||
inputs TEXT NOT NULL DEFAULT '[]',
|
inputs TEXT NOT NULL DEFAULT '[]',
|
||||||
outputs TEXT NOT NULL DEFAULT '[]',
|
outputs TEXT NOT NULL DEFAULT '[]',
|
||||||
|
primary_class TEXT NOT NULL DEFAULT 'capability',
|
||||||
|
attributes TEXT NOT NULL DEFAULT '[]',
|
||||||
confidence REAL NOT NULL DEFAULT 0.0,
|
confidence REAL NOT NULL DEFAULT 0.0,
|
||||||
status TEXT NOT NULL DEFAULT 'candidate',
|
status TEXT NOT NULL DEFAULT 'candidate',
|
||||||
source_refs TEXT NOT NULL DEFAULT '[]',
|
source_refs TEXT NOT NULL DEFAULT '[]',
|
||||||
@@ -90,6 +94,8 @@ CREATE TABLE IF NOT EXISTS candidate_features (
|
|||||||
capability_id INTEGER NOT NULL REFERENCES candidate_capabilities(id) ON DELETE CASCADE,
|
capability_id INTEGER NOT NULL REFERENCES candidate_capabilities(id) ON DELETE CASCADE,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
type TEXT NOT NULL,
|
type TEXT NOT NULL,
|
||||||
|
primary_class TEXT NOT NULL DEFAULT '',
|
||||||
|
attributes TEXT NOT NULL DEFAULT '[]',
|
||||||
location TEXT NOT NULL DEFAULT '',
|
location TEXT NOT NULL DEFAULT '',
|
||||||
confidence REAL NOT NULL DEFAULT 0.0,
|
confidence REAL NOT NULL DEFAULT 0.0,
|
||||||
status TEXT NOT NULL DEFAULT 'candidate',
|
status TEXT NOT NULL DEFAULT 'candidate',
|
||||||
@@ -128,6 +134,8 @@ CREATE TABLE IF NOT EXISTS repository_scopes (
|
|||||||
repository_id INTEGER NOT NULL UNIQUE REFERENCES repositories(id) ON DELETE CASCADE,
|
repository_id INTEGER NOT NULL UNIQUE REFERENCES repositories(id) ON DELETE CASCADE,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
description TEXT NOT NULL DEFAULT '',
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
primary_class TEXT NOT NULL DEFAULT 'ability',
|
||||||
|
attributes TEXT NOT NULL DEFAULT '[]',
|
||||||
confidence REAL NOT NULL DEFAULT 1.0,
|
confidence REAL NOT NULL DEFAULT 1.0,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -149,6 +157,8 @@ CREATE TABLE IF NOT EXISTS approved_capabilities (
|
|||||||
description TEXT NOT NULL DEFAULT '',
|
description TEXT NOT NULL DEFAULT '',
|
||||||
inputs TEXT NOT NULL DEFAULT '[]',
|
inputs TEXT NOT NULL DEFAULT '[]',
|
||||||
outputs TEXT NOT NULL DEFAULT '[]',
|
outputs TEXT NOT NULL DEFAULT '[]',
|
||||||
|
primary_class TEXT NOT NULL DEFAULT 'capability',
|
||||||
|
attributes TEXT NOT NULL DEFAULT '[]',
|
||||||
confidence REAL NOT NULL DEFAULT 1.0,
|
confidence REAL NOT NULL DEFAULT 1.0,
|
||||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
@@ -159,6 +169,8 @@ CREATE TABLE IF NOT EXISTS approved_features (
|
|||||||
capability_id INTEGER NOT NULL REFERENCES approved_capabilities(id) ON DELETE CASCADE,
|
capability_id INTEGER NOT NULL REFERENCES approved_capabilities(id) ON DELETE CASCADE,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
type TEXT NOT NULL,
|
type TEXT NOT NULL,
|
||||||
|
primary_class TEXT NOT NULL DEFAULT '',
|
||||||
|
attributes TEXT NOT NULL DEFAULT '[]',
|
||||||
location TEXT NOT NULL DEFAULT '',
|
location TEXT NOT NULL DEFAULT '',
|
||||||
confidence REAL NOT NULL DEFAULT 1.0,
|
confidence REAL NOT NULL DEFAULT 1.0,
|
||||||
source_refs TEXT NOT NULL DEFAULT '[]',
|
source_refs TEXT NOT NULL DEFAULT '[]',
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class CandidateFeatureDraft:
|
|||||||
location: str
|
location: str
|
||||||
confidence: float
|
confidence: float
|
||||||
source_refs: list[SourceReference]
|
source_refs: list[SourceReference]
|
||||||
|
primary_class: str = ""
|
||||||
|
attributes: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -31,6 +33,8 @@ class CandidateCapabilityDraft:
|
|||||||
outputs: list[str]
|
outputs: list[str]
|
||||||
confidence: float
|
confidence: float
|
||||||
source_refs: list[SourceReference]
|
source_refs: list[SourceReference]
|
||||||
|
primary_class: str = "capability"
|
||||||
|
attributes: list[str] = field(default_factory=list)
|
||||||
features: list[CandidateFeatureDraft] = field(default_factory=list)
|
features: list[CandidateFeatureDraft] = field(default_factory=list)
|
||||||
evidence: list[CandidateEvidenceDraft] = field(default_factory=list)
|
evidence: list[CandidateEvidenceDraft] = field(default_factory=list)
|
||||||
|
|
||||||
@@ -41,6 +45,8 @@ class CandidateAbilityDraft:
|
|||||||
description: str
|
description: str
|
||||||
confidence: float
|
confidence: float
|
||||||
source_refs: list[SourceReference]
|
source_refs: list[SourceReference]
|
||||||
|
primary_class: str = "ability"
|
||||||
|
attributes: list[str] = field(default_factory=list)
|
||||||
capabilities: list[CandidateCapabilityDraft] = field(default_factory=list)
|
capabilities: list[CandidateCapabilityDraft] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@@ -68,6 +74,11 @@ class CandidateGraphGenerator:
|
|||||||
credential_configs = self._facts(facts, "credential_config")
|
credential_configs = self._facts(facts, "credential_config")
|
||||||
provider_registries = self._facts(facts, "provider_registry")
|
provider_registries = self._facts(facts, "provider_registry")
|
||||||
fallback_policies = self._facts(facts, "fallback_policy")
|
fallback_policies = self._facts(facts, "fallback_policy")
|
||||||
|
ability_primary_class, ability_attributes = self._ability_classification(
|
||||||
|
repository,
|
||||||
|
facts,
|
||||||
|
chunks,
|
||||||
|
)
|
||||||
|
|
||||||
ability_sources = docs or manifests or languages
|
ability_sources = docs or manifests or languages
|
||||||
ability = CandidateAbilityDraft(
|
ability = CandidateAbilityDraft(
|
||||||
@@ -82,6 +93,8 @@ class CandidateGraphGenerator:
|
|||||||
languages=languages,
|
languages=languages,
|
||||||
),
|
),
|
||||||
source_refs=self._source_refs(ability_sources),
|
source_refs=self._source_refs(ability_sources),
|
||||||
|
primary_class=ability_primary_class,
|
||||||
|
attributes=ability_attributes,
|
||||||
capabilities=[],
|
capabilities=[],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -119,6 +132,12 @@ class CandidateGraphGenerator:
|
|||||||
docs=docs,
|
docs=docs,
|
||||||
),
|
),
|
||||||
source_refs=self._source_refs(manifests + frameworks + languages),
|
source_refs=self._source_refs(manifests + frameworks + languages),
|
||||||
|
primary_class="repository-structure",
|
||||||
|
attributes=self._structure_attributes(
|
||||||
|
manifests,
|
||||||
|
frameworks,
|
||||||
|
languages,
|
||||||
|
),
|
||||||
evidence=self._evidence(tests, examples, docs),
|
evidence=self._evidence(tests, examples, docs),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -129,6 +148,8 @@ class CandidateGraphGenerator:
|
|||||||
description=ability.description,
|
description=ability.description,
|
||||||
confidence=ability.confidence,
|
confidence=ability.confidence,
|
||||||
source_refs=ability.source_refs,
|
source_refs=ability.source_refs,
|
||||||
|
primary_class=ability.primary_class,
|
||||||
|
attributes=ability.attributes,
|
||||||
capabilities=capabilities,
|
capabilities=capabilities,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -154,6 +175,8 @@ class CandidateGraphGenerator:
|
|||||||
docs=docs,
|
docs=docs,
|
||||||
),
|
),
|
||||||
source_refs=self._source_refs(interfaces),
|
source_refs=self._source_refs(interfaces),
|
||||||
|
primary_class="interface",
|
||||||
|
attributes=self._interface_attributes(interfaces),
|
||||||
features=features,
|
features=features,
|
||||||
evidence=self._evidence(tests, examples, docs),
|
evidence=self._evidence(tests, examples, docs),
|
||||||
)
|
)
|
||||||
@@ -181,6 +204,8 @@ class CandidateGraphGenerator:
|
|||||||
source_refs=self._source_refs(
|
source_refs=self._source_refs(
|
||||||
[fact for fact in providers if fact.name == provider]
|
[fact for fact in providers if fact.name == provider]
|
||||||
),
|
),
|
||||||
|
primary_class="integration",
|
||||||
|
attributes=["llm-provider", provider.lower()],
|
||||||
)
|
)
|
||||||
for provider in provider_names
|
for provider in provider_names
|
||||||
]
|
]
|
||||||
@@ -192,6 +217,8 @@ class CandidateGraphGenerator:
|
|||||||
location=self._grouped_location(credentials),
|
location=self._grouped_location(credentials),
|
||||||
confidence=0.7,
|
confidence=0.7,
|
||||||
source_refs=self._source_refs(credentials),
|
source_refs=self._source_refs(credentials),
|
||||||
|
primary_class="configuration",
|
||||||
|
attributes=["credential", "llm-provider"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if registries:
|
if registries:
|
||||||
@@ -202,6 +229,8 @@ class CandidateGraphGenerator:
|
|||||||
location=self._grouped_location(registries),
|
location=self._grouped_location(registries),
|
||||||
confidence=0.65,
|
confidence=0.65,
|
||||||
source_refs=self._source_refs(registries),
|
source_refs=self._source_refs(registries),
|
||||||
|
primary_class="backend",
|
||||||
|
attributes=["provider-registry", "llm-provider"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if fallback_policies:
|
if fallback_policies:
|
||||||
@@ -212,6 +241,8 @@ class CandidateGraphGenerator:
|
|||||||
location=self._grouped_location(fallback_policies),
|
location=self._grouped_location(fallback_policies),
|
||||||
confidence=0.6,
|
confidence=0.6,
|
||||||
source_refs=self._source_refs(fallback_policies),
|
source_refs=self._source_refs(fallback_policies),
|
||||||
|
primary_class="backend",
|
||||||
|
attributes=["fallback-policy", "llm-provider"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return CandidateCapabilityDraft(
|
return CandidateCapabilityDraft(
|
||||||
@@ -232,6 +263,13 @@ class CandidateGraphGenerator:
|
|||||||
source_refs=self._source_refs(
|
source_refs=self._source_refs(
|
||||||
providers + credentials + registries + fallback_policies
|
providers + credentials + registries + fallback_policies
|
||||||
),
|
),
|
||||||
|
primary_class="llm-integration",
|
||||||
|
attributes=self._llm_provider_attributes(
|
||||||
|
providers,
|
||||||
|
credentials,
|
||||||
|
registries,
|
||||||
|
fallback_policies,
|
||||||
|
),
|
||||||
features=features,
|
features=features,
|
||||||
evidence=self._evidence(tests, examples, docs),
|
evidence=self._evidence(tests, examples, docs),
|
||||||
)
|
)
|
||||||
@@ -256,6 +294,8 @@ class CandidateGraphGenerator:
|
|||||||
location=fact.path,
|
location=fact.path,
|
||||||
confidence=0.65 if fact.value else 0.45,
|
confidence=0.65 if fact.value else 0.45,
|
||||||
source_refs=self._source_refs([fact]),
|
source_refs=self._source_refs([fact]),
|
||||||
|
primary_class=feature_type,
|
||||||
|
attributes=self._feature_attributes(feature_type, [fact]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@@ -271,6 +311,8 @@ class CandidateGraphGenerator:
|
|||||||
location=self._grouped_location(facts),
|
location=self._grouped_location(facts),
|
||||||
confidence=self._grouped_interface_confidence(facts),
|
confidence=self._grouped_interface_confidence(facts),
|
||||||
source_refs=self._source_refs(facts),
|
source_refs=self._source_refs(facts),
|
||||||
|
primary_class=feature_type,
|
||||||
|
attributes=self._feature_attributes(feature_type, facts),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return features
|
return features
|
||||||
@@ -357,6 +399,96 @@ class CandidateGraphGenerator:
|
|||||||
return "API"
|
return "API"
|
||||||
return "interface"
|
return "interface"
|
||||||
|
|
||||||
|
def _ability_classification(
|
||||||
|
self,
|
||||||
|
repository: Repository,
|
||||||
|
facts: list[ObservedFact],
|
||||||
|
chunks: list[ContentChunk],
|
||||||
|
) -> tuple[str, list[str]]:
|
||||||
|
text = " ".join(
|
||||||
|
[
|
||||||
|
repository.name,
|
||||||
|
repository.description or "",
|
||||||
|
" ".join(chunk.text[:600] for chunk in chunks if chunk.kind == "documentation"),
|
||||||
|
" ".join(f"{fact.kind} {fact.name} {fact.value}" for fact in facts),
|
||||||
|
]
|
||||||
|
).lower()
|
||||||
|
attributes: list[str] = []
|
||||||
|
if any(token in text for token in ("repository", "repo", "registry")):
|
||||||
|
attributes.append("repository")
|
||||||
|
if any(token in text for token in ("ability", "capability", "feature")):
|
||||||
|
return "repository-intelligence", self._unique(attributes + ["capability-mapping"])
|
||||||
|
if any(token in text for token in ("llm", "openrouter", "claude", "model provider")):
|
||||||
|
return "ai-integration", self._unique(attributes + ["llm-provider"])
|
||||||
|
if any(fact.kind == "interface" for fact in facts):
|
||||||
|
attributes.append("interface")
|
||||||
|
return "developer-tooling", self._unique(attributes)
|
||||||
|
|
||||||
|
def _interface_attributes(self, interfaces: list[ObservedFact]) -> list[str]:
|
||||||
|
feature_types = {self._feature_type(fact) for fact in interfaces}
|
||||||
|
attributes = ["api" if item == "API" else "cli" if item == "CLI" else "callable" for item in feature_types]
|
||||||
|
return self._unique(["surface", *attributes])
|
||||||
|
|
||||||
|
def _feature_attributes(
|
||||||
|
self,
|
||||||
|
feature_type: str,
|
||||||
|
facts: list[ObservedFact],
|
||||||
|
) -> list[str]:
|
||||||
|
attributes = [feature_type]
|
||||||
|
if feature_type == "API":
|
||||||
|
attributes.extend(["surface", "http"])
|
||||||
|
elif feature_type == "CLI":
|
||||||
|
attributes.extend(["surface", "command"])
|
||||||
|
else:
|
||||||
|
attributes.append("surface")
|
||||||
|
paths = " ".join(fact.path.lower() for fact in facts)
|
||||||
|
if "test" in paths:
|
||||||
|
attributes.append("test-linked")
|
||||||
|
return self._unique(attributes)
|
||||||
|
|
||||||
|
def _structure_attributes(
|
||||||
|
self,
|
||||||
|
manifests: list[ObservedFact],
|
||||||
|
frameworks: list[ObservedFact],
|
||||||
|
languages: list[ObservedFact],
|
||||||
|
) -> list[str]:
|
||||||
|
return self._unique(
|
||||||
|
[
|
||||||
|
"manifest" if manifests else "",
|
||||||
|
*[fact.name for fact in frameworks],
|
||||||
|
*[fact.name for fact in languages],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _llm_provider_attributes(
|
||||||
|
self,
|
||||||
|
providers: list[ObservedFact],
|
||||||
|
credentials: list[ObservedFact],
|
||||||
|
registries: list[ObservedFact],
|
||||||
|
fallback_policies: list[ObservedFact],
|
||||||
|
) -> list[str]:
|
||||||
|
return self._unique(
|
||||||
|
[
|
||||||
|
"llm-provider",
|
||||||
|
*[fact.name.lower() for fact in providers],
|
||||||
|
"credential" if credentials else "",
|
||||||
|
"provider-registry" if registries else "",
|
||||||
|
"fallback-policy" if fallback_policies else "",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _unique(self, values: list[str]) -> list[str]:
|
||||||
|
result: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for value in values:
|
||||||
|
item = value.strip()
|
||||||
|
key = item.lower()
|
||||||
|
if not item or key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
result.append(item)
|
||||||
|
return result
|
||||||
|
|
||||||
def _interface_inputs(self, interfaces: list[ObservedFact]) -> list[str]:
|
def _interface_inputs(self, interfaces: list[ObservedFact]) -> list[str]:
|
||||||
feature_types = {self._feature_type(fact) for fact in interfaces}
|
feature_types = {self._feature_type(fact) for fact in interfaces}
|
||||||
inputs: list[str] = []
|
inputs: list[str] = []
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ def _combine_abilities(
|
|||||||
description=_preferred_description(left.description, right.description),
|
description=_preferred_description(left.description, right.description),
|
||||||
confidence=max(left.confidence, right.confidence),
|
confidence=max(left.confidence, right.confidence),
|
||||||
source_refs=_merge_source_refs(left.source_refs, right.source_refs),
|
source_refs=_merge_source_refs(left.source_refs, right.source_refs),
|
||||||
|
primary_class=_preferred_text(left.primary_class, right.primary_class),
|
||||||
|
attributes=_merge_strings(left.attributes, right.attributes),
|
||||||
capabilities=_merge_capabilities(left.capabilities + right.capabilities),
|
capabilities=_merge_capabilities(left.capabilities + right.capabilities),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,6 +109,8 @@ def _combine_capabilities(
|
|||||||
outputs=_merge_strings(left.outputs, right.outputs),
|
outputs=_merge_strings(left.outputs, right.outputs),
|
||||||
confidence=max(left.confidence, right.confidence),
|
confidence=max(left.confidence, right.confidence),
|
||||||
source_refs=_merge_source_refs(left.source_refs, right.source_refs),
|
source_refs=_merge_source_refs(left.source_refs, right.source_refs),
|
||||||
|
primary_class=_preferred_text(left.primary_class, right.primary_class),
|
||||||
|
attributes=_merge_strings(left.attributes, right.attributes),
|
||||||
features=_merge_features(left.features + right.features),
|
features=_merge_features(left.features + right.features),
|
||||||
evidence=_merge_evidence(left.evidence + right.evidence),
|
evidence=_merge_evidence(left.evidence + right.evidence),
|
||||||
)
|
)
|
||||||
@@ -128,6 +132,8 @@ def _merge_features(
|
|||||||
location=_preferred_text(existing.location, feature.location),
|
location=_preferred_text(existing.location, feature.location),
|
||||||
confidence=max(existing.confidence, feature.confidence),
|
confidence=max(existing.confidence, feature.confidence),
|
||||||
source_refs=_merge_source_refs(existing.source_refs, feature.source_refs),
|
source_refs=_merge_source_refs(existing.source_refs, feature.source_refs),
|
||||||
|
primary_class=_preferred_text(existing.primary_class, feature.primary_class),
|
||||||
|
attributes=_merge_strings(existing.attributes, feature.attributes),
|
||||||
)
|
)
|
||||||
return merged
|
return merged
|
||||||
|
|
||||||
|
|||||||
@@ -161,6 +161,8 @@ class CandidateFeature:
|
|||||||
status: str
|
status: str
|
||||||
source_refs: list[SourceReference]
|
source_refs: list[SourceReference]
|
||||||
confidence_label: str = ""
|
confidence_label: str = ""
|
||||||
|
primary_class: str = ""
|
||||||
|
attributes: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -174,6 +176,8 @@ class CandidateCapability:
|
|||||||
status: str
|
status: str
|
||||||
source_refs: list[SourceReference]
|
source_refs: list[SourceReference]
|
||||||
confidence_label: str = ""
|
confidence_label: str = ""
|
||||||
|
primary_class: str = "capability"
|
||||||
|
attributes: list[str] = field(default_factory=list)
|
||||||
features: list[CandidateFeature] = field(default_factory=list)
|
features: list[CandidateFeature] = field(default_factory=list)
|
||||||
evidence: list[CandidateEvidence] = field(default_factory=list)
|
evidence: list[CandidateEvidence] = field(default_factory=list)
|
||||||
|
|
||||||
@@ -187,6 +191,8 @@ class CandidateAbility:
|
|||||||
status: str
|
status: str
|
||||||
source_refs: list[SourceReference]
|
source_refs: list[SourceReference]
|
||||||
confidence_label: str = ""
|
confidence_label: str = ""
|
||||||
|
primary_class: str = "ability"
|
||||||
|
attributes: list[str] = field(default_factory=list)
|
||||||
capabilities: list[CandidateCapability] = field(default_factory=list)
|
capabilities: list[CandidateCapability] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@@ -228,6 +234,8 @@ class Feature:
|
|||||||
confidence: float
|
confidence: float
|
||||||
confidence_label: str = ""
|
confidence_label: str = ""
|
||||||
source_refs: list[SourceReference] = field(default_factory=list)
|
source_refs: list[SourceReference] = field(default_factory=list)
|
||||||
|
primary_class: str = ""
|
||||||
|
attributes: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -239,6 +247,8 @@ class Capability:
|
|||||||
outputs: list[str]
|
outputs: list[str]
|
||||||
confidence: float
|
confidence: float
|
||||||
confidence_label: str = ""
|
confidence_label: str = ""
|
||||||
|
primary_class: str = "capability"
|
||||||
|
attributes: list[str] = field(default_factory=list)
|
||||||
features: list[Feature] = field(default_factory=list)
|
features: list[Feature] = field(default_factory=list)
|
||||||
evidence: list[Evidence] = field(default_factory=list)
|
evidence: list[Evidence] = field(default_factory=list)
|
||||||
|
|
||||||
@@ -250,6 +260,8 @@ class Ability:
|
|||||||
description: str
|
description: str
|
||||||
confidence: float
|
confidence: float
|
||||||
confidence_label: str = ""
|
confidence_label: str = ""
|
||||||
|
primary_class: str = "ability"
|
||||||
|
attributes: list[str] = field(default_factory=list)
|
||||||
capabilities: list[Capability] = field(default_factory=list)
|
capabilities: list[Capability] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -369,6 +369,8 @@ class RegistryService:
|
|||||||
location=feature.location,
|
location=feature.location,
|
||||||
confidence=feature.confidence,
|
confidence=feature.confidence,
|
||||||
source_refs=feature.source_refs,
|
source_refs=feature.source_refs,
|
||||||
|
primary_class=feature.primary_class,
|
||||||
|
attributes=feature.attributes,
|
||||||
)
|
)
|
||||||
for evidence in capability.evidence:
|
for evidence in capability.evidence:
|
||||||
if evidence.status != "candidate":
|
if evidence.status != "candidate":
|
||||||
@@ -512,6 +514,8 @@ class RegistryService:
|
|||||||
location=feature.location,
|
location=feature.location,
|
||||||
confidence=feature.confidence,
|
confidence=feature.confidence,
|
||||||
source_refs=feature.source_refs,
|
source_refs=feature.source_refs,
|
||||||
|
primary_class=feature.primary_class,
|
||||||
|
attributes=feature.attributes,
|
||||||
)
|
)
|
||||||
self.store.mark_candidate_feature_status(
|
self.store.mark_candidate_feature_status(
|
||||||
repository_id,
|
repository_id,
|
||||||
@@ -655,6 +659,8 @@ class RegistryService:
|
|||||||
inputs=capability.inputs,
|
inputs=capability.inputs,
|
||||||
outputs=capability.outputs,
|
outputs=capability.outputs,
|
||||||
confidence=capability.confidence,
|
confidence=capability.confidence,
|
||||||
|
primary_class=capability.primary_class,
|
||||||
|
attributes=capability.attributes,
|
||||||
)
|
)
|
||||||
for feature in capability.features:
|
for feature in capability.features:
|
||||||
if feature.status != "candidate":
|
if feature.status != "candidate":
|
||||||
@@ -667,6 +673,8 @@ class RegistryService:
|
|||||||
location=feature.location,
|
location=feature.location,
|
||||||
confidence=feature.confidence,
|
confidence=feature.confidence,
|
||||||
source_refs=feature.source_refs,
|
source_refs=feature.source_refs,
|
||||||
|
primary_class=feature.primary_class,
|
||||||
|
attributes=feature.attributes,
|
||||||
)
|
)
|
||||||
for evidence in capability.evidence:
|
for evidence in capability.evidence:
|
||||||
if evidence.status != "candidate":
|
if evidence.status != "candidate":
|
||||||
@@ -702,6 +710,8 @@ class RegistryService:
|
|||||||
name=candidate_ability.name,
|
name=candidate_ability.name,
|
||||||
description=candidate_ability.description,
|
description=candidate_ability.description,
|
||||||
confidence=candidate_ability.confidence,
|
confidence=candidate_ability.confidence,
|
||||||
|
primary_class=candidate_ability.primary_class,
|
||||||
|
attributes=candidate_ability.attributes,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _ensure_approved_capability(
|
def _ensure_approved_capability(
|
||||||
@@ -726,6 +736,8 @@ class RegistryService:
|
|||||||
inputs=candidate_capability.inputs,
|
inputs=candidate_capability.inputs,
|
||||||
outputs=candidate_capability.outputs,
|
outputs=candidate_capability.outputs,
|
||||||
confidence=candidate_capability.confidence,
|
confidence=candidate_capability.confidence,
|
||||||
|
primary_class=candidate_capability.primary_class,
|
||||||
|
attributes=candidate_capability.attributes,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _candidate_capability_with_parent(
|
def _candidate_capability_with_parent(
|
||||||
@@ -884,6 +896,8 @@ class RegistryService:
|
|||||||
name: str,
|
name: str,
|
||||||
description: str,
|
description: str,
|
||||||
confidence: float,
|
confidence: float,
|
||||||
|
primary_class: str = "ability",
|
||||||
|
attributes: Sequence[str] = (),
|
||||||
notes: str = "",
|
notes: str = "",
|
||||||
) -> CandidateGraph:
|
) -> CandidateGraph:
|
||||||
self.store.update_candidate_ability(
|
self.store.update_candidate_ability(
|
||||||
@@ -893,6 +907,8 @@ class RegistryService:
|
|||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class,
|
||||||
|
attributes=list(attributes),
|
||||||
)
|
)
|
||||||
self.store.create_review_decision(
|
self.store.create_review_decision(
|
||||||
repository_id,
|
repository_id,
|
||||||
@@ -912,6 +928,8 @@ class RegistryService:
|
|||||||
name: str,
|
name: str,
|
||||||
description: str,
|
description: str,
|
||||||
confidence: float,
|
confidence: float,
|
||||||
|
primary_class: str = "capability",
|
||||||
|
attributes: Sequence[str] = (),
|
||||||
notes: str = "",
|
notes: str = "",
|
||||||
) -> CandidateGraph:
|
) -> CandidateGraph:
|
||||||
self.store.update_candidate_capability(
|
self.store.update_candidate_capability(
|
||||||
@@ -921,6 +939,8 @@ class RegistryService:
|
|||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class,
|
||||||
|
attributes=list(attributes),
|
||||||
)
|
)
|
||||||
self.store.create_review_decision(
|
self.store.create_review_decision(
|
||||||
repository_id,
|
repository_id,
|
||||||
@@ -931,6 +951,40 @@ class RegistryService:
|
|||||||
self.store.update_repository_status(repository_id, "reviewing")
|
self.store.update_repository_status(repository_id, "reviewing")
|
||||||
return self.store.get_candidate_graph(repository_id, analysis_run_id)
|
return self.store.get_candidate_graph(repository_id, analysis_run_id)
|
||||||
|
|
||||||
|
def edit_candidate_feature(
|
||||||
|
self,
|
||||||
|
repository_id: int,
|
||||||
|
analysis_run_id: int,
|
||||||
|
candidate_feature_id: int,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
type: str,
|
||||||
|
location: str,
|
||||||
|
confidence: float,
|
||||||
|
primary_class: str | None = None,
|
||||||
|
attributes: Sequence[str] = (),
|
||||||
|
notes: str = "",
|
||||||
|
) -> CandidateGraph:
|
||||||
|
self.store.update_candidate_feature(
|
||||||
|
repository_id,
|
||||||
|
analysis_run_id,
|
||||||
|
candidate_feature_id,
|
||||||
|
name=name,
|
||||||
|
type=type,
|
||||||
|
location=location,
|
||||||
|
confidence=confidence,
|
||||||
|
primary_class=primary_class,
|
||||||
|
attributes=list(attributes),
|
||||||
|
)
|
||||||
|
self.store.create_review_decision(
|
||||||
|
repository_id,
|
||||||
|
analysis_run_id,
|
||||||
|
action="edit_candidate_feature",
|
||||||
|
notes=notes,
|
||||||
|
)
|
||||||
|
self.store.update_repository_status(repository_id, "reviewing")
|
||||||
|
return self.store.get_candidate_graph(repository_id, analysis_run_id)
|
||||||
|
|
||||||
def relink_candidate_capability(
|
def relink_candidate_capability(
|
||||||
self,
|
self,
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
@@ -1106,6 +1160,8 @@ class RegistryService:
|
|||||||
name: str,
|
name: str,
|
||||||
description: str = "",
|
description: str = "",
|
||||||
confidence: float = 1.0,
|
confidence: float = 1.0,
|
||||||
|
primary_class: str = "ability",
|
||||||
|
attributes: Sequence[str] = (),
|
||||||
) -> int:
|
) -> int:
|
||||||
self.store.get_repository(repository_id)
|
self.store.get_repository(repository_id)
|
||||||
return self.store.create_ability(
|
return self.store.create_ability(
|
||||||
@@ -1113,6 +1169,8 @@ class RegistryService:
|
|||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class,
|
||||||
|
attributes=list(attributes),
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_ability(
|
def update_ability(
|
||||||
@@ -1123,6 +1181,8 @@ class RegistryService:
|
|||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
confidence: float | None = None,
|
confidence: float | None = None,
|
||||||
|
primary_class: str | None = None,
|
||||||
|
attributes: Sequence[str] | None = None,
|
||||||
) -> RepositoryAbilityMap:
|
) -> RepositoryAbilityMap:
|
||||||
self.store.update_ability(
|
self.store.update_ability(
|
||||||
repository_id,
|
repository_id,
|
||||||
@@ -1130,6 +1190,8 @@ class RegistryService:
|
|||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class,
|
||||||
|
attributes=list(attributes) if attributes is not None else None,
|
||||||
)
|
)
|
||||||
return self.store.get_ability_map(repository_id)
|
return self.store.get_ability_map(repository_id)
|
||||||
|
|
||||||
@@ -1151,6 +1213,8 @@ class RegistryService:
|
|||||||
inputs: Sequence[str] = (),
|
inputs: Sequence[str] = (),
|
||||||
outputs: Sequence[str] = (),
|
outputs: Sequence[str] = (),
|
||||||
confidence: float = 1.0,
|
confidence: float = 1.0,
|
||||||
|
primary_class: str = "capability",
|
||||||
|
attributes: Sequence[str] = (),
|
||||||
) -> int:
|
) -> int:
|
||||||
self.store.ensure_ability(repository_id, ability_id)
|
self.store.ensure_ability(repository_id, ability_id)
|
||||||
return self.store.create_capability(
|
return self.store.create_capability(
|
||||||
@@ -1161,6 +1225,8 @@ class RegistryService:
|
|||||||
inputs=list(inputs),
|
inputs=list(inputs),
|
||||||
outputs=list(outputs),
|
outputs=list(outputs),
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class,
|
||||||
|
attributes=list(attributes),
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_capability(
|
def update_capability(
|
||||||
@@ -1173,6 +1239,8 @@ class RegistryService:
|
|||||||
inputs: Sequence[str] | None = None,
|
inputs: Sequence[str] | None = None,
|
||||||
outputs: Sequence[str] | None = None,
|
outputs: Sequence[str] | None = None,
|
||||||
confidence: float | None = None,
|
confidence: float | None = None,
|
||||||
|
primary_class: str | None = None,
|
||||||
|
attributes: Sequence[str] | None = None,
|
||||||
) -> RepositoryAbilityMap:
|
) -> RepositoryAbilityMap:
|
||||||
self.store.update_capability(
|
self.store.update_capability(
|
||||||
repository_id,
|
repository_id,
|
||||||
@@ -1182,6 +1250,8 @@ class RegistryService:
|
|||||||
inputs=list(inputs) if inputs is not None else None,
|
inputs=list(inputs) if inputs is not None else None,
|
||||||
outputs=list(outputs) if outputs is not None else None,
|
outputs=list(outputs) if outputs is not None else None,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class,
|
||||||
|
attributes=list(attributes) if attributes is not None else None,
|
||||||
)
|
)
|
||||||
return self.store.get_ability_map(repository_id)
|
return self.store.get_ability_map(repository_id)
|
||||||
|
|
||||||
@@ -1202,6 +1272,8 @@ class RegistryService:
|
|||||||
type: str,
|
type: str,
|
||||||
location: str = "",
|
location: str = "",
|
||||||
confidence: float = 1.0,
|
confidence: float = 1.0,
|
||||||
|
primary_class: str | None = None,
|
||||||
|
attributes: Sequence[str] = (),
|
||||||
) -> int:
|
) -> int:
|
||||||
self.store.ensure_capability(repository_id, capability_id)
|
self.store.ensure_capability(repository_id, capability_id)
|
||||||
return self.store.create_feature(
|
return self.store.create_feature(
|
||||||
@@ -1211,6 +1283,8 @@ class RegistryService:
|
|||||||
type=type,
|
type=type,
|
||||||
location=location,
|
location=location,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class,
|
||||||
|
attributes=list(attributes),
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_feature(
|
def update_feature(
|
||||||
@@ -1222,6 +1296,8 @@ class RegistryService:
|
|||||||
type: str | None = None,
|
type: str | None = None,
|
||||||
location: str | None = None,
|
location: str | None = None,
|
||||||
confidence: float | None = None,
|
confidence: float | None = None,
|
||||||
|
primary_class: str | None = None,
|
||||||
|
attributes: Sequence[str] | None = None,
|
||||||
) -> RepositoryAbilityMap:
|
) -> RepositoryAbilityMap:
|
||||||
self.store.update_feature(
|
self.store.update_feature(
|
||||||
repository_id,
|
repository_id,
|
||||||
@@ -1230,6 +1306,8 @@ class RegistryService:
|
|||||||
type=type,
|
type=type,
|
||||||
location=location,
|
location=location,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class,
|
||||||
|
attributes=list(attributes) if attributes is not None else None,
|
||||||
)
|
)
|
||||||
return self.store.get_ability_map(repository_id)
|
return self.store.get_ability_map(repository_id)
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class RegistryStore:
|
|||||||
self._ensure_repository_scopes_table(connection)
|
self._ensure_repository_scopes_table(connection)
|
||||||
self._ensure_approved_source_ref_columns(connection)
|
self._ensure_approved_source_ref_columns(connection)
|
||||||
self._ensure_evidence_relationship_columns(connection)
|
self._ensure_evidence_relationship_columns(connection)
|
||||||
|
self._ensure_characteristic_classification_columns(connection)
|
||||||
self._ensure_expectation_gaps_table(connection)
|
self._ensure_expectation_gaps_table(connection)
|
||||||
|
|
||||||
def connect(self) -> sqlite3.Connection:
|
def connect(self) -> sqlite3.Connection:
|
||||||
@@ -108,6 +109,60 @@ class RegistryStore:
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _ensure_characteristic_classification_columns(
|
||||||
|
self,
|
||||||
|
connection: sqlite3.Connection,
|
||||||
|
) -> None:
|
||||||
|
defaults = {
|
||||||
|
"candidate_abilities": "ability",
|
||||||
|
"approved_abilities": "ability",
|
||||||
|
"candidate_capabilities": "capability",
|
||||||
|
"approved_capabilities": "capability",
|
||||||
|
"candidate_features": "",
|
||||||
|
"approved_features": "",
|
||||||
|
}
|
||||||
|
for table, default_class in defaults.items():
|
||||||
|
columns = {
|
||||||
|
row["name"]
|
||||||
|
for row in connection.execute(f"PRAGMA table_info({table})").fetchall()
|
||||||
|
}
|
||||||
|
if "primary_class" not in columns:
|
||||||
|
connection.execute(
|
||||||
|
f"ALTER TABLE {table} ADD COLUMN primary_class TEXT NOT NULL DEFAULT '{default_class}'"
|
||||||
|
)
|
||||||
|
if "attributes" not in columns:
|
||||||
|
connection.execute(
|
||||||
|
f"ALTER TABLE {table} ADD COLUMN attributes TEXT NOT NULL DEFAULT '[]'"
|
||||||
|
)
|
||||||
|
|
||||||
|
for table in ("candidate_abilities", "approved_abilities"):
|
||||||
|
connection.execute(
|
||||||
|
f"""
|
||||||
|
UPDATE {table}
|
||||||
|
SET primary_class = COALESCE(NULLIF(primary_class, ''), 'ability'),
|
||||||
|
attributes = COALESCE(NULLIF(attributes, ''), '[]')
|
||||||
|
WHERE primary_class = '' OR attributes = ''
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
for table in ("candidate_capabilities", "approved_capabilities"):
|
||||||
|
connection.execute(
|
||||||
|
f"""
|
||||||
|
UPDATE {table}
|
||||||
|
SET primary_class = COALESCE(NULLIF(primary_class, ''), 'capability'),
|
||||||
|
attributes = COALESCE(NULLIF(attributes, ''), '[]')
|
||||||
|
WHERE primary_class = '' OR attributes = ''
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
for table in ("candidate_features", "approved_features"):
|
||||||
|
connection.execute(
|
||||||
|
f"""
|
||||||
|
UPDATE {table}
|
||||||
|
SET primary_class = COALESCE(NULLIF(primary_class, ''), type),
|
||||||
|
attributes = COALESCE(NULLIF(attributes, ''), json_array(type))
|
||||||
|
WHERE primary_class = '' OR attributes = ''
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
def _ensure_content_chunks_table(self, connection: sqlite3.Connection) -> None:
|
def _ensure_content_chunks_table(self, connection: sqlite3.Connection) -> None:
|
||||||
connection.execute(
|
connection.execute(
|
||||||
"""
|
"""
|
||||||
@@ -361,14 +416,17 @@ class RegistryStore:
|
|||||||
ability_cursor = connection.execute(
|
ability_cursor = connection.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO candidate_abilities
|
INSERT INTO candidate_abilities
|
||||||
(repository_id, analysis_run_id, name, description, confidence, source_refs)
|
(repository_id, analysis_run_id, name, description, primary_class,
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
attributes, confidence, source_refs)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
repository_id,
|
repository_id,
|
||||||
analysis_run_id,
|
analysis_run_id,
|
||||||
ability.name,
|
ability.name,
|
||||||
ability.description,
|
ability.description,
|
||||||
|
ability.primary_class or "ability",
|
||||||
|
self._attributes_to_json(ability.attributes),
|
||||||
ability.confidence,
|
ability.confidence,
|
||||||
self._source_refs_to_json(ability.source_refs),
|
self._source_refs_to_json(ability.source_refs),
|
||||||
),
|
),
|
||||||
@@ -379,8 +437,8 @@ class RegistryStore:
|
|||||||
"""
|
"""
|
||||||
INSERT INTO candidate_capabilities
|
INSERT INTO candidate_capabilities
|
||||||
(repository_id, analysis_run_id, ability_id, name, description,
|
(repository_id, analysis_run_id, ability_id, name, description,
|
||||||
inputs, outputs, confidence, source_refs)
|
inputs, outputs, primary_class, attributes, confidence, source_refs)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
repository_id,
|
repository_id,
|
||||||
@@ -390,6 +448,8 @@ class RegistryStore:
|
|||||||
capability.description,
|
capability.description,
|
||||||
json.dumps(capability.inputs),
|
json.dumps(capability.inputs),
|
||||||
json.dumps(capability.outputs),
|
json.dumps(capability.outputs),
|
||||||
|
capability.primary_class or "capability",
|
||||||
|
self._attributes_to_json(capability.attributes),
|
||||||
capability.confidence,
|
capability.confidence,
|
||||||
self._source_refs_to_json(capability.source_refs),
|
self._source_refs_to_json(capability.source_refs),
|
||||||
),
|
),
|
||||||
@@ -400,8 +460,8 @@ class RegistryStore:
|
|||||||
"""
|
"""
|
||||||
INSERT INTO candidate_features
|
INSERT INTO candidate_features
|
||||||
(repository_id, analysis_run_id, capability_id, name, type,
|
(repository_id, analysis_run_id, capability_id, name, type,
|
||||||
location, confidence, source_refs)
|
primary_class, attributes, location, confidence, source_refs)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
repository_id,
|
repository_id,
|
||||||
@@ -409,6 +469,10 @@ class RegistryStore:
|
|||||||
capability_id,
|
capability_id,
|
||||||
feature.name,
|
feature.name,
|
||||||
feature.type,
|
feature.type,
|
||||||
|
feature.primary_class or feature.type,
|
||||||
|
self._attributes_to_json(
|
||||||
|
feature.attributes or [feature.type]
|
||||||
|
),
|
||||||
feature.location,
|
feature.location,
|
||||||
feature.confidence,
|
feature.confidence,
|
||||||
self._source_refs_to_json(feature.source_refs),
|
self._source_refs_to_json(feature.source_refs),
|
||||||
@@ -448,7 +512,8 @@ class RegistryStore:
|
|||||||
with self.connect() as connection:
|
with self.connect() as connection:
|
||||||
ability_rows = connection.execute(
|
ability_rows = connection.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, name, description, confidence, status, source_refs
|
SELECT id, name, description, primary_class, attributes, confidence,
|
||||||
|
status, source_refs
|
||||||
FROM candidate_abilities
|
FROM candidate_abilities
|
||||||
WHERE repository_id = ? AND analysis_run_id = ?
|
WHERE repository_id = ? AND analysis_run_id = ?
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
@@ -458,7 +523,7 @@ class RegistryStore:
|
|||||||
capability_rows = connection.execute(
|
capability_rows = connection.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, ability_id, name, description, inputs, outputs,
|
SELECT id, ability_id, name, description, inputs, outputs,
|
||||||
confidence, status, source_refs
|
primary_class, attributes, confidence, status, source_refs
|
||||||
FROM candidate_capabilities
|
FROM candidate_capabilities
|
||||||
WHERE repository_id = ? AND analysis_run_id = ?
|
WHERE repository_id = ? AND analysis_run_id = ?
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
@@ -467,8 +532,8 @@ class RegistryStore:
|
|||||||
).fetchall()
|
).fetchall()
|
||||||
feature_rows = connection.execute(
|
feature_rows = connection.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, capability_id, name, type, location, confidence,
|
SELECT id, capability_id, name, type, primary_class, attributes,
|
||||||
status, source_refs
|
location, confidence, status, source_refs
|
||||||
FROM candidate_features
|
FROM candidate_features
|
||||||
WHERE repository_id = ? AND analysis_run_id = ?
|
WHERE repository_id = ? AND analysis_run_id = ?
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
@@ -498,6 +563,8 @@ class RegistryStore:
|
|||||||
status=row["status"],
|
status=row["status"],
|
||||||
source_refs=self._source_refs_from_json(row["source_refs"]),
|
source_refs=self._source_refs_from_json(row["source_refs"]),
|
||||||
confidence_label=confidence_label(row["confidence"]),
|
confidence_label=confidence_label(row["confidence"]),
|
||||||
|
primary_class=row["primary_class"] or row["type"],
|
||||||
|
attributes=self._attributes_from_json(row["attributes"]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -531,6 +598,8 @@ class RegistryStore:
|
|||||||
status=row["status"],
|
status=row["status"],
|
||||||
source_refs=self._source_refs_from_json(row["source_refs"]),
|
source_refs=self._source_refs_from_json(row["source_refs"]),
|
||||||
confidence_label=confidence_label(row["confidence"]),
|
confidence_label=confidence_label(row["confidence"]),
|
||||||
|
primary_class=row["primary_class"] or "capability",
|
||||||
|
attributes=self._attributes_from_json(row["attributes"]),
|
||||||
features=features_by_capability.get(row["id"], []),
|
features=features_by_capability.get(row["id"], []),
|
||||||
evidence=evidence_by_capability.get(row["id"], []),
|
evidence=evidence_by_capability.get(row["id"], []),
|
||||||
)
|
)
|
||||||
@@ -545,6 +614,8 @@ class RegistryStore:
|
|||||||
status=row["status"],
|
status=row["status"],
|
||||||
source_refs=self._source_refs_from_json(row["source_refs"]),
|
source_refs=self._source_refs_from_json(row["source_refs"]),
|
||||||
confidence_label=confidence_label(row["confidence"]),
|
confidence_label=confidence_label(row["confidence"]),
|
||||||
|
primary_class=row["primary_class"] or "ability",
|
||||||
|
attributes=self._attributes_from_json(row["attributes"]),
|
||||||
capabilities=capabilities_by_ability.get(row["id"], []),
|
capabilities=capabilities_by_ability.get(row["id"], []),
|
||||||
)
|
)
|
||||||
for row in ability_rows
|
for row in ability_rows
|
||||||
@@ -861,17 +932,22 @@ class RegistryStore:
|
|||||||
name: str,
|
name: str,
|
||||||
description: str,
|
description: str,
|
||||||
confidence: float,
|
confidence: float,
|
||||||
|
primary_class: str = "ability",
|
||||||
|
attributes: list[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
with self.connect() as connection:
|
with self.connect() as connection:
|
||||||
cursor = connection.execute(
|
cursor = connection.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE candidate_abilities
|
UPDATE candidate_abilities
|
||||||
SET name = ?, description = ?, confidence = ?
|
SET name = ?, description = ?, primary_class = ?, attributes = ?,
|
||||||
|
confidence = ?
|
||||||
WHERE id = ? AND repository_id = ? AND analysis_run_id = ?
|
WHERE id = ? AND repository_id = ? AND analysis_run_id = ?
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
primary_class or "ability",
|
||||||
|
self._attributes_to_json(attributes or []),
|
||||||
confidence,
|
confidence,
|
||||||
candidate_ability_id,
|
candidate_ability_id,
|
||||||
repository_id,
|
repository_id,
|
||||||
@@ -894,17 +970,22 @@ class RegistryStore:
|
|||||||
name: str,
|
name: str,
|
||||||
description: str,
|
description: str,
|
||||||
confidence: float,
|
confidence: float,
|
||||||
|
primary_class: str = "capability",
|
||||||
|
attributes: list[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
with self.connect() as connection:
|
with self.connect() as connection:
|
||||||
cursor = connection.execute(
|
cursor = connection.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE candidate_capabilities
|
UPDATE candidate_capabilities
|
||||||
SET name = ?, description = ?, confidence = ?
|
SET name = ?, description = ?, primary_class = ?, attributes = ?,
|
||||||
|
confidence = ?
|
||||||
WHERE id = ? AND repository_id = ? AND analysis_run_id = ?
|
WHERE id = ? AND repository_id = ? AND analysis_run_id = ?
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
primary_class or "capability",
|
||||||
|
self._attributes_to_json(attributes or []),
|
||||||
confidence,
|
confidence,
|
||||||
candidate_capability_id,
|
candidate_capability_id,
|
||||||
repository_id,
|
repository_id,
|
||||||
@@ -918,6 +999,46 @@ class RegistryStore:
|
|||||||
f"{repository_id} analysis run {analysis_run_id}"
|
f"{repository_id} analysis run {analysis_run_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def update_candidate_feature(
|
||||||
|
self,
|
||||||
|
repository_id: int,
|
||||||
|
analysis_run_id: int,
|
||||||
|
candidate_feature_id: int,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
type: str,
|
||||||
|
location: str,
|
||||||
|
confidence: float,
|
||||||
|
primary_class: str | None = None,
|
||||||
|
attributes: list[str] | None = None,
|
||||||
|
) -> None:
|
||||||
|
with self.connect() as connection:
|
||||||
|
cursor = connection.execute(
|
||||||
|
"""
|
||||||
|
UPDATE candidate_features
|
||||||
|
SET name = ?, type = ?, primary_class = ?, attributes = ?,
|
||||||
|
location = ?, confidence = ?
|
||||||
|
WHERE id = ? AND repository_id = ? AND analysis_run_id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
primary_class or type,
|
||||||
|
self._attributes_to_json(attributes or [type]),
|
||||||
|
location,
|
||||||
|
confidence,
|
||||||
|
candidate_feature_id,
|
||||||
|
repository_id,
|
||||||
|
analysis_run_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if cursor.rowcount == 0:
|
||||||
|
raise NotFoundError(
|
||||||
|
"candidate feature "
|
||||||
|
f"{candidate_feature_id} was not found for repository "
|
||||||
|
f"{repository_id} analysis run {analysis_run_id}"
|
||||||
|
)
|
||||||
|
|
||||||
def relink_candidate_capability(
|
def relink_candidate_capability(
|
||||||
self,
|
self,
|
||||||
repository_id: int,
|
repository_id: int,
|
||||||
@@ -1604,15 +1725,24 @@ class RegistryStore:
|
|||||||
name: str,
|
name: str,
|
||||||
description: str,
|
description: str,
|
||||||
confidence: float,
|
confidence: float,
|
||||||
|
primary_class: str = "ability",
|
||||||
|
attributes: list[str] | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
with self.connect() as connection:
|
with self.connect() as connection:
|
||||||
cursor = connection.execute(
|
cursor = connection.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO approved_abilities
|
INSERT INTO approved_abilities
|
||||||
(repository_id, name, description, confidence)
|
(repository_id, name, description, primary_class, attributes, confidence)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(repository_id, name, description, confidence),
|
(
|
||||||
|
repository_id,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
primary_class or "ability",
|
||||||
|
self._attributes_to_json(attributes or []),
|
||||||
|
confidence,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return int(cursor.lastrowid)
|
return int(cursor.lastrowid)
|
||||||
|
|
||||||
@@ -1638,6 +1768,8 @@ class RegistryStore:
|
|||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
confidence: float | None = None,
|
confidence: float | None = None,
|
||||||
|
primary_class: str | None = None,
|
||||||
|
attributes: list[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._update_approved_row(
|
self._update_approved_row(
|
||||||
table="approved_abilities",
|
table="approved_abilities",
|
||||||
@@ -1648,6 +1780,12 @@ class RegistryStore:
|
|||||||
"name": name,
|
"name": name,
|
||||||
"description": description,
|
"description": description,
|
||||||
"confidence": confidence,
|
"confidence": confidence,
|
||||||
|
"primary_class": primary_class,
|
||||||
|
"attributes": (
|
||||||
|
self._attributes_to_json(attributes)
|
||||||
|
if attributes is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1669,13 +1807,16 @@ class RegistryStore:
|
|||||||
inputs: list[str],
|
inputs: list[str],
|
||||||
outputs: list[str],
|
outputs: list[str],
|
||||||
confidence: float,
|
confidence: float,
|
||||||
|
primary_class: str = "capability",
|
||||||
|
attributes: list[str] | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
with self.connect() as connection:
|
with self.connect() as connection:
|
||||||
cursor = connection.execute(
|
cursor = connection.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO approved_capabilities
|
INSERT INTO approved_capabilities
|
||||||
(repository_id, ability_id, name, description, inputs, outputs, confidence)
|
(repository_id, ability_id, name, description, inputs, outputs,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
primary_class, attributes, confidence)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
repository_id,
|
repository_id,
|
||||||
@@ -1684,6 +1825,8 @@ class RegistryStore:
|
|||||||
description,
|
description,
|
||||||
json.dumps(inputs),
|
json.dumps(inputs),
|
||||||
json.dumps(outputs),
|
json.dumps(outputs),
|
||||||
|
primary_class or "capability",
|
||||||
|
self._attributes_to_json(attributes or []),
|
||||||
confidence,
|
confidence,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1713,6 +1856,8 @@ class RegistryStore:
|
|||||||
inputs: list[str] | None = None,
|
inputs: list[str] | None = None,
|
||||||
outputs: list[str] | None = None,
|
outputs: list[str] | None = None,
|
||||||
confidence: float | None = None,
|
confidence: float | None = None,
|
||||||
|
primary_class: str | None = None,
|
||||||
|
attributes: list[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._update_approved_row(
|
self._update_approved_row(
|
||||||
table="approved_capabilities",
|
table="approved_capabilities",
|
||||||
@@ -1725,6 +1870,12 @@ class RegistryStore:
|
|||||||
"inputs": json.dumps(inputs) if inputs is not None else None,
|
"inputs": json.dumps(inputs) if inputs is not None else None,
|
||||||
"outputs": json.dumps(outputs) if outputs is not None else None,
|
"outputs": json.dumps(outputs) if outputs is not None else None,
|
||||||
"confidence": confidence,
|
"confidence": confidence,
|
||||||
|
"primary_class": primary_class,
|
||||||
|
"attributes": (
|
||||||
|
self._attributes_to_json(attributes)
|
||||||
|
if attributes is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1746,19 +1897,24 @@ class RegistryStore:
|
|||||||
location: str,
|
location: str,
|
||||||
confidence: float,
|
confidence: float,
|
||||||
source_refs: list[SourceReference] | None = None,
|
source_refs: list[SourceReference] | None = None,
|
||||||
|
primary_class: str | None = None,
|
||||||
|
attributes: list[str] | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
with self.connect() as connection:
|
with self.connect() as connection:
|
||||||
cursor = connection.execute(
|
cursor = connection.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO approved_features
|
INSERT INTO approved_features
|
||||||
(repository_id, capability_id, name, type, location, confidence, source_refs)
|
(repository_id, capability_id, name, type, primary_class, attributes,
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
location, confidence, source_refs)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
repository_id,
|
repository_id,
|
||||||
capability_id,
|
capability_id,
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
|
primary_class or type,
|
||||||
|
self._attributes_to_json(attributes or [type]),
|
||||||
location,
|
location,
|
||||||
confidence,
|
confidence,
|
||||||
self._source_refs_to_json(source_refs or []),
|
self._source_refs_to_json(source_refs or []),
|
||||||
@@ -1775,6 +1931,8 @@ class RegistryStore:
|
|||||||
type: str | None = None,
|
type: str | None = None,
|
||||||
location: str | None = None,
|
location: str | None = None,
|
||||||
confidence: float | None = None,
|
confidence: float | None = None,
|
||||||
|
primary_class: str | None = None,
|
||||||
|
attributes: list[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._update_approved_row(
|
self._update_approved_row(
|
||||||
table="approved_features",
|
table="approved_features",
|
||||||
@@ -1784,6 +1942,12 @@ class RegistryStore:
|
|||||||
values={
|
values={
|
||||||
"name": name,
|
"name": name,
|
||||||
"type": type,
|
"type": type,
|
||||||
|
"primary_class": primary_class,
|
||||||
|
"attributes": (
|
||||||
|
self._attributes_to_json(attributes)
|
||||||
|
if attributes is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
"location": location,
|
"location": location,
|
||||||
"confidence": confidence,
|
"confidence": confidence,
|
||||||
},
|
},
|
||||||
@@ -1968,13 +2132,15 @@ class RegistryStore:
|
|||||||
ability_cursor = connection.execute(
|
ability_cursor = connection.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO approved_abilities
|
INSERT INTO approved_abilities
|
||||||
(repository_id, name, description, confidence)
|
(repository_id, name, description, primary_class, attributes, confidence)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
repository_id,
|
repository_id,
|
||||||
ability.name,
|
ability.name,
|
||||||
ability.description,
|
ability.description,
|
||||||
|
ability.primary_class or "ability",
|
||||||
|
self._attributes_to_json(ability.attributes),
|
||||||
ability.confidence,
|
ability.confidence,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -1986,8 +2152,8 @@ class RegistryStore:
|
|||||||
"""
|
"""
|
||||||
INSERT INTO approved_capabilities
|
INSERT INTO approved_capabilities
|
||||||
(repository_id, ability_id, name, description, inputs, outputs,
|
(repository_id, ability_id, name, description, inputs, outputs,
|
||||||
confidence)
|
primary_class, attributes, confidence)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
repository_id,
|
repository_id,
|
||||||
@@ -1996,6 +2162,8 @@ class RegistryStore:
|
|||||||
capability.description,
|
capability.description,
|
||||||
json.dumps(capability.inputs),
|
json.dumps(capability.inputs),
|
||||||
json.dumps(capability.outputs),
|
json.dumps(capability.outputs),
|
||||||
|
capability.primary_class or "capability",
|
||||||
|
self._attributes_to_json(capability.attributes),
|
||||||
capability.confidence,
|
capability.confidence,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -2006,15 +2174,19 @@ class RegistryStore:
|
|||||||
connection.execute(
|
connection.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO approved_features
|
INSERT INTO approved_features
|
||||||
(repository_id, capability_id, name, type, location,
|
(repository_id, capability_id, name, type, primary_class,
|
||||||
confidence, source_refs)
|
attributes, location, confidence, source_refs)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
repository_id,
|
repository_id,
|
||||||
approved_capability_id,
|
approved_capability_id,
|
||||||
feature.name,
|
feature.name,
|
||||||
feature.type,
|
feature.type,
|
||||||
|
feature.primary_class or feature.type,
|
||||||
|
self._attributes_to_json(
|
||||||
|
feature.attributes or [feature.type]
|
||||||
|
),
|
||||||
feature.location,
|
feature.location,
|
||||||
feature.confidence,
|
feature.confidence,
|
||||||
self._source_refs_to_json(feature.source_refs),
|
self._source_refs_to_json(feature.source_refs),
|
||||||
@@ -2051,7 +2223,7 @@ class RegistryStore:
|
|||||||
with self.connect() as connection:
|
with self.connect() as connection:
|
||||||
ability_rows = connection.execute(
|
ability_rows = connection.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, name, description, confidence
|
SELECT id, name, description, primary_class, attributes, confidence
|
||||||
FROM approved_abilities
|
FROM approved_abilities
|
||||||
WHERE repository_id = ?
|
WHERE repository_id = ?
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
@@ -2060,7 +2232,8 @@ class RegistryStore:
|
|||||||
).fetchall()
|
).fetchall()
|
||||||
capability_rows = connection.execute(
|
capability_rows = connection.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, ability_id, name, description, inputs, outputs, confidence
|
SELECT id, ability_id, name, description, inputs, outputs,
|
||||||
|
primary_class, attributes, confidence
|
||||||
FROM approved_capabilities
|
FROM approved_capabilities
|
||||||
WHERE repository_id = ?
|
WHERE repository_id = ?
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
@@ -2069,7 +2242,8 @@ class RegistryStore:
|
|||||||
).fetchall()
|
).fetchall()
|
||||||
feature_rows = connection.execute(
|
feature_rows = connection.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id, capability_id, name, type, location, confidence, source_refs
|
SELECT id, capability_id, name, type, primary_class, attributes,
|
||||||
|
location, confidence, source_refs
|
||||||
FROM approved_features
|
FROM approved_features
|
||||||
WHERE repository_id = ?
|
WHERE repository_id = ?
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
@@ -2098,6 +2272,8 @@ class RegistryStore:
|
|||||||
confidence=row["confidence"],
|
confidence=row["confidence"],
|
||||||
confidence_label=confidence_label(row["confidence"]),
|
confidence_label=confidence_label(row["confidence"]),
|
||||||
source_refs=self._source_refs_from_json(row["source_refs"]),
|
source_refs=self._source_refs_from_json(row["source_refs"]),
|
||||||
|
primary_class=row["primary_class"] or row["type"],
|
||||||
|
attributes=self._attributes_from_json(row["attributes"]),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2128,6 +2304,8 @@ class RegistryStore:
|
|||||||
outputs=json.loads(row["outputs"]),
|
outputs=json.loads(row["outputs"]),
|
||||||
confidence=row["confidence"],
|
confidence=row["confidence"],
|
||||||
confidence_label=confidence_label(row["confidence"]),
|
confidence_label=confidence_label(row["confidence"]),
|
||||||
|
primary_class=row["primary_class"] or "capability",
|
||||||
|
attributes=self._attributes_from_json(row["attributes"]),
|
||||||
features=features_by_capability.get(row["id"], []),
|
features=features_by_capability.get(row["id"], []),
|
||||||
evidence=evidence_by_capability.get(row["id"], []),
|
evidence=evidence_by_capability.get(row["id"], []),
|
||||||
)
|
)
|
||||||
@@ -2140,6 +2318,8 @@ class RegistryStore:
|
|||||||
description=row["description"],
|
description=row["description"],
|
||||||
confidence=row["confidence"],
|
confidence=row["confidence"],
|
||||||
confidence_label=confidence_label(row["confidence"]),
|
confidence_label=confidence_label(row["confidence"]),
|
||||||
|
primary_class=row["primary_class"] or "ability",
|
||||||
|
attributes=self._attributes_from_json(row["attributes"]),
|
||||||
capabilities=capabilities_by_ability.get(row["id"], []),
|
capabilities=capabilities_by_ability.get(row["id"], []),
|
||||||
)
|
)
|
||||||
for row in ability_rows
|
for row in ability_rows
|
||||||
@@ -2578,6 +2758,17 @@ class RegistryStore:
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _attributes_to_json(self, attributes: list[str]) -> str:
|
||||||
|
return json.dumps([item.strip() for item in attributes if item.strip()])
|
||||||
|
|
||||||
|
def _attributes_from_json(self, value: str) -> list[str]:
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
parsed = json.loads(value)
|
||||||
|
if not isinstance(parsed, list):
|
||||||
|
return []
|
||||||
|
return [str(item) for item in parsed if str(item).strip()]
|
||||||
|
|
||||||
def _source_refs_from_json(self, value: str) -> list[SourceReference]:
|
def _source_refs_from_json(self, value: str) -> list[SourceReference]:
|
||||||
return [
|
return [
|
||||||
SourceReference(
|
SourceReference(
|
||||||
|
|||||||
@@ -495,6 +495,8 @@ class CandidateFeatureResponse(BaseModel):
|
|||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
type: str
|
type: str
|
||||||
|
primary_class: str
|
||||||
|
attributes: list[str]
|
||||||
location: str
|
location: str
|
||||||
confidence: float
|
confidence: float
|
||||||
status: str
|
status: str
|
||||||
@@ -508,6 +510,8 @@ class CandidateCapabilityResponse(BaseModel):
|
|||||||
description: str
|
description: str
|
||||||
inputs: list[str]
|
inputs: list[str]
|
||||||
outputs: list[str]
|
outputs: list[str]
|
||||||
|
primary_class: str
|
||||||
|
attributes: list[str]
|
||||||
confidence: float
|
confidence: float
|
||||||
status: str
|
status: str
|
||||||
source_refs: list[SourceReferenceResponse]
|
source_refs: list[SourceReferenceResponse]
|
||||||
@@ -520,6 +524,8 @@ class CandidateAbilityResponse(BaseModel):
|
|||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
|
primary_class: str
|
||||||
|
attributes: list[str]
|
||||||
confidence: float
|
confidence: float
|
||||||
status: str
|
status: str
|
||||||
source_refs: list[SourceReferenceResponse]
|
source_refs: list[SourceReferenceResponse]
|
||||||
@@ -689,6 +695,8 @@ class FeatureResponse(BaseModel):
|
|||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
type: str
|
type: str
|
||||||
|
primary_class: str
|
||||||
|
attributes: list[str]
|
||||||
location: str
|
location: str
|
||||||
confidence: float
|
confidence: float
|
||||||
confidence_label: str
|
confidence_label: str
|
||||||
@@ -701,6 +709,8 @@ class CapabilityResponse(BaseModel):
|
|||||||
description: str
|
description: str
|
||||||
inputs: list[str]
|
inputs: list[str]
|
||||||
outputs: list[str]
|
outputs: list[str]
|
||||||
|
primary_class: str
|
||||||
|
attributes: list[str]
|
||||||
confidence: float
|
confidence: float
|
||||||
confidence_label: str
|
confidence_label: str
|
||||||
features: list[FeatureResponse]
|
features: list[FeatureResponse]
|
||||||
@@ -711,6 +721,8 @@ class AbilityResponse(BaseModel):
|
|||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
description: str
|
description: str
|
||||||
|
primary_class: str
|
||||||
|
attributes: list[str]
|
||||||
confidence: float
|
confidence: float
|
||||||
confidence_label: str
|
confidence_label: str
|
||||||
capabilities: list[CapabilityResponse]
|
capabilities: list[CapabilityResponse]
|
||||||
|
|||||||
@@ -719,6 +719,8 @@ def repository_detail(
|
|||||||
<h3>Add Ability</h3>
|
<h3>Add Ability</h3>
|
||||||
<label>Name <input name="name" required></label>
|
<label>Name <input name="name" required></label>
|
||||||
<label>Description <textarea name="description" rows="2"></textarea></label>
|
<label>Description <textarea name="description" rows="2"></textarea></label>
|
||||||
|
<label>Primary class <input name="primary_class" value="ability" required></label>
|
||||||
|
<label>Attributes <input name="attributes" placeholder="Comma-separated"></label>
|
||||||
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
|
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
|
||||||
<button type="submit">Add Ability</button>
|
<button type="submit">Add Ability</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -729,6 +731,8 @@ def repository_detail(
|
|||||||
<label>Description <textarea name="description" rows="2"></textarea></label>
|
<label>Description <textarea name="description" rows="2"></textarea></label>
|
||||||
<label>Inputs <input name="inputs" placeholder="Comma-separated"></label>
|
<label>Inputs <input name="inputs" placeholder="Comma-separated"></label>
|
||||||
<label>Outputs <input name="outputs" placeholder="Comma-separated"></label>
|
<label>Outputs <input name="outputs" placeholder="Comma-separated"></label>
|
||||||
|
<label>Primary class <input name="primary_class" value="capability" required></label>
|
||||||
|
<label>Attributes <input name="attributes" placeholder="Comma-separated"></label>
|
||||||
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
|
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
|
||||||
<button type="submit">Add Capability</button>
|
<button type="submit">Add Capability</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -737,6 +741,8 @@ def repository_detail(
|
|||||||
<label>Capability ID <input name="capability_id" type="number" min="1" required></label>
|
<label>Capability ID <input name="capability_id" type="number" min="1" required></label>
|
||||||
<label>Name <input name="name" required></label>
|
<label>Name <input name="name" required></label>
|
||||||
<label>Type <input name="type" required></label>
|
<label>Type <input name="type" required></label>
|
||||||
|
<label>Primary class <input name="primary_class" placeholder="Defaults to type"></label>
|
||||||
|
<label>Attributes <input name="attributes" placeholder="Comma-separated; defaults to type"></label>
|
||||||
<label>Location <input name="location"></label>
|
<label>Location <input name="location"></label>
|
||||||
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
|
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
|
||||||
<button type="submit">Add Feature</button>
|
<button type="submit">Add Feature</button>
|
||||||
@@ -835,6 +841,8 @@ def create_ability_from_form(
|
|||||||
name: str = Form(...),
|
name: str = Form(...),
|
||||||
description: str = Form(""),
|
description: str = Form(""),
|
||||||
confidence: float = Form(1.0),
|
confidence: float = Form(1.0),
|
||||||
|
primary_class: str = Form("ability"),
|
||||||
|
attributes: str = Form(""),
|
||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
service.add_ability(
|
service.add_ability(
|
||||||
@@ -842,6 +850,8 @@ def create_ability_from_form(
|
|||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class or "ability",
|
||||||
|
attributes=split_csv(attributes),
|
||||||
)
|
)
|
||||||
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
||||||
|
|
||||||
@@ -855,6 +865,8 @@ def create_capability_from_form(
|
|||||||
inputs: str = Form(""),
|
inputs: str = Form(""),
|
||||||
outputs: str = Form(""),
|
outputs: str = Form(""),
|
||||||
confidence: float = Form(1.0),
|
confidence: float = Form(1.0),
|
||||||
|
primary_class: str = Form("capability"),
|
||||||
|
attributes: str = Form(""),
|
||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
service.add_capability(
|
service.add_capability(
|
||||||
@@ -865,6 +877,8 @@ def create_capability_from_form(
|
|||||||
inputs=split_csv(inputs),
|
inputs=split_csv(inputs),
|
||||||
outputs=split_csv(outputs),
|
outputs=split_csv(outputs),
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class or "capability",
|
||||||
|
attributes=split_csv(attributes),
|
||||||
)
|
)
|
||||||
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
||||||
|
|
||||||
@@ -877,6 +891,8 @@ def create_feature_from_form(
|
|||||||
type: str = Form(...),
|
type: str = Form(...),
|
||||||
location: str = Form(""),
|
location: str = Form(""),
|
||||||
confidence: float = Form(1.0),
|
confidence: float = Form(1.0),
|
||||||
|
primary_class: str = Form(""),
|
||||||
|
attributes: str = Form(""),
|
||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
service.add_feature(
|
service.add_feature(
|
||||||
@@ -886,6 +902,8 @@ def create_feature_from_form(
|
|||||||
type=type,
|
type=type,
|
||||||
location=location,
|
location=location,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class or type,
|
||||||
|
attributes=split_csv(attributes) or [type],
|
||||||
)
|
)
|
||||||
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
||||||
|
|
||||||
@@ -924,6 +942,8 @@ def edit_ability_from_form(
|
|||||||
name: str = Form(...),
|
name: str = Form(...),
|
||||||
description: str = Form(""),
|
description: str = Form(""),
|
||||||
confidence: float = Form(1.0),
|
confidence: float = Form(1.0),
|
||||||
|
primary_class: str = Form("ability"),
|
||||||
|
attributes: str = Form(""),
|
||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
service.update_ability(
|
service.update_ability(
|
||||||
@@ -932,6 +952,8 @@ def edit_ability_from_form(
|
|||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class or "ability",
|
||||||
|
attributes=split_csv(attributes),
|
||||||
)
|
)
|
||||||
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
||||||
|
|
||||||
@@ -955,6 +977,8 @@ def edit_capability_from_form(
|
|||||||
inputs: str = Form(""),
|
inputs: str = Form(""),
|
||||||
outputs: str = Form(""),
|
outputs: str = Form(""),
|
||||||
confidence: float = Form(1.0),
|
confidence: float = Form(1.0),
|
||||||
|
primary_class: str = Form("capability"),
|
||||||
|
attributes: str = Form(""),
|
||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
service.update_capability(
|
service.update_capability(
|
||||||
@@ -965,6 +989,8 @@ def edit_capability_from_form(
|
|||||||
inputs=split_csv(inputs),
|
inputs=split_csv(inputs),
|
||||||
outputs=split_csv(outputs),
|
outputs=split_csv(outputs),
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class or "capability",
|
||||||
|
attributes=split_csv(attributes),
|
||||||
)
|
)
|
||||||
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
||||||
|
|
||||||
@@ -987,6 +1013,8 @@ def edit_feature_from_form(
|
|||||||
type: str = Form(...),
|
type: str = Form(...),
|
||||||
location: str = Form(""),
|
location: str = Form(""),
|
||||||
confidence: float = Form(1.0),
|
confidence: float = Form(1.0),
|
||||||
|
primary_class: str = Form(""),
|
||||||
|
attributes: str = Form(""),
|
||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
service.update_feature(
|
service.update_feature(
|
||||||
@@ -996,6 +1024,8 @@ def edit_feature_from_form(
|
|||||||
type=type,
|
type=type,
|
||||||
location=location,
|
location=location,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class or type,
|
||||||
|
attributes=split_csv(attributes) or [type],
|
||||||
)
|
)
|
||||||
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
|
||||||
|
|
||||||
@@ -1208,6 +1238,7 @@ def repository_element_listing(
|
|||||||
type: str = Query("abilities"),
|
type: str = Query("abilities"),
|
||||||
q: str = Query(""),
|
q: str = Query(""),
|
||||||
class_filter: str = Query(""),
|
class_filter: str = Query(""),
|
||||||
|
attribute_filter: str = Query(""),
|
||||||
entry_filter: str = Query(""),
|
entry_filter: str = Query(""),
|
||||||
candidate_status_filter: str = Query("active"),
|
candidate_status_filter: str = Query("active"),
|
||||||
support_orientation_filter: str = Query(""),
|
support_orientation_filter: str = Query(""),
|
||||||
@@ -1252,13 +1283,15 @@ def repository_element_listing(
|
|||||||
elements,
|
elements,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
entry_filter,
|
"",
|
||||||
candidate_status_filter,
|
entry_filter=entry_filter,
|
||||||
|
candidate_status_filter=candidate_status_filter,
|
||||||
)
|
)
|
||||||
filtered = filter_element_rows(
|
filtered = filter_element_rows(
|
||||||
entry_scoped_elements,
|
entry_scoped_elements,
|
||||||
q,
|
q,
|
||||||
class_filter,
|
class_filter,
|
||||||
|
attribute_filter,
|
||||||
candidate_status_filter="all",
|
candidate_status_filter="all",
|
||||||
support_orientation_filter=support_orientation_filter,
|
support_orientation_filter=support_orientation_filter,
|
||||||
)
|
)
|
||||||
@@ -1283,11 +1316,13 @@ def repository_element_listing(
|
|||||||
<div class="grid">
|
<div class="grid">
|
||||||
<label>Search <input name="q" value="{escape(q)}" placeholder="Name, parent, source, or class"></label>
|
<label>Search <input name="q" value="{escape(q)}" placeholder="Name, parent, source, or class"></label>
|
||||||
<label>Class <input name="class_filter" value="{escape(class_filter)}" list="element-classes" placeholder="Any class"></label>
|
<label>Class <input name="class_filter" value="{escape(class_filter)}" list="element-classes" placeholder="Any class"></label>
|
||||||
|
<label>Attribute <input name="attribute_filter" value="{escape(attribute_filter)}" list="element-attributes" placeholder="Any attribute"></label>
|
||||||
{render_entry_filter(entry_filter) if scope != "facts" else ""}
|
{render_entry_filter(entry_filter) if scope != "facts" else ""}
|
||||||
{render_candidate_status_filter(candidate_status_filter) if scope != "facts" else ""}
|
{render_candidate_status_filter(candidate_status_filter) if scope != "facts" else ""}
|
||||||
{render_support_orientation_filter(support_orientation_filter) if type in {"supports", "evidence"} else ""}
|
{render_support_orientation_filter(support_orientation_filter) if type in {"supports", "evidence"} else ""}
|
||||||
</div>
|
</div>
|
||||||
{render_class_datalist(entry_scoped_elements)}
|
{render_class_datalist(entry_scoped_elements)}
|
||||||
|
{render_attribute_datalist(entry_scoped_elements)}
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit">Filter</button>
|
<button type="submit">Filter</button>
|
||||||
<a class="button secondary" href="{filter_action}?scope={escape(listing_scope)}&type={escape(type)}{render_analysis_run_query_suffix(analysis_run_id)}">Clear</a>
|
<a class="button secondary" href="{filter_action}?scope={escape(listing_scope)}&type={escape(type)}{render_analysis_run_query_suffix(analysis_run_id)}">Clear</a>
|
||||||
@@ -1562,6 +1597,8 @@ def edit_candidate_ability_from_form(
|
|||||||
name: str = Form(...),
|
name: str = Form(...),
|
||||||
description: str = Form(""),
|
description: str = Form(""),
|
||||||
confidence: float = Form(...),
|
confidence: float = Form(...),
|
||||||
|
primary_class: str = Form("ability"),
|
||||||
|
attributes: str = Form(""),
|
||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
service.edit_candidate_ability(
|
service.edit_candidate_ability(
|
||||||
@@ -1571,6 +1608,8 @@ def edit_candidate_ability_from_form(
|
|||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class or "ability",
|
||||||
|
attributes=split_csv(attributes),
|
||||||
notes="Edited from web UI",
|
notes="Edited from web UI",
|
||||||
)
|
)
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
@@ -1590,6 +1629,8 @@ def edit_candidate_capability_from_form(
|
|||||||
name: str = Form(...),
|
name: str = Form(...),
|
||||||
description: str = Form(""),
|
description: str = Form(""),
|
||||||
confidence: float = Form(...),
|
confidence: float = Form(...),
|
||||||
|
primary_class: str = Form("capability"),
|
||||||
|
attributes: str = Form(""),
|
||||||
service: RegistryService = Depends(get_service),
|
service: RegistryService = Depends(get_service),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
service.edit_candidate_capability(
|
service.edit_candidate_capability(
|
||||||
@@ -1599,6 +1640,42 @@ def edit_candidate_capability_from_form(
|
|||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
confidence=confidence,
|
confidence=confidence,
|
||||||
|
primary_class=primary_class or "capability",
|
||||||
|
attributes=split_csv(attributes),
|
||||||
|
notes="Edited from web UI",
|
||||||
|
)
|
||||||
|
return RedirectResponse(
|
||||||
|
f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}",
|
||||||
|
status_code=303,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
||||||
|
"/candidate-features/{candidate_feature_id}/edit"
|
||||||
|
)
|
||||||
|
def edit_candidate_feature_from_form(
|
||||||
|
repository_id: int,
|
||||||
|
analysis_run_id: int,
|
||||||
|
candidate_feature_id: int,
|
||||||
|
name: str = Form(...),
|
||||||
|
type: str = Form(...),
|
||||||
|
location: str = Form(""),
|
||||||
|
confidence: float = Form(...),
|
||||||
|
primary_class: str = Form(""),
|
||||||
|
attributes: str = Form(""),
|
||||||
|
service: RegistryService = Depends(get_service),
|
||||||
|
) -> RedirectResponse:
|
||||||
|
service.edit_candidate_feature(
|
||||||
|
repository_id,
|
||||||
|
analysis_run_id,
|
||||||
|
candidate_feature_id,
|
||||||
|
name=name,
|
||||||
|
type=type,
|
||||||
|
location=location,
|
||||||
|
confidence=confidence,
|
||||||
|
primary_class=primary_class or type,
|
||||||
|
attributes=split_csv(attributes) or [type],
|
||||||
notes="Edited from web UI",
|
notes="Edited from web UI",
|
||||||
)
|
)
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
@@ -2245,6 +2322,7 @@ def graph_element_rows(
|
|||||||
item_kind="abilities",
|
item_kind="abilities",
|
||||||
description=ability.get("description", ""),
|
description=ability.get("description", ""),
|
||||||
confidence=ability.get("confidence", 1.0),
|
confidence=ability.get("confidence", 1.0),
|
||||||
|
attributes=ability.get("attributes", []),
|
||||||
status=ability.get("status", ""),
|
status=ability.get("status", ""),
|
||||||
entry_state=entry_state,
|
entry_state=entry_state,
|
||||||
)
|
)
|
||||||
@@ -2261,6 +2339,7 @@ def graph_element_rows(
|
|||||||
item_kind="capabilities",
|
item_kind="capabilities",
|
||||||
description=capability.get("description", ""),
|
description=capability.get("description", ""),
|
||||||
confidence=capability.get("confidence", 1.0),
|
confidence=capability.get("confidence", 1.0),
|
||||||
|
attributes=capability.get("attributes", []),
|
||||||
inputs=capability.get("inputs", []),
|
inputs=capability.get("inputs", []),
|
||||||
outputs=capability.get("outputs", []),
|
outputs=capability.get("outputs", []),
|
||||||
status=capability.get("status", ""),
|
status=capability.get("status", ""),
|
||||||
@@ -2271,12 +2350,14 @@ def graph_element_rows(
|
|||||||
if item_type == "features":
|
if item_type == "features":
|
||||||
rows.append(
|
rows.append(
|
||||||
element_row(
|
element_row(
|
||||||
feature.get("type", "feature"),
|
feature.get("primary_class", feature.get("type", "feature")),
|
||||||
feature["name"],
|
feature["name"],
|
||||||
capability["name"],
|
capability["name"],
|
||||||
feature.get("source_refs", []),
|
feature.get("source_refs", []),
|
||||||
item_id=feature.get("id"),
|
item_id=feature.get("id"),
|
||||||
item_kind="features",
|
item_kind="features",
|
||||||
|
type=feature.get("type", ""),
|
||||||
|
attributes=feature.get("attributes", []),
|
||||||
confidence=feature.get("confidence", 1.0),
|
confidence=feature.get("confidence", 1.0),
|
||||||
location=feature.get("location", ""),
|
location=feature.get("location", ""),
|
||||||
status=feature.get("status", ""),
|
status=feature.get("status", ""),
|
||||||
@@ -2367,12 +2448,14 @@ def filter_element_rows(
|
|||||||
rows: list[dict],
|
rows: list[dict],
|
||||||
query: str,
|
query: str,
|
||||||
class_filter: str,
|
class_filter: str,
|
||||||
|
attribute_filter: str = "",
|
||||||
entry_filter: str = "",
|
entry_filter: str = "",
|
||||||
candidate_status_filter: str = "",
|
candidate_status_filter: str = "",
|
||||||
support_orientation_filter: str = "",
|
support_orientation_filter: str = "",
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
query = query.strip().lower()
|
query = query.strip().lower()
|
||||||
class_filter = class_filter.strip().lower()
|
class_filter = class_filter.strip().lower()
|
||||||
|
attribute_filter = attribute_filter.strip().lower()
|
||||||
entry_filter = entry_filter.strip().lower()
|
entry_filter = entry_filter.strip().lower()
|
||||||
candidate_status_filter = candidate_status_filter.strip().lower()
|
candidate_status_filter = candidate_status_filter.strip().lower()
|
||||||
support_orientation_filter = support_orientation_filter.strip().lower()
|
support_orientation_filter = support_orientation_filter.strip().lower()
|
||||||
@@ -2387,9 +2470,15 @@ def filter_element_rows(
|
|||||||
row_class = str(row["primary_class"]).lower()
|
row_class = str(row["primary_class"]).lower()
|
||||||
if class_filter and class_filter not in row_class:
|
if class_filter and class_filter not in row_class:
|
||||||
continue
|
continue
|
||||||
|
row_attributes = [str(item).lower() for item in row.get("attributes", [])]
|
||||||
|
if attribute_filter and not any(
|
||||||
|
attribute_filter in item for item in row_attributes
|
||||||
|
):
|
||||||
|
continue
|
||||||
haystack = " ".join(
|
haystack = " ".join(
|
||||||
[
|
[
|
||||||
str(row["primary_class"]),
|
str(row["primary_class"]),
|
||||||
|
" ".join(str(item) for item in row.get("attributes", [])),
|
||||||
str(row["name"]),
|
str(row["name"]),
|
||||||
str(row["parent"]),
|
str(row["parent"]),
|
||||||
str(row.get("entry_state", "")),
|
str(row.get("entry_state", "")),
|
||||||
@@ -2443,7 +2532,7 @@ def render_element_row(
|
|||||||
return f"""
|
return f"""
|
||||||
<tr>
|
<tr>
|
||||||
<td>{render_entry_badge(row)}</td>
|
<td>{render_entry_badge(row)}</td>
|
||||||
<td><span class="pill">{escape(str(row["primary_class"]))}</span></td>
|
<td><span class="pill">{escape(str(row["primary_class"]))}</span>{render_attribute_pills(row)}</td>
|
||||||
<td>{escape(str(row["name"]))}</td>
|
<td>{escape(str(row["name"]))}</td>
|
||||||
<td>{escape(str(row["parent"]))}</td>
|
<td>{escape(str(row["parent"]))}</td>
|
||||||
<td>{render_element_source_detail(row)}</td>
|
<td>{render_element_source_detail(row)}</td>
|
||||||
@@ -2452,6 +2541,20 @@ def render_element_row(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_attribute_pills(row: dict) -> str:
|
||||||
|
attributes = [
|
||||||
|
str(attribute)
|
||||||
|
for attribute in row.get("attributes", [])
|
||||||
|
if str(attribute) and str(attribute) != str(row.get("primary_class", ""))
|
||||||
|
]
|
||||||
|
if not attributes:
|
||||||
|
return ""
|
||||||
|
return "".join(
|
||||||
|
f' <span class="pill">{escape(attribute)}</span>'
|
||||||
|
for attribute in attributes
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def render_element_source_detail(row: dict) -> str:
|
def render_element_source_detail(row: dict) -> str:
|
||||||
if row.get("item_kind") == "evidence":
|
if row.get("item_kind") == "evidence":
|
||||||
target = escape(str(row.get("target_kind") or "capability"))
|
target = escape(str(row.get("target_kind") or "capability"))
|
||||||
@@ -2647,7 +2750,7 @@ def render_candidate_element_actions(
|
|||||||
)
|
)
|
||||||
status = row.get("status", "candidate")
|
status = row.get("status", "candidate")
|
||||||
edit = ""
|
edit = ""
|
||||||
if item_kind in {"abilities", "capabilities"}:
|
if item_kind in {"abilities", "capabilities", "features"}:
|
||||||
edit_action = (
|
edit_action = (
|
||||||
f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}"
|
||||||
f"/{collection}/{item_id}/edit"
|
f"/{collection}/{item_id}/edit"
|
||||||
@@ -2722,6 +2825,7 @@ def render_support_orientation_filter(support_orientation_filter: str) -> str:
|
|||||||
def render_element_edit_fields(row: dict) -> str:
|
def render_element_edit_fields(row: dict) -> str:
|
||||||
item_kind = row["item_kind"]
|
item_kind = row["item_kind"]
|
||||||
name = escape(str(row["name"]))
|
name = escape(str(row["name"]))
|
||||||
|
classification = render_classification_edit_fields(row)
|
||||||
if item_kind == "scope":
|
if item_kind == "scope":
|
||||||
return f"""
|
return f"""
|
||||||
<label>Name <input name="name" value="{name}" required></label>
|
<label>Name <input name="name" value="{name}" required></label>
|
||||||
@@ -2734,6 +2838,7 @@ def render_element_edit_fields(row: dict) -> str:
|
|||||||
return f"""
|
return f"""
|
||||||
<label>Name <input name="name" value="{name}" required></label>
|
<label>Name <input name="name" value="{name}" required></label>
|
||||||
<label>Type <input name="type" value="{feature_type}" required></label>
|
<label>Type <input name="type" value="{feature_type}" required></label>
|
||||||
|
{classification}
|
||||||
<label>Location <input name="location" value="{location}"></label>
|
<label>Location <input name="location" value="{location}"></label>
|
||||||
"""
|
"""
|
||||||
if item_kind == "evidence":
|
if item_kind == "evidence":
|
||||||
@@ -2746,7 +2851,10 @@ def render_element_edit_fields(row: dict) -> str:
|
|||||||
<label>Reference ID <input name="reference_id" type="number" min="1" value="{row.get('reference_id') or ''}"></label>
|
<label>Reference ID <input name="reference_id" type="number" min="1" value="{row.get('reference_id') or ''}"></label>
|
||||||
<label>Strength <input name="strength" value="{escape(str(row.get("strength", "medium")))}" required></label>
|
<label>Strength <input name="strength" value="{escape(str(row.get("strength", "medium")))}" required></label>
|
||||||
"""
|
"""
|
||||||
return f'<label>Name <input name="name" value="{name}" required></label>'
|
return f"""
|
||||||
|
<label>Name <input name="name" value="{name}" required></label>
|
||||||
|
{classification}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def render_element_hidden_fields(row: dict) -> str:
|
def render_element_hidden_fields(row: dict) -> str:
|
||||||
@@ -2774,7 +2882,17 @@ def render_element_hidden_fields(row: dict) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def render_candidate_edit_fields(row: dict) -> str:
|
def render_candidate_edit_fields(row: dict) -> str:
|
||||||
return f'<label>Name <input name="name" value="{escape(str(row["name"]))}" required></label>'
|
if row.get("item_kind") == "features":
|
||||||
|
return f"""
|
||||||
|
<label>Name <input name="name" value="{escape(str(row["name"]))}" required></label>
|
||||||
|
<label>Type <input name="type" value="{escape(str(row.get("type", row.get("primary_class", ""))))}" required></label>
|
||||||
|
{render_classification_edit_fields(row)}
|
||||||
|
<label>Location <input name="location" value="{escape(str(row.get("location", "")))}"></label>
|
||||||
|
"""
|
||||||
|
return f"""
|
||||||
|
<label>Name <input name="name" value="{escape(str(row["name"]))}" required></label>
|
||||||
|
{render_classification_edit_fields(row)}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def render_candidate_hidden_fields(row: dict) -> str:
|
def render_candidate_hidden_fields(row: dict) -> str:
|
||||||
@@ -2784,6 +2902,17 @@ def render_candidate_hidden_fields(row: dict) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def render_classification_edit_fields(row: dict) -> str:
|
||||||
|
return f"""
|
||||||
|
<label>Primary class <input name="primary_class" value="{escape(str(row.get("primary_class", "")))}" required></label>
|
||||||
|
<label>Attributes <input name="attributes" value="{escape(attributes_text(row))}" placeholder="Comma-separated"></label>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def attributes_text(row: dict) -> str:
|
||||||
|
return ", ".join(str(item) for item in row.get("attributes", []) if str(item))
|
||||||
|
|
||||||
|
|
||||||
def render_class_datalist(rows: list[dict]) -> str:
|
def render_class_datalist(rows: list[dict]) -> str:
|
||||||
classes = sorted({str(row["primary_class"]) for row in rows if row["primary_class"]})
|
classes = sorted({str(row["primary_class"]) for row in rows if row["primary_class"]})
|
||||||
options = "".join(
|
options = "".join(
|
||||||
@@ -2793,6 +2922,22 @@ def render_class_datalist(rows: list[dict]) -> str:
|
|||||||
return f'<datalist id="element-classes">{options}</datalist>'
|
return f'<datalist id="element-classes">{options}</datalist>'
|
||||||
|
|
||||||
|
|
||||||
|
def render_attribute_datalist(rows: list[dict]) -> str:
|
||||||
|
attributes = sorted(
|
||||||
|
{
|
||||||
|
str(attribute)
|
||||||
|
for row in rows
|
||||||
|
for attribute in row.get("attributes", [])
|
||||||
|
if str(attribute)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
options = "".join(
|
||||||
|
f'<option value="{escape(item)}"></option>'
|
||||||
|
for item in attributes
|
||||||
|
)
|
||||||
|
return f'<datalist id="element-attributes">{options}</datalist>'
|
||||||
|
|
||||||
|
|
||||||
def render_optional_hidden(name: str, value: int | None) -> str:
|
def render_optional_hidden(name: str, value: int | None) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
@@ -2958,10 +3103,17 @@ def render_candidate_edit_form(
|
|||||||
f"/{collection}/{candidate['id']}/edit"
|
f"/{collection}/{candidate['id']}/edit"
|
||||||
)
|
)
|
||||||
confidence = f"{candidate['confidence']:.2f}"
|
confidence = f"{candidate['confidence']:.2f}"
|
||||||
|
extra_fields = ""
|
||||||
|
if collection == "candidate-features":
|
||||||
|
extra_fields = f"""
|
||||||
|
<label>Type <input name="type" value="{escape(candidate['type'])}" required></label>
|
||||||
|
<label>Location <input name="location" value="{escape(candidate.get('location', ''))}"></label>
|
||||||
|
"""
|
||||||
return f"""
|
return f"""
|
||||||
<form class="stack" method="post" action="{action}">
|
<form class="stack" method="post" action="{action}">
|
||||||
<label>Name <input name="name" value="{escape(candidate['name'])}" required></label>
|
<label>Name <input name="name" value="{escape(candidate['name'])}" required></label>
|
||||||
<label>Description <textarea name="description" rows="2">{escape(candidate['description'])}</textarea></label>
|
{extra_fields or f'<label>Description <textarea name="description" rows="2">{escape(candidate.get("description", ""))}</textarea></label>'}
|
||||||
|
{render_classification_edit_fields(candidate)}
|
||||||
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="{confidence}" required></label>
|
<label>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="{confidence}" required></label>
|
||||||
<button class="secondary" type="submit">Save Edit</button>
|
<button class="secondary" type="submit">Save Edit</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -3012,8 +3164,10 @@ def render_candidate_feature(
|
|||||||
<span class="pill">ID {feature["id"]}</span>
|
<span class="pill">ID {feature["id"]}</span>
|
||||||
<span class="pill">{escape(feature["status"])}</span>
|
<span class="pill">{escape(feature["status"])}</span>
|
||||||
<span class="pill">{escape(feature["type"])}</span>
|
<span class="pill">{escape(feature["type"])}</span>
|
||||||
|
<span class="pill">{escape(feature.get("primary_class", feature["type"]))}</span>
|
||||||
<span class="source">{escape(feature["location"])}</span>
|
<span class="source">{escape(feature["location"])}</span>
|
||||||
{render_candidate_reject_form('candidate-features', feature, repository_id, analysis_run_id)}
|
{render_candidate_reject_form('candidate-features', feature, repository_id, analysis_run_id)}
|
||||||
|
{render_candidate_edit_form('candidate-features', feature, repository_id, analysis_run_id)}
|
||||||
{render_candidate_relink_form('candidate-features', feature, repository_id, analysis_run_id, 'target_capability_id', 'Target capability ID')}
|
{render_candidate_relink_form('candidate-features', feature, repository_id, analysis_run_id, 'target_capability_id', 'Target capability ID')}
|
||||||
{render_candidate_merge_form('candidate-features', feature, repository_id, analysis_run_id, 'target_feature_id', 'Merge into feature ID')}
|
{render_candidate_merge_form('candidate-features', feature, repository_id, analysis_run_id, 'target_feature_id', 'Merge into feature ID')}
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -52,13 +52,19 @@ def test_candidate_generator_builds_purpose_seed_from_observed_facts():
|
|||||||
ability = graph[0]
|
ability = graph[0]
|
||||||
assert ability.name == "Route Incoming Customer Email To The Right Team"
|
assert ability.name == "Route Incoming Customer Email To The Right Team"
|
||||||
assert "Usefulness" not in ability.name
|
assert "Usefulness" not in ability.name
|
||||||
|
assert ability.primary_class == "developer-tooling"
|
||||||
|
assert "interface" in ability.attributes
|
||||||
assert ability.source_refs[0].path == "README.md"
|
assert ability.source_refs[0].path == "README.md"
|
||||||
interface_capability = ability.capabilities[0]
|
interface_capability = ability.capabilities[0]
|
||||||
assert interface_capability.name == "Expose Repository Interface"
|
assert interface_capability.name == "Expose Repository Interface"
|
||||||
|
assert interface_capability.primary_class == "interface"
|
||||||
|
assert {"surface", "api"} <= set(interface_capability.attributes)
|
||||||
assert interface_capability.confidence == 0.75
|
assert interface_capability.confidence == 0.75
|
||||||
assert interface_capability.inputs == ["HTTP request"]
|
assert interface_capability.inputs == ["HTTP request"]
|
||||||
assert interface_capability.outputs == ["HTTP response"]
|
assert interface_capability.outputs == ["HTTP response"]
|
||||||
assert interface_capability.features[0].type == "API"
|
assert interface_capability.features[0].type == "API"
|
||||||
|
assert interface_capability.features[0].primary_class == "API"
|
||||||
|
assert {"API", "surface", "http"} <= set(interface_capability.features[0].attributes)
|
||||||
assert interface_capability.features[0].name == "POST /classify"
|
assert interface_capability.features[0].name == "POST /classify"
|
||||||
assert interface_capability.features[0].location == "app.py"
|
assert interface_capability.features[0].location == "app.py"
|
||||||
assert interface_capability.evidence[0].strength == "strong"
|
assert interface_capability.evidence[0].strength == "strong"
|
||||||
@@ -273,8 +279,18 @@ def test_candidate_generator_maps_llm_provider_facts_to_capability():
|
|||||||
for capability in graph[0].capabilities
|
for capability in graph[0].capabilities
|
||||||
if capability.name == "Route LLM Requests Across Providers"
|
if capability.name == "Route LLM Requests Across Providers"
|
||||||
)
|
)
|
||||||
|
assert graph[0].primary_class == "ai-integration"
|
||||||
|
assert capability.primary_class == "llm-integration"
|
||||||
|
assert {"llm-provider", "openrouter", "claude", "fallback-policy"} <= set(
|
||||||
|
capability.attributes
|
||||||
|
)
|
||||||
feature_names = {feature.name for feature in capability.features}
|
feature_names = {feature.name for feature in capability.features}
|
||||||
assert {"Use OpenRouter Models", "Use Claude Models"} <= feature_names
|
assert {"Use OpenRouter Models", "Use Claude Models"} <= feature_names
|
||||||
assert "Configure LLM Provider Credentials" in feature_names
|
assert "Configure LLM Provider Credentials" in feature_names
|
||||||
assert "Maintain LLM Provider Registry" in feature_names
|
assert "Maintain LLM Provider Registry" in feature_names
|
||||||
assert "Apply LLM Provider Fallback Policy" in feature_names
|
assert "Apply LLM Provider Fallback Policy" in feature_names
|
||||||
|
openrouter_feature = next(
|
||||||
|
feature for feature in capability.features if feature.name == "Use OpenRouter Models"
|
||||||
|
)
|
||||||
|
assert openrouter_feature.primary_class == "integration"
|
||||||
|
assert {"llm-provider", "openrouter"} <= set(openrouter_feature.attributes)
|
||||||
|
|||||||
@@ -813,13 +813,33 @@ def test_approve_candidate_graph_publishes_ability_map_once(tmp_path):
|
|||||||
assert len(ability_map.abilities) == 1
|
assert len(ability_map.abilities) == 1
|
||||||
assert len(second_approval.abilities) == 1
|
assert len(second_approval.abilities) == 1
|
||||||
assert ability_map.abilities[0].name == "Support Example"
|
assert ability_map.abilities[0].name == "Support Example"
|
||||||
|
assert ability_map.abilities[0].primary_class == "developer-tooling"
|
||||||
|
assert ability_map.abilities[0].attributes == ["interface"]
|
||||||
|
assert ability_map.abilities[0].capabilities[0].primary_class == "interface"
|
||||||
assert ability_map.abilities[0].capabilities[0].features[0].location == "app.py"
|
assert ability_map.abilities[0].capabilities[0].features[0].location == "app.py"
|
||||||
|
assert ability_map.abilities[0].capabilities[0].features[0].primary_class == "API"
|
||||||
|
assert ability_map.abilities[0].capabilities[0].features[0].attributes == [
|
||||||
|
"API",
|
||||||
|
"surface",
|
||||||
|
"http",
|
||||||
|
]
|
||||||
assert ability_map.abilities[0].capabilities[0].features[0].source_refs
|
assert ability_map.abilities[0].capabilities[0].features[0].source_refs
|
||||||
assert ability_map.abilities[0].capabilities[0].features[0].source_refs[0].line == 3
|
assert ability_map.abilities[0].capabilities[0].features[0].source_refs[0].line == 3
|
||||||
assert ability_map.abilities[0].capabilities[0].evidence[0].source_refs
|
assert ability_map.abilities[0].capabilities[0].evidence[0].source_refs
|
||||||
|
|
||||||
candidate_graph = service.candidate_graph(repository.id, summary.analysis_run.id)
|
candidate_graph = service.candidate_graph(repository.id, summary.analysis_run.id)
|
||||||
assert candidate_graph.abilities[0].status == "approved"
|
assert candidate_graph.abilities[0].status == "approved"
|
||||||
|
assert candidate_graph.abilities[0].primary_class == "developer-tooling"
|
||||||
|
assert candidate_graph.abilities[0].capabilities[0].primary_class == "interface"
|
||||||
|
assert (
|
||||||
|
candidate_graph.abilities[0].capabilities[0].features[0].primary_class
|
||||||
|
== "API"
|
||||||
|
)
|
||||||
|
assert candidate_graph.abilities[0].capabilities[0].features[0].attributes == [
|
||||||
|
"API",
|
||||||
|
"surface",
|
||||||
|
"http",
|
||||||
|
]
|
||||||
decisions = service.list_review_decisions(repository.id, summary.analysis_run.id)
|
decisions = service.list_review_decisions(repository.id, summary.analysis_run.id)
|
||||||
assert decisions[0].action == "approve_candidate_graph"
|
assert decisions[0].action == "approve_candidate_graph"
|
||||||
assert decisions[0].notes == "Looks good for the first pass."
|
assert decisions[0].notes == "Looks good for the first pass."
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ def test_initialize_is_idempotent_and_applies_expected_columns(tmp_path):
|
|||||||
feature_columns = {
|
feature_columns = {
|
||||||
row[1] for row in connection.execute("PRAGMA table_info(approved_features)")
|
row[1] for row in connection.execute("PRAGMA table_info(approved_features)")
|
||||||
}
|
}
|
||||||
|
ability_columns = {
|
||||||
|
row[1] for row in connection.execute("PRAGMA table_info(approved_abilities)")
|
||||||
|
}
|
||||||
|
capability_columns = {
|
||||||
|
row[1]
|
||||||
|
for row in connection.execute("PRAGMA table_info(approved_capabilities)")
|
||||||
|
}
|
||||||
|
candidate_feature_columns = {
|
||||||
|
row[1] for row in connection.execute("PRAGMA table_info(candidate_features)")
|
||||||
|
}
|
||||||
evidence_columns = {
|
evidence_columns = {
|
||||||
row[1] for row in connection.execute("PRAGMA table_info(approved_evidence)")
|
row[1] for row in connection.execute("PRAGMA table_info(approved_evidence)")
|
||||||
}
|
}
|
||||||
@@ -33,6 +43,10 @@ def test_initialize_is_idempotent_and_applies_expected_columns(tmp_path):
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert "source_refs" in feature_columns
|
assert "source_refs" in feature_columns
|
||||||
|
assert {"primary_class", "attributes"} <= ability_columns
|
||||||
|
assert {"primary_class", "attributes"} <= capability_columns
|
||||||
|
assert {"primary_class", "attributes"} <= feature_columns
|
||||||
|
assert {"primary_class", "attributes"} <= candidate_feature_columns
|
||||||
assert "source_refs" in evidence_columns
|
assert "source_refs" in evidence_columns
|
||||||
assert {
|
assert {
|
||||||
"target_kind",
|
"target_kind",
|
||||||
|
|||||||
@@ -1723,6 +1723,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
|
|||||||
data={
|
data={
|
||||||
"name": "Manual Ability",
|
"name": "Manual Ability",
|
||||||
"description": "Curated by hand.",
|
"description": "Curated by hand.",
|
||||||
|
"primary_class": "repository-intelligence",
|
||||||
|
"attributes": "review, curation",
|
||||||
"confidence": "0.95",
|
"confidence": "0.95",
|
||||||
},
|
},
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
@@ -1740,6 +1742,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
|
|||||||
"description": "Curated capability.",
|
"description": "Curated capability.",
|
||||||
"inputs": "request, context",
|
"inputs": "request, context",
|
||||||
"outputs": "response",
|
"outputs": "response",
|
||||||
|
"primary_class": "review",
|
||||||
|
"attributes": "ui, workflow",
|
||||||
"confidence": "0.9",
|
"confidence": "0.9",
|
||||||
},
|
},
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
@@ -1755,6 +1759,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
|
|||||||
"capability_id": str(capability_id),
|
"capability_id": str(capability_id),
|
||||||
"name": "Manual API",
|
"name": "Manual API",
|
||||||
"type": "REST endpoint",
|
"type": "REST endpoint",
|
||||||
|
"primary_class": "api",
|
||||||
|
"attributes": "integration, review",
|
||||||
"location": "src/manual.py",
|
"location": "src/manual.py",
|
||||||
"confidence": "0.88",
|
"confidence": "0.88",
|
||||||
},
|
},
|
||||||
@@ -1807,6 +1813,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
|
|||||||
data={
|
data={
|
||||||
"name": "Edited Manual Ability",
|
"name": "Edited Manual Ability",
|
||||||
"description": "Edited by hand.",
|
"description": "Edited by hand.",
|
||||||
|
"primary_class": "workflow-automation",
|
||||||
|
"attributes": "manual, curation",
|
||||||
"confidence": "0.8",
|
"confidence": "0.8",
|
||||||
},
|
},
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
@@ -1820,6 +1828,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
|
|||||||
"description": "Edited capability.",
|
"description": "Edited capability.",
|
||||||
"inputs": "ticket",
|
"inputs": "ticket",
|
||||||
"outputs": "decision",
|
"outputs": "decision",
|
||||||
|
"primary_class": "decisioning",
|
||||||
|
"attributes": "workflow, review",
|
||||||
"confidence": "0.75",
|
"confidence": "0.75",
|
||||||
},
|
},
|
||||||
follow_redirects=False,
|
follow_redirects=False,
|
||||||
@@ -1835,6 +1845,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
|
|||||||
data={
|
data={
|
||||||
"name": "Edited Manual API",
|
"name": "Edited Manual API",
|
||||||
"type": "HTTP endpoint",
|
"type": "HTTP endpoint",
|
||||||
|
"primary_class": "ui",
|
||||||
|
"attributes": "api, review",
|
||||||
"location": "src/edited.py",
|
"location": "src/edited.py",
|
||||||
"confidence": "0.7",
|
"confidence": "0.7",
|
||||||
},
|
},
|
||||||
@@ -1865,6 +1877,32 @@ def test_ui_manual_registry_entry_loop(tmp_path):
|
|||||||
assert f"references feature #{feature_id}" in detail_response.text
|
assert f"references feature #{feature_id}" in detail_response.text
|
||||||
assert "downward support" in detail_response.text
|
assert "downward support" in detail_response.text
|
||||||
|
|
||||||
|
ability_map = client.get(f"/repos/{repository_id}/ability-map").json()
|
||||||
|
edited_ability = ability_map["abilities"][0]
|
||||||
|
edited_capability = edited_ability["capabilities"][0]
|
||||||
|
edited_feature = edited_capability["features"][0]
|
||||||
|
assert edited_ability["primary_class"] == "workflow-automation"
|
||||||
|
assert edited_ability["attributes"] == ["manual", "curation"]
|
||||||
|
assert edited_capability["primary_class"] == "decisioning"
|
||||||
|
assert edited_capability["attributes"] == ["workflow", "review"]
|
||||||
|
assert edited_feature["primary_class"] == "ui"
|
||||||
|
assert edited_feature["attributes"] == ["api", "review"]
|
||||||
|
|
||||||
|
filtered_feature_listing = client.get(
|
||||||
|
f"/ui/repos/{repository_id}/elements",
|
||||||
|
params={
|
||||||
|
"scope": "all",
|
||||||
|
"entry_filter": "approved",
|
||||||
|
"type": "features",
|
||||||
|
"class_filter": "ui",
|
||||||
|
"attribute_filter": "api",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert filtered_feature_listing.status_code == 200
|
||||||
|
assert "Attribute" in filtered_feature_listing.text
|
||||||
|
assert "Edited Manual API" in filtered_feature_listing.text
|
||||||
|
assert "1 of 1 shown" in filtered_feature_listing.text
|
||||||
|
|
||||||
upward_support_listing = client.get(
|
upward_support_listing = client.get(
|
||||||
f"/ui/repos/{repository_id}/elements",
|
f"/ui/repos/{repository_id}/elements",
|
||||||
params={
|
params={
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
---
|
||||||
|
id: RREG-WP-0004
|
||||||
|
type: workplan
|
||||||
|
title: "Repository Ability Registry - Characteristic Classification And Navigation"
|
||||||
|
domain: capabilities
|
||||||
|
repo: repo-registry
|
||||||
|
status: active
|
||||||
|
owner: codex
|
||||||
|
topic_slug: foerster-capabilities
|
||||||
|
created: "2026-04-29"
|
||||||
|
updated: "2026-04-29"
|
||||||
|
state_hub_workstream_id: "ad67787f-a89d-4cde-957c-18ef39b43912"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make repository profiles easier to understand and refine by adding first-class
|
||||||
|
classification and navigation support for characteristics. The product should
|
||||||
|
help curators move from repository scope to abilities, capabilities, features,
|
||||||
|
support, and observed facts while preserving flexibility for overlap and messy
|
||||||
|
real-world abstraction.
|
||||||
|
|
||||||
|
## P0: Characteristic Classification Fields
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RREG-WP-0004-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "fd2664c2-eb33-42f3-9624-c74bb0d30456"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `primary_class` and `attributes` to approved and candidate abilities,
|
||||||
|
capabilities, and features. Keep the migration additive, preserve existing
|
||||||
|
feature `type` compatibility, and default existing data to useful classes.
|
||||||
|
|
||||||
|
Acceptance: API/UI serialization includes classification fields for approved and
|
||||||
|
candidate graph elements; existing repositories migrate without data loss; tests
|
||||||
|
cover default backfill and candidate-to-approved promotion.
|
||||||
|
|
||||||
|
Implementation note 2026-04-29: approved and candidate abilities, capabilities,
|
||||||
|
and features now carry `primary_class` and `attributes`. The migration is
|
||||||
|
additive, existing features default their primary class and first attribute from
|
||||||
|
legacy `type`, API response models expose the new fields, and approval preserves
|
||||||
|
candidate classification metadata.
|
||||||
|
|
||||||
|
## P1: Classification Review UI
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RREG-WP-0004-T02
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "ec9a2676-9e41-4f2d-8436-80670e5e051f"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expose primary class and attributes in manual add/edit forms and candidate review
|
||||||
|
actions. Let curators filter characteristic lists by primary class and secondary
|
||||||
|
attributes without losing approved/candidate status filters.
|
||||||
|
|
||||||
|
Acceptance: a curator can edit the class and attributes of an approved or
|
||||||
|
candidate ability, capability, or feature and immediately use those values for
|
||||||
|
filtering.
|
||||||
|
|
||||||
|
Implementation note 2026-04-29: manual add/edit forms and element-list edit
|
||||||
|
forms expose primary class and attributes for approved characteristics. Candidate
|
||||||
|
abilities, capabilities, and features can also be edited with classification
|
||||||
|
metadata, and element listings can filter by primary class and secondary
|
||||||
|
attribute.
|
||||||
|
|
||||||
|
## P1: Deterministic Classification Proposals
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RREG-WP-0004-T03
|
||||||
|
status: done
|
||||||
|
priority: medium
|
||||||
|
state_hub_task_id: "d1f9646e-ac74-4fad-9183-7d5c4d5c93e0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Teach deterministic candidate generation to propose conservative primary classes
|
||||||
|
and attributes from scanner facts, file surfaces, routes, manifests, tests,
|
||||||
|
documentation, and provider/config signals.
|
||||||
|
|
||||||
|
Acceptance: repo-registry and llm-connect analyses produce useful
|
||||||
|
classification labels while LLM assistance remains optional and disabled paths
|
||||||
|
remain smooth.
|
||||||
|
|
||||||
|
Implementation note 2026-04-29: deterministic candidate generation now proposes
|
||||||
|
conservative classification metadata. Abilities can classify as
|
||||||
|
`repository-intelligence`, `ai-integration`, or `developer-tooling`; capabilities
|
||||||
|
include `interface`, `llm-integration`, and `repository-structure`; features add
|
||||||
|
surface/provider attributes such as `api`, `cli`, `http`, `llm-provider`,
|
||||||
|
`openrouter`, `claude`, `credential`, and `fallback-policy`.
|
||||||
|
|
||||||
|
## P1: Characteristic Drilldown Navigation
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RREG-WP-0004-T04
|
||||||
|
status: todo
|
||||||
|
priority: medium
|
||||||
|
state_hub_task_id: "14ede41f-a0cb-4a9a-b4ba-f23b34d7ae33"
|
||||||
|
```
|
||||||
|
|
||||||
|
Improve navigation from high-level scope and ability views down to capabilities,
|
||||||
|
features, support, and observed facts. Facts should feel like drilldown evidence,
|
||||||
|
not the primary orientation surface.
|
||||||
|
|
||||||
|
Acceptance: the repository page and element listings make it natural to move
|
||||||
|
from scope to abilities to lower-level support/facts, with counts, filters, and
|
||||||
|
clear breadcrumbs.
|
||||||
|
|
||||||
|
## P2: Classification Quality Feedback
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RREG-WP-0004-T05
|
||||||
|
status: todo
|
||||||
|
priority: low
|
||||||
|
state_hub_task_id: "691f3cb7-c8a2-4f80-a6c2-29cb5a0c7a96"
|
||||||
|
```
|
||||||
|
|
||||||
|
Capture classification expectation gaps and review smells, including missing
|
||||||
|
primary classes, wrong feature surface classes, overbroad attributes, and
|
||||||
|
same-level/upward support patterns that indicate suboptimal organization.
|
||||||
|
|
||||||
|
Acceptance: reviewers can record classification-specific improvement inputs that
|
||||||
|
feed the scanner coevolution workflow.
|
||||||
Reference in New Issue
Block a user