Improved datamodel and deterministic generation

This commit is contained in:
2026-04-30 01:29:29 +02:00
parent 973d4bbe7c
commit 26e87ab52c
14 changed files with 848 additions and 39 deletions

View File

@@ -48,8 +48,8 @@ the Custodian State Hub. Read it at the start of each session. It documents:
## Active Workplan
`workplans/RREG-WP-0002-production-hardening.md` is the current active workplan.
Six tasks remain open (T01T06). Start with T01 (P0: Update Safety and Change Review)
`workplans/RREG-WP-0004-characteristic-classification-navigation.md` is the
current active workplan. Start with T01 (P0: Characteristic Classification Fields)
as the highest-priority item.
---

View File

@@ -62,6 +62,8 @@ CREATE TABLE IF NOT EXISTS candidate_abilities (
analysis_run_id INTEGER NOT NULL REFERENCES analysis_runs(id) ON DELETE CASCADE,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
primary_class TEXT NOT NULL DEFAULT 'ability',
attributes TEXT NOT NULL DEFAULT '[]',
confidence REAL NOT NULL DEFAULT 0.0,
status TEXT NOT NULL DEFAULT 'candidate',
source_refs TEXT NOT NULL DEFAULT '[]',
@@ -77,6 +79,8 @@ CREATE TABLE IF NOT EXISTS candidate_capabilities (
description TEXT NOT NULL DEFAULT '',
inputs 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,
status TEXT NOT NULL DEFAULT 'candidate',
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,
name TEXT NOT NULL,
type TEXT NOT NULL,
primary_class TEXT NOT NULL DEFAULT '',
attributes TEXT NOT NULL DEFAULT '[]',
location TEXT NOT NULL DEFAULT '',
confidence REAL NOT NULL DEFAULT 0.0,
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,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
primary_class TEXT NOT NULL DEFAULT 'ability',
attributes TEXT NOT NULL DEFAULT '[]',
confidence REAL NOT NULL DEFAULT 1.0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -149,6 +157,8 @@ CREATE TABLE IF NOT EXISTS approved_capabilities (
description TEXT NOT NULL DEFAULT '',
inputs 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,
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,
name TEXT NOT NULL,
type TEXT NOT NULL,
primary_class TEXT NOT NULL DEFAULT '',
attributes TEXT NOT NULL DEFAULT '[]',
location TEXT NOT NULL DEFAULT '',
confidence REAL NOT NULL DEFAULT 1.0,
source_refs TEXT NOT NULL DEFAULT '[]',

View File

@@ -21,6 +21,8 @@ class CandidateFeatureDraft:
location: str
confidence: float
source_refs: list[SourceReference]
primary_class: str = ""
attributes: list[str] = field(default_factory=list)
@dataclass(frozen=True)
@@ -31,6 +33,8 @@ class CandidateCapabilityDraft:
outputs: list[str]
confidence: float
source_refs: list[SourceReference]
primary_class: str = "capability"
attributes: list[str] = field(default_factory=list)
features: list[CandidateFeatureDraft] = field(default_factory=list)
evidence: list[CandidateEvidenceDraft] = field(default_factory=list)
@@ -41,6 +45,8 @@ class CandidateAbilityDraft:
description: str
confidence: float
source_refs: list[SourceReference]
primary_class: str = "ability"
attributes: list[str] = field(default_factory=list)
capabilities: list[CandidateCapabilityDraft] = field(default_factory=list)
@@ -68,6 +74,11 @@ class CandidateGraphGenerator:
credential_configs = self._facts(facts, "credential_config")
provider_registries = self._facts(facts, "provider_registry")
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 = CandidateAbilityDraft(
@@ -82,6 +93,8 @@ class CandidateGraphGenerator:
languages=languages,
),
source_refs=self._source_refs(ability_sources),
primary_class=ability_primary_class,
attributes=ability_attributes,
capabilities=[],
)
@@ -119,6 +132,12 @@ class CandidateGraphGenerator:
docs=docs,
),
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),
)
)
@@ -129,6 +148,8 @@ class CandidateGraphGenerator:
description=ability.description,
confidence=ability.confidence,
source_refs=ability.source_refs,
primary_class=ability.primary_class,
attributes=ability.attributes,
capabilities=capabilities,
)
]
@@ -154,6 +175,8 @@ class CandidateGraphGenerator:
docs=docs,
),
source_refs=self._source_refs(interfaces),
primary_class="interface",
attributes=self._interface_attributes(interfaces),
features=features,
evidence=self._evidence(tests, examples, docs),
)
@@ -181,6 +204,8 @@ class CandidateGraphGenerator:
source_refs=self._source_refs(
[fact for fact in providers if fact.name == provider]
),
primary_class="integration",
attributes=["llm-provider", provider.lower()],
)
for provider in provider_names
]
@@ -192,6 +217,8 @@ class CandidateGraphGenerator:
location=self._grouped_location(credentials),
confidence=0.7,
source_refs=self._source_refs(credentials),
primary_class="configuration",
attributes=["credential", "llm-provider"],
)
)
if registries:
@@ -202,6 +229,8 @@ class CandidateGraphGenerator:
location=self._grouped_location(registries),
confidence=0.65,
source_refs=self._source_refs(registries),
primary_class="backend",
attributes=["provider-registry", "llm-provider"],
)
)
if fallback_policies:
@@ -212,6 +241,8 @@ class CandidateGraphGenerator:
location=self._grouped_location(fallback_policies),
confidence=0.6,
source_refs=self._source_refs(fallback_policies),
primary_class="backend",
attributes=["fallback-policy", "llm-provider"],
)
)
return CandidateCapabilityDraft(
@@ -232,6 +263,13 @@ class CandidateGraphGenerator:
source_refs=self._source_refs(
providers + credentials + registries + fallback_policies
),
primary_class="llm-integration",
attributes=self._llm_provider_attributes(
providers,
credentials,
registries,
fallback_policies,
),
features=features,
evidence=self._evidence(tests, examples, docs),
)
@@ -256,6 +294,8 @@ class CandidateGraphGenerator:
location=fact.path,
confidence=0.65 if fact.value else 0.45,
source_refs=self._source_refs([fact]),
primary_class=feature_type,
attributes=self._feature_attributes(feature_type, [fact]),
)
)
continue
@@ -271,6 +311,8 @@ class CandidateGraphGenerator:
location=self._grouped_location(facts),
confidence=self._grouped_interface_confidence(facts),
source_refs=self._source_refs(facts),
primary_class=feature_type,
attributes=self._feature_attributes(feature_type, facts),
)
)
return features
@@ -357,6 +399,96 @@ class CandidateGraphGenerator:
return "API"
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]:
feature_types = {self._feature_type(fact) for fact in interfaces}
inputs: list[str] = []

