Record repository UI status in the scanner and tolerate older jsonschema.

Adds the repo-ui-metadata extractor (ui_status yes/candidate/not_detected
plus ui_evidence), falls back to the legacy validator when jsonschema does
not accept a referencing registry, and covers unknown edge endpoints in the
graph explorer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 7458@bnt-lap001
Assistant-Session: 62534cdf-8348-48a7-9c0d-46e0f74c8eae
This commit is contained in:
codex 2026-09-22 08:06:04 +02:00
parent c65b9eddfe
commit 564dc59c4f
5 changed files with 138 additions and 4 deletions

View file

@ -43,6 +43,8 @@ The deterministic extractor framework currently covers:
- repository metadata from local git/path evidence
- README, INTENT, and SCOPE document presence and headings
- repository-level UI status and supporting indicators (`yes`, `candidate`, or
`not_detected`)
- repo-owned Fabric declarations under `fabric/`
- Python `pyproject.toml` package metadata and dependencies
- Node `package.json` package metadata and dependencies
@ -54,6 +56,12 @@ The deterministic extractor framework currently covers:
- common service config files such as `application.yaml` and
`appsettings.json`
The UI status is a first-level repository attribute. `yes` requires an explicit
`web-ui` interface declaration; `candidate` records deterministic frontend
markers such as a UI package, entrypoint, or build configuration; and
`not_detected` means no such marker was found. The status is filterable through
the discovery attributes and retains `ui_evidence` source paths for review.
Each extractor emits candidates through the same accumulator so stable-key
duplicates merge inside a scan before the snapshot is returned.

View file

@ -395,6 +395,7 @@ def _deterministic_extractors() -> list:
return [
_extract_repo_metadata,
_extract_text_metadata,
_extract_ui_metadata,
_extract_fabric_declarations,
_extract_python_package,
_extract_node_package,
@ -478,6 +479,100 @@ def _extract_text_metadata(context: ScanContext) -> None:
)
def _extract_ui_metadata(context: ScanContext) -> None:
"""Record first-level evidence about whether a repository provides a UI."""
scope = context.accumulator.add_scope(
extractor_id="repo-ui-metadata",
source_kind="file",
source_path=".",
description="Repository-level user-interface indicators.",
)
evidence: list[str] = []
declared = False
for path in declaration_files(context.repo_path):
try:
data = load_yaml(path)
except Exception:
continue
if not isinstance(data, dict):
continue
spec = data.get("spec") if isinstance(data.get("spec"), dict) else {}
if spec.get("interface_type") == "web-ui":
declared = True
evidence.append(context.relpath(path))
marker_paths = (
"index.html",
"src/main.ts",
"src/main.tsx",
"src/main.js",
"src/main.jsx",
"src/App.tsx",
"src/App.jsx",
"vite.config.ts",
"vite.config.js",
"next.config.js",
"next.config.mjs",
"angular.json",
)
for marker in marker_paths:
if (context.repo_path / marker).is_file():
evidence.append(marker)
for package_name in ("package.json",):
package_path = context.repo_path / package_name
if not package_path.is_file():
continue
try:
package = json.loads(package_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
scripts = package.get("scripts") if isinstance(package, dict) else {}
dependencies = {}
if isinstance(package, dict):
dependencies.update(package.get("dependencies") or {})
dependencies.update(package.get("devDependencies") or {})
ui_packages = {"react", "react-dom", "vue", "@angular/core", "svelte", "next", "nuxt"}
if isinstance(scripts, dict) and any("dev" in str(value).lower() or "vite" in str(value).lower() for value in scripts.values()):
evidence.append(package_name)
if ui_packages.intersection(dependencies):
evidence.append(package_name)
evidence = sorted(set(evidence))
if declared:
status = "yes"
confidence = 1.0
elif evidence:
status = "candidate"
confidence = 0.65
else:
status = "not_detected"
confidence = 0.4
anchor = _source_anchor("file", evidence[0] if evidence else ".")
provenance = _provenance("repo-ui-metadata")
context.accumulator.add_attribute(
entity_key=context.repository_key,
name="ui_status",
value=status,
replacement_scope=scope,
provenance=provenance,
source_anchor=anchor,
confidence=confidence,
)
context.accumulator.add_attribute(
entity_key=context.repository_key,
name="ui_evidence",
value=evidence,
replacement_scope=scope,
provenance=provenance,
source_anchor=anchor,
confidence=confidence,
)
def _extract_fabric_declarations(context: ScanContext) -> None:
declarations: list[tuple[Path, dict[str, Any]]] = []
for path in declaration_files(context.repo_path):

View file

@ -39,10 +39,17 @@ def draft202012_validator(schema_path: Path) -> jsonschema.Draft202012Validator:
registry = schema_registry(schema_path.parent)
if registry is None:
return _legacy_validator(schema_path, schema)
return jsonschema.Draft202012Validator(
schema,
registry=registry,
)
try:
return jsonschema.Draft202012Validator(
schema,
registry=registry,
)
except TypeError as exc:
# jsonschema < 4.17 does not accept referencing.Registry. Keep the
# same schema set usable on older operator workstations.
if "unexpected keyword argument 'registry'" not in str(exc):
raise
return _legacy_validator(schema_path, schema)
def _legacy_validator(

View file

@ -159,6 +159,25 @@ def test_graph_explorer_collapses_discovered_repository_nodes() -> None:
assert declares_package["data"]["displayOnly"] is False
def test_graph_explorer_materializes_unknown_edge_endpoints() -> None:
graph = {
"apiVersion": "railiance.fabric/v1alpha1",
"kind": "FabricGraphExport",
"nodes": [{"id": "repo:reef-railiance", "kind": "Repository", "name": "reef-railiance", "repo": "reef-railiance"}],
"edges": [{"from": "repo:reef-railiance", "to": "repo:rail-knative", "type": "hosts_rail"}],
}
payload = fabric_graph_explorer_payload(graph, [{"slug": "reef-railiance", "name": "reef-railiance"}], {"reef-railiance"})
nodes = [element for element in payload["elements"] if "source" not in element["data"]]
edges = [element for element in payload["elements"] if "source" in element["data"]]
unknown = next(node for node in nodes if node["data"]["id"] == "repo:rail-knative")
edge = next(edge for edge in edges if edge["data"]["target"] == "repo:rail-knative")
assert unknown["data"]["kind"] == "Unknown"
assert unknown["data"]["unresolved"] is True
assert edge["data"]["source"] == "repo:reef-railiance"
def test_graph_explorer_surfaces_repository_source_references() -> None:
graph = {
"apiVersion": "railiance.fabric/v1alpha1",

View file

@ -104,7 +104,12 @@ def test_scan_repo_emits_schema_valid_deterministic_snapshot(tmp_path: Path) ->
"readme_title",
"intent_present",
"scope_present",
"ui_status",
"ui_evidence",
}
attributes_by_name = {attribute["name"]: attribute["value"] for attribute in candidates["attributes"]}
assert attributes_by_name["ui_status"] == "candidate"
assert "package.json" in attributes_by_name["ui_evidence"]
for collection_name in ("nodes", "edges", "attributes"):
stable_keys = [item["stable_key"] for item in candidates[collection_name]]