View File

@@ -73,6 +73,8 @@ def _combine_abilities(
description=_preferred_description(left.description, right.description),
confidence=max(left.confidence, right.confidence),
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),
)
@@ -107,6 +109,8 @@ def _combine_capabilities(
outputs=_merge_strings(left.outputs, right.outputs),
confidence=max(left.confidence, right.confidence),
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),
evidence=_merge_evidence(left.evidence + right.evidence),
)
@@ -128,6 +132,8 @@ def _merge_features(
location=_preferred_text(existing.location, feature.location),
confidence=max(existing.confidence, feature.confidence),
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

View File

@@ -161,6 +161,8 @@ class CandidateFeature:
status: str
source_refs: list[SourceReference]
confidence_label: str = ""
primary_class: str = ""
attributes: list[str] = field(default_factory=list)
@dataclass(frozen=True)
@@ -174,6 +176,8 @@ class CandidateCapability:
status: str
source_refs: list[SourceReference]
confidence_label: str = ""
primary_class: str = "capability"
attributes: list[str] = field(default_factory=list)
features: list[CandidateFeature] = field(default_factory=list)
evidence: list[CandidateEvidence] = field(default_factory=list)
@@ -187,6 +191,8 @@ class CandidateAbility:
status: str
source_refs: list[SourceReference]
confidence_label: str = ""
primary_class: str = "ability"
attributes: list[str] = field(default_factory=list)
capabilities: list[CandidateCapability] = field(default_factory=list)
@@ -228,6 +234,8 @@ class Feature:
confidence: float
confidence_label: str = ""
source_refs: list[SourceReference] = field(default_factory=list)
primary_class: str = ""
attributes: list[str] = field(default_factory=list)
@dataclass(frozen=True)
@@ -239,6 +247,8 @@ class Capability:
outputs: list[str]
confidence: float
confidence_label: str = ""
primary_class: str = "capability"
attributes: list[str] = field(default_factory=list)
features: list[Feature] = field(default_factory=list)
evidence: list[Evidence] = field(default_factory=list)
@@ -250,6 +260,8 @@ class Ability:
description: str
confidence: float
confidence_label: str = ""
primary_class: str = "ability"
attributes: list[str] = field(default_factory=list)
capabilities: list[Capability] = field(default_factory=list)

View File

@@ -369,6 +369,8 @@ class RegistryService:
location=feature.location,
confidence=feature.confidence,
source_refs=feature.source_refs,
primary_class=feature.primary_class,
attributes=feature.attributes,
)
for evidence in capability.evidence:
if evidence.status != "candidate":
@@ -512,6 +514,8 @@ class RegistryService:
location=feature.location,
confidence=feature.confidence,
source_refs=feature.source_refs,
primary_class=feature.primary_class,
attributes=feature.attributes,
)
self.store.mark_candidate_feature_status(
repository_id,
@@ -655,6 +659,8 @@ class RegistryService:
inputs=capability.inputs,
outputs=capability.outputs,
confidence=capability.confidence,
primary_class=capability.primary_class,
attributes=capability.attributes,
)
for feature in capability.features:
if feature.status != "candidate":
@@ -667,6 +673,8 @@ class RegistryService:
location=feature.location,
confidence=feature.confidence,
source_refs=feature.source_refs,
primary_class=feature.primary_class,
attributes=feature.attributes,
)
for evidence in capability.evidence:
if evidence.status != "candidate":
@@ -702,6 +710,8 @@ class RegistryService:
name=candidate_ability.name,
description=candidate_ability.description,
confidence=candidate_ability.confidence,
primary_class=candidate_ability.primary_class,
attributes=candidate_ability.attributes,
)
def _ensure_approved_capability(
@@ -726,6 +736,8 @@ class RegistryService:
inputs=candidate_capability.inputs,
outputs=candidate_capability.outputs,
confidence=candidate_capability.confidence,
primary_class=candidate_capability.primary_class,
attributes=candidate_capability.attributes,
)
def _candidate_capability_with_parent(
@@ -884,6 +896,8 @@ class RegistryService:
name: str,
description: str,
confidence: float,
primary_class: str = "ability",
attributes: Sequence[str] = (),
notes: str = "",
) -> CandidateGraph:
self.store.update_candidate_ability(
@@ -893,6 +907,8 @@ class RegistryService:
name=name,
description=description,
confidence=confidence,
primary_class=primary_class,
attributes=list(attributes),
)
self.store.create_review_decision(
repository_id,
@@ -912,6 +928,8 @@ class RegistryService:
name: str,
description: str,
confidence: float,
primary_class: str = "capability",
attributes: Sequence[str] = (),
notes: str = "",
) -> CandidateGraph:
self.store.update_candidate_capability(
@@ -921,6 +939,8 @@ class RegistryService:
name=name,
description=description,
confidence=confidence,
primary_class=primary_class,
attributes=list(attributes),
)
self.store.create_review_decision(
repository_id,
@@ -931,6 +951,40 @@ class RegistryService:
self.store.update_repository_status(repository_id, "reviewing")
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(
self,
repository_id: int,
@@ -1106,6 +1160,8 @@ class RegistryService:
name: str,
description: str = "",
confidence: float = 1.0,
primary_class: str = "ability",
attributes: Sequence[str] = (),
) -> int:
self.store.get_repository(repository_id)
return self.store.create_ability(
@@ -1113,6 +1169,8 @@ class RegistryService:
name=name,
description=description,
confidence=confidence,
primary_class=primary_class,
attributes=list(attributes),
)
def update_ability(
@@ -1123,6 +1181,8 @@ class RegistryService:
name: str | None = None,
description: str | None = None,
confidence: float | None = None,
primary_class: str | None = None,
attributes: Sequence[str] | None = None,
) -> RepositoryAbilityMap:
self.store.update_ability(
repository_id,
@@ -1130,6 +1190,8 @@ class RegistryService:
name=name,
description=description,
confidence=confidence,
primary_class=primary_class,
attributes=list(attributes) if attributes is not None else None,
)
return self.store.get_ability_map(repository_id)
@@ -1151,6 +1213,8 @@ class RegistryService:
inputs: Sequence[str] = (),
outputs: Sequence[str] = (),
confidence: float = 1.0,
primary_class: str = "capability",
attributes: Sequence[str] = (),
) -> int:
self.store.ensure_ability(repository_id, ability_id)
return self.store.create_capability(
@@ -1161,6 +1225,8 @@ class RegistryService:
inputs=list(inputs),
outputs=list(outputs),
confidence=confidence,
primary_class=primary_class,
attributes=list(attributes),
)
def update_capability(
@@ -1173,6 +1239,8 @@ class RegistryService:
inputs: Sequence[str] | None = None,
outputs: Sequence[str] | None = None,
confidence: float | None = None,
primary_class: str | None = None,
attributes: Sequence[str] | None = None,
) -> RepositoryAbilityMap:
self.store.update_capability(
repository_id,
@@ -1182,6 +1250,8 @@ class RegistryService:
inputs=list(inputs) if inputs is not None else None,
outputs=list(outputs) if outputs is not None else None,
confidence=confidence,
primary_class=primary_class,
attributes=list(attributes) if attributes is not None else None,
)
return self.store.get_ability_map(repository_id)
@@ -1202,6 +1272,8 @@ class RegistryService:
type: str,
location: str = "",
confidence: float = 1.0,
primary_class: str | None = None,
attributes: Sequence[str] = (),
) -> int:
self.store.ensure_capability(repository_id, capability_id)
return self.store.create_feature(
@@ -1211,6 +1283,8 @@ class RegistryService:
type=type,
location=location,
confidence=confidence,
primary_class=primary_class,
attributes=list(attributes),
)
def update_feature(
@@ -1222,6 +1296,8 @@ class RegistryService:
type: str | None = None,
location: str | None = None,
confidence: float | None = None,
primary_class: str | None = None,
attributes: Sequence[str] | None = None,
) -> RepositoryAbilityMap:
self.store.update_feature(
repository_id,
@@ -1230,6 +1306,8 @@ class RegistryService:
type=type,
location=location,
confidence=confidence,
primary_class=primary_class,
attributes=list(attributes) if attributes is not None else None,
)
return self.store.get_ability_map(repository_id)

View File

@@ -51,6 +51,7 @@ class RegistryStore:
self._ensure_repository_scopes_table(connection)
self._ensure_approved_source_ref_columns(connection)
self._ensure_evidence_relationship_columns(connection)
self._ensure_characteristic_classification_columns(connection)
self._ensure_expectation_gaps_table(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:
connection.execute(
"""
@@ -361,14 +416,17 @@ class RegistryStore:
ability_cursor = connection.execute(
"""
INSERT INTO candidate_abilities
(repository_id, analysis_run_id, name, description, confidence, source_refs)
VALUES (?, ?, ?, ?, ?, ?)
(repository_id, analysis_run_id, name, description, primary_class,
attributes, confidence, source_refs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""",
(
repository_id,
analysis_run_id,
ability.name,
ability.description,
ability.primary_class or "ability",
self._attributes_to_json(ability.attributes),
ability.confidence,
self._source_refs_to_json(ability.source_refs),
),
@@ -379,8 +437,8 @@ class RegistryStore:
"""
INSERT INTO candidate_capabilities
(repository_id, analysis_run_id, ability_id, name, description,
inputs, outputs, confidence, source_refs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
inputs, outputs, primary_class, attributes, confidence, source_refs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
repository_id,
@@ -390,6 +448,8 @@ class RegistryStore:
capability.description,
json.dumps(capability.inputs),
json.dumps(capability.outputs),
capability.primary_class or "capability",
self._attributes_to_json(capability.attributes),
capability.confidence,
self._source_refs_to_json(capability.source_refs),
),
@@ -400,8 +460,8 @@ class RegistryStore:
"""
INSERT INTO candidate_features
(repository_id, analysis_run_id, capability_id, name, type,
location, confidence, source_refs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
primary_class, attributes, location, confidence, source_refs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
repository_id,
@@ -409,6 +469,10 @@ class RegistryStore:
capability_id,
feature.name,
feature.type,
feature.primary_class or feature.type,
self._attributes_to_json(
feature.attributes or [feature.type]
),
feature.location,
feature.confidence,
self._source_refs_to_json(feature.source_refs),
@@ -448,7 +512,8 @@ class RegistryStore:
with self.connect() as connection:
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
WHERE repository_id = ? AND analysis_run_id = ?
ORDER BY id
@@ -458,7 +523,7 @@ class RegistryStore:
capability_rows = connection.execute(
"""
SELECT id, ability_id, name, description, inputs, outputs,
confidence, status, source_refs
primary_class, attributes, confidence, status, source_refs
FROM candidate_capabilities
WHERE repository_id = ? AND analysis_run_id = ?
ORDER BY id
@@ -467,8 +532,8 @@ class RegistryStore:
).fetchall()
feature_rows = connection.execute(
"""
SELECT id, capability_id, name, type, location, confidence,
status, source_refs
SELECT id, capability_id, name, type, primary_class, attributes,
location, confidence, status, source_refs
FROM candidate_features
WHERE repository_id = ? AND analysis_run_id = ?
ORDER BY id
@@ -498,6 +563,8 @@ class RegistryStore:
status=row["status"],
source_refs=self._source_refs_from_json(row["source_refs"]),
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"],
source_refs=self._source_refs_from_json(row["source_refs"]),
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"], []),
evidence=evidence_by_capability.get(row["id"], []),
)
@@ -545,6 +614,8 @@ class RegistryStore:
status=row["status"],
source_refs=self._source_refs_from_json(row["source_refs"]),
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"], []),
)
for row in ability_rows
@@ -861,17 +932,22 @@ class RegistryStore:
name: str,
description: str,
confidence: float,
primary_class: str = "ability",
attributes: list[str] | None = None,
) -> None:
with self.connect() as connection:
cursor = connection.execute(
"""
UPDATE candidate_abilities
SET name = ?, description = ?, confidence = ?
SET name = ?, description = ?, primary_class = ?, attributes = ?,
confidence = ?
WHERE id = ? AND repository_id = ? AND analysis_run_id = ?
""",
(
name,
description,
primary_class or "ability",
self._attributes_to_json(attributes or []),
confidence,
candidate_ability_id,
repository_id,
@@ -894,17 +970,22 @@ class RegistryStore:
name: str,
description: str,
confidence: float,
primary_class: str = "capability",
attributes: list[str] | None = None,
) -> None:
with self.connect() as connection:
cursor = connection.execute(
"""
UPDATE candidate_capabilities
SET name = ?, description = ?, confidence = ?
SET name = ?, description = ?, primary_class = ?, attributes = ?,
confidence = ?
WHERE id = ? AND repository_id = ? AND analysis_run_id = ?
""",
(
name,
description,
primary_class or "capability",
self._attributes_to_json(attributes or []),
confidence,
candidate_capability_id,
repository_id,
@@ -918,6 +999,46 @@ class RegistryStore:
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(
self,
repository_id: int,
@@ -1604,15 +1725,24 @@ class RegistryStore:
name: str,
description: str,
confidence: float,
primary_class: str = "ability",
attributes: list[str] | None = None,
) -> int:
with self.connect() as connection:
cursor = connection.execute(
"""
INSERT INTO approved_abilities
(repository_id, name, description, confidence)
VALUES (?, ?, ?, ?)
(repository_id, name, description, primary_class, attributes, confidence)
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)
@@ -1638,6 +1768,8 @@ class RegistryStore:
name: str | None = None,
description: str | None = None,
confidence: float | None = None,
primary_class: str | None = None,
attributes: list[str] | None = None,
) -> None:
self._update_approved_row(
table="approved_abilities",
@@ -1648,6 +1780,12 @@ class RegistryStore:
"name": name,
"description": description,
"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],
outputs: list[str],
confidence: float,
primary_class: str = "capability",
attributes: list[str] | None = None,
) -> int:
with self.connect() as connection:
cursor = connection.execute(
"""
INSERT INTO approved_capabilities
(repository_id, ability_id, name, description, inputs, outputs, confidence)
VALUES (?, ?, ?, ?, ?, ?, ?)
(repository_id, ability_id, name, description, inputs, outputs,
primary_class, attributes, confidence)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
repository_id,
@@ -1684,6 +1825,8 @@ class RegistryStore:
description,
json.dumps(inputs),
json.dumps(outputs),
primary_class or "capability",
self._attributes_to_json(attributes or []),
confidence,
),
)
@@ -1713,6 +1856,8 @@ class RegistryStore:
inputs: list[str] | None = None,
outputs: list[str] | None = None,
confidence: float | None = None,
primary_class: str | None = None,
attributes: list[str] | None = None,
) -> None:
self._update_approved_row(
table="approved_capabilities",
@@ -1725,6 +1870,12 @@ class RegistryStore:
"inputs": json.dumps(inputs) if inputs is not None else None,
"outputs": json.dumps(outputs) if outputs is not None else None,
"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,
confidence: float,
source_refs: list[SourceReference] | None = None,
primary_class: str | None = None,
attributes: list[str] | None = None,
) -> int:
with self.connect() as connection:
cursor = connection.execute(
"""
INSERT INTO approved_features
(repository_id, capability_id, name, type, location, confidence, source_refs)
VALUES (?, ?, ?, ?, ?, ?, ?)
(repository_id, capability_id, name, type, primary_class, attributes,
location, confidence, source_refs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
repository_id,
capability_id,
name,
type,
primary_class or type,
self._attributes_to_json(attributes or [type]),
location,
confidence,
self._source_refs_to_json(source_refs or []),
@@ -1775,6 +1931,8 @@ class RegistryStore:
type: str | None = None,
location: str | None = None,
confidence: float | None = None,
primary_class: str | None = None,
attributes: list[str] | None = None,
) -> None:
self._update_approved_row(
table="approved_features",
@@ -1784,6 +1942,12 @@ class RegistryStore:
values={
"name": name,
"type": type,
"primary_class": primary_class,
"attributes": (
self._attributes_to_json(attributes)
if attributes is not None
else None
),
"location": location,
"confidence": confidence,
},
@@ -1968,13 +2132,15 @@ class RegistryStore:
ability_cursor = connection.execute(
"""
INSERT INTO approved_abilities
(repository_id, name, description, confidence)
VALUES (?, ?, ?, ?)
(repository_id, name, description, primary_class, attributes, confidence)
VALUES (?, ?, ?, ?, ?, ?)
""",
(
repository_id,
ability.name,
ability.description,
ability.primary_class or "ability",
self._attributes_to_json(ability.attributes),
ability.confidence,
),
)
@@ -1986,8 +2152,8 @@ class RegistryStore:
"""
INSERT INTO approved_capabilities
(repository_id, ability_id, name, description, inputs, outputs,
confidence)
VALUES (?, ?, ?, ?, ?, ?, ?)
primary_class, attributes, confidence)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
repository_id,
@@ -1996,6 +2162,8 @@ class RegistryStore:
capability.description,
json.dumps(capability.inputs),
json.dumps(capability.outputs),
capability.primary_class or "capability",
self._attributes_to_json(capability.attributes),
capability.confidence,
),
)
@@ -2006,15 +2174,19 @@ class RegistryStore:
connection.execute(
"""
INSERT INTO approved_features
(repository_id, capability_id, name, type, location,
confidence, source_refs)
VALUES (?, ?, ?, ?, ?, ?, ?)
(repository_id, capability_id, name, type, primary_class,
attributes, location, confidence, source_refs)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
repository_id,
approved_capability_id,
feature.name,
feature.type,
feature.primary_class or feature.type,
self._attributes_to_json(
feature.attributes or [feature.type]
),
feature.location,
feature.confidence,
self._source_refs_to_json(feature.source_refs),
@@ -2051,7 +2223,7 @@ class RegistryStore:
with self.connect() as connection:
ability_rows = connection.execute(
"""
SELECT id, name, description, confidence
SELECT id, name, description, primary_class, attributes, confidence
FROM approved_abilities
WHERE repository_id = ?
ORDER BY id
@@ -2060,7 +2232,8 @@ class RegistryStore:
).fetchall()
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
WHERE repository_id = ?
ORDER BY id
@@ -2069,7 +2242,8 @@ class RegistryStore:
).fetchall()
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
WHERE repository_id = ?
ORDER BY id
@@ -2098,6 +2272,8 @@ class RegistryStore:
confidence=row["confidence"],
confidence_label=confidence_label(row["confidence"]),
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"]),
confidence=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"], []),
evidence=evidence_by_capability.get(row["id"], []),
)
@@ -2140,6 +2318,8 @@ class RegistryStore:
description=row["description"],
confidence=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"], []),
)
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]:
return [
SourceReference(

View File

@@ -495,6 +495,8 @@ class CandidateFeatureResponse(BaseModel):
id: int
name: str
type: str
primary_class: str
attributes: list[str]
location: str
confidence: float
status: str
@@ -508,6 +510,8 @@ class CandidateCapabilityResponse(BaseModel):
description: str
inputs: list[str]
outputs: list[str]
primary_class: str
attributes: list[str]
confidence: float
status: str
source_refs: list[SourceReferenceResponse]
@@ -520,6 +524,8 @@ class CandidateAbilityResponse(BaseModel):
id: int
name: str
description: str
primary_class: str
attributes: list[str]
confidence: float
status: str
source_refs: list[SourceReferenceResponse]
@@ -689,6 +695,8 @@ class FeatureResponse(BaseModel):
id: int
name: str
type: str
primary_class: str
attributes: list[str]
location: str
confidence: float
confidence_label: str
@@ -701,6 +709,8 @@ class CapabilityResponse(BaseModel):
description: str
inputs: list[str]
outputs: list[str]
primary_class: str
attributes: list[str]
confidence: float
confidence_label: str
features: list[FeatureResponse]
@@ -711,6 +721,8 @@ class AbilityResponse(BaseModel):
id: int
name: str
description: str
primary_class: str
attributes: list[str]
confidence: float
confidence_label: str
capabilities: list[CapabilityResponse]

View File

@@ -719,6 +719,8 @@ def repository_detail(
<h3>Add Ability</h3>
<label>Name <input name="name" required></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>
<button type="submit">Add Ability</button>
</form>
@@ -729,6 +731,8 @@ def repository_detail(
<label>Description <textarea name="description" rows="2"></textarea></label>
<label>Inputs <input name="inputs" 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>
<button type="submit">Add Capability</button>
</form>
@@ -737,6 +741,8 @@ def repository_detail(
<label>Capability ID <input name="capability_id" type="number" min="1" required></label>
<label>Name <input name="name" 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>Confidence <input name="confidence" type="number" min="0" max="1" step="0.01" value="1.0" required></label>
<button type="submit">Add Feature</button>
@@ -835,6 +841,8 @@ def create_ability_from_form(
name: str = Form(...),
description: str = Form(""),
confidence: float = Form(1.0),
primary_class: str = Form("ability"),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.add_ability(
@@ -842,6 +850,8 @@ def create_ability_from_form(
name=name,
description=description,
confidence=confidence,
primary_class=primary_class or "ability",
attributes=split_csv(attributes),
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -855,6 +865,8 @@ def create_capability_from_form(
inputs: str = Form(""),
outputs: str = Form(""),
confidence: float = Form(1.0),
primary_class: str = Form("capability"),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.add_capability(
@@ -865,6 +877,8 @@ def create_capability_from_form(
inputs=split_csv(inputs),
outputs=split_csv(outputs),
confidence=confidence,
primary_class=primary_class or "capability",
attributes=split_csv(attributes),
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -877,6 +891,8 @@ def create_feature_from_form(
type: str = Form(...),
location: str = Form(""),
confidence: float = Form(1.0),
primary_class: str = Form(""),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.add_feature(
@@ -886,6 +902,8 @@ def create_feature_from_form(
type=type,
location=location,
confidence=confidence,
primary_class=primary_class or type,
attributes=split_csv(attributes) or [type],
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -924,6 +942,8 @@ def edit_ability_from_form(
name: str = Form(...),
description: str = Form(""),
confidence: float = Form(1.0),
primary_class: str = Form("ability"),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.update_ability(
@@ -932,6 +952,8 @@ def edit_ability_from_form(
name=name,
description=description,
confidence=confidence,
primary_class=primary_class or "ability",
attributes=split_csv(attributes),
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -955,6 +977,8 @@ def edit_capability_from_form(
inputs: str = Form(""),
outputs: str = Form(""),
confidence: float = Form(1.0),
primary_class: str = Form("capability"),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.update_capability(
@@ -965,6 +989,8 @@ def edit_capability_from_form(
inputs=split_csv(inputs),
outputs=split_csv(outputs),
confidence=confidence,
primary_class=primary_class or "capability",
attributes=split_csv(attributes),
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -987,6 +1013,8 @@ def edit_feature_from_form(
type: str = Form(...),
location: str = Form(""),
confidence: float = Form(1.0),
primary_class: str = Form(""),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.update_feature(
@@ -996,6 +1024,8 @@ def edit_feature_from_form(
type=type,
location=location,
confidence=confidence,
primary_class=primary_class or type,
attributes=split_csv(attributes) or [type],
)
return RedirectResponse(f"/ui/repos/{repository_id}", status_code=303)
@@ -1208,6 +1238,7 @@ def repository_element_listing(
type: str = Query("abilities"),
q: str = Query(""),
class_filter: str = Query(""),
attribute_filter: str = Query(""),
entry_filter: str = Query(""),
candidate_status_filter: str = Query("active"),
support_orientation_filter: str = Query(""),
@@ -1252,13 +1283,15 @@ def repository_element_listing(
elements,
"",
"",
entry_filter,
candidate_status_filter,
"",
entry_filter=entry_filter,
candidate_status_filter=candidate_status_filter,
)
filtered = filter_element_rows(
entry_scoped_elements,
q,
class_filter,
attribute_filter,
candidate_status_filter="all",
support_orientation_filter=support_orientation_filter,
)
@@ -1283,11 +1316,13 @@ def repository_element_listing(
<div class="grid">
<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>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_candidate_status_filter(candidate_status_filter) if scope != "facts" else ""}
{render_support_orientation_filter(support_orientation_filter) if type in {"supports", "evidence"} else ""}
</div>
{render_class_datalist(entry_scoped_elements)}
{render_attribute_datalist(entry_scoped_elements)}
<div class="actions">
<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>
@@ -1562,6 +1597,8 @@ def edit_candidate_ability_from_form(
name: str = Form(...),
description: str = Form(""),
confidence: float = Form(...),
primary_class: str = Form("ability"),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.edit_candidate_ability(
@@ -1571,6 +1608,8 @@ def edit_candidate_ability_from_form(
name=name,
description=description,
confidence=confidence,
primary_class=primary_class or "ability",
attributes=split_csv(attributes),
notes="Edited from web UI",
)
return RedirectResponse(
@@ -1590,6 +1629,8 @@ def edit_candidate_capability_from_form(
name: str = Form(...),
description: str = Form(""),
confidence: float = Form(...),
primary_class: str = Form("capability"),
attributes: str = Form(""),
service: RegistryService = Depends(get_service),
) -> RedirectResponse:
service.edit_candidate_capability(
@@ -1599,6 +1640,42 @@ def edit_candidate_capability_from_form(
name=name,
description=description,
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",
)
return RedirectResponse(
@@ -2245,6 +2322,7 @@ def graph_element_rows(
item_kind="abilities",
description=ability.get("description", ""),
confidence=ability.get("confidence", 1.0),
attributes=ability.get("attributes", []),
status=ability.get("status", ""),
entry_state=entry_state,
)
@@ -2261,6 +2339,7 @@ def graph_element_rows(
item_kind="capabilities",
description=capability.get("description", ""),
confidence=capability.get("confidence", 1.0),
attributes=capability.get("attributes", []),
inputs=capability.get("inputs", []),
outputs=capability.get("outputs", []),
status=capability.get("status", ""),
@@ -2271,12 +2350,14 @@ def graph_element_rows(
if item_type == "features":
rows.append(
element_row(
feature.get("type", "feature"),
feature.get("primary_class", feature.get("type", "feature")),
feature["name"],
capability["name"],
feature.get("source_refs", []),
item_id=feature.get("id"),
item_kind="features",
type=feature.get("type", ""),
attributes=feature.get("attributes", []),
confidence=feature.get("confidence", 1.0),
location=feature.get("location", ""),
status=feature.get("status", ""),
@@ -2367,12 +2448,14 @@ def filter_element_rows(
rows: list[dict],
query: str,
class_filter: str,
attribute_filter: str = "",
entry_filter: str = "",
candidate_status_filter: str = "",
support_orientation_filter: str = "",
) -> list[dict]:
query = query.strip().lower()
class_filter = class_filter.strip().lower()
attribute_filter = attribute_filter.strip().lower()
entry_filter = entry_filter.strip().lower()
candidate_status_filter = candidate_status_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()
if class_filter and class_filter not in row_class:
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(
[
str(row["primary_class"]),
" ".join(str(item) for item in row.get("attributes", [])),
str(row["name"]),
str(row["parent"]),
str(row.get("entry_state", "")),
@@ -2443,7 +2532,7 @@ def render_element_row(
return f"""
<tr>
<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["parent"]))}</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:
if row.get("item_kind") == "evidence":
target = escape(str(row.get("target_kind") or "capability"))
@@ -2647,7 +2750,7 @@ def render_candidate_element_actions(
)
status = row.get("status", "candidate")
edit = ""
if item_kind in {"abilities", "capabilities"}:
if item_kind in {"abilities", "capabilities", "features"}:
edit_action = (
f"/ui/repos/{repository_id}/analysis-runs/{analysis_run_id}"
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:
item_kind = row["item_kind"]
name = escape(str(row["name"]))
classification = render_classification_edit_fields(row)
if item_kind == "scope":
return f"""
<label>Name <input name="name" value="{name}" required></label>
@@ -2734,6 +2838,7 @@ def render_element_edit_fields(row: dict) -> str:
return f"""
<label>Name <input name="name" value="{name}" required></label>
<label>Type <input name="type" value="{feature_type}" required></label>
{classification}
<label>Location <input name="location" value="{location}"></label>
"""
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>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:
@@ -2774,7 +2882,17 @@ def render_element_hidden_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:
@@ -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:
classes = sorted({str(row["primary_class"]) for row in rows if row["primary_class"]})
options = "".join(
@@ -2793,6 +2922,22 @@ def render_class_datalist(rows: list[dict]) -> str:
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:
if value is None:
return ""
@@ -2958,10 +3103,17 @@ def render_candidate_edit_form(
f"/{collection}/{candidate['id']}/edit"
)
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"""
<form class="stack" method="post" action="{action}">
<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>
<button class="secondary" type="submit">Save Edit</button>
</form>
@@ -3012,8 +3164,10 @@ def render_candidate_feature(
<span class="pill">ID {feature["id"]}</span>
<span class="pill">{escape(feature["status"])}</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>
{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_merge_form('candidate-features', feature, repository_id, analysis_run_id, 'target_feature_id', 'Merge into feature ID')}
</li>

View File

@@ -52,13 +52,19 @@ def test_candidate_generator_builds_purpose_seed_from_observed_facts():
ability = graph[0]
assert ability.name == "Route Incoming Customer Email To The Right Team"
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"
interface_capability = ability.capabilities[0]
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.inputs == ["HTTP request"]
assert interface_capability.outputs == ["HTTP response"]
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].location == "app.py"
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
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}
assert {"Use OpenRouter Models", "Use Claude Models"} <= feature_names
assert "Configure LLM Provider Credentials" in feature_names
assert "Maintain LLM Provider Registry" 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)

View File

@@ -813,13 +813,33 @@ def test_approve_candidate_graph_publishes_ability_map_once(tmp_path):
assert len(ability_map.abilities) == 1
assert len(second_approval.abilities) == 1
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].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[0].line == 3
assert ability_map.abilities[0].capabilities[0].evidence[0].source_refs
candidate_graph = service.candidate_graph(repository.id, summary.analysis_run.id)
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)
assert decisions[0].action == "approve_candidate_graph"
assert decisions[0].notes == "Looks good for the first pass."

View File

@@ -22,6 +22,16 @@ def test_initialize_is_idempotent_and_applies_expected_columns(tmp_path):
feature_columns = {
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 = {
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 {"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 {
"target_kind",

View File

@@ -1723,6 +1723,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
data={
"name": "Manual Ability",
"description": "Curated by hand.",
"primary_class": "repository-intelligence",
"attributes": "review, curation",
"confidence": "0.95",
},
follow_redirects=False,
@@ -1740,6 +1742,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
"description": "Curated capability.",
"inputs": "request, context",
"outputs": "response",
"primary_class": "review",
"attributes": "ui, workflow",
"confidence": "0.9",
},
follow_redirects=False,
@@ -1755,6 +1759,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
"capability_id": str(capability_id),
"name": "Manual API",
"type": "REST endpoint",
"primary_class": "api",
"attributes": "integration, review",
"location": "src/manual.py",
"confidence": "0.88",
},
@@ -1807,6 +1813,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
data={
"name": "Edited Manual Ability",
"description": "Edited by hand.",
"primary_class": "workflow-automation",
"attributes": "manual, curation",
"confidence": "0.8",
},
follow_redirects=False,
@@ -1820,6 +1828,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
"description": "Edited capability.",
"inputs": "ticket",
"outputs": "decision",
"primary_class": "decisioning",
"attributes": "workflow, review",
"confidence": "0.75",
},
follow_redirects=False,
@@ -1835,6 +1845,8 @@ def test_ui_manual_registry_entry_loop(tmp_path):
data={
"name": "Edited Manual API",
"type": "HTTP endpoint",
"primary_class": "ui",
"attributes": "api, review",
"location": "src/edited.py",
"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 "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(
f"/ui/repos/{repository_id}/elements",
params={

View File

@@ -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.