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:
parent
c65b9eddfe
commit
564dc59c4f
5 changed files with 138 additions and 4 deletions
|
|
@ -43,6 +43,8 @@ The deterministic extractor framework currently covers:
|
||||||
|
|
||||||
- repository metadata from local git/path evidence
|
- repository metadata from local git/path evidence
|
||||||
- README, INTENT, and SCOPE document presence and headings
|
- 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/`
|
- repo-owned Fabric declarations under `fabric/`
|
||||||
- Python `pyproject.toml` package metadata and dependencies
|
- Python `pyproject.toml` package metadata and dependencies
|
||||||
- Node `package.json` 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
|
- common service config files such as `application.yaml` and
|
||||||
`appsettings.json`
|
`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
|
Each extractor emits candidates through the same accumulator so stable-key
|
||||||
duplicates merge inside a scan before the snapshot is returned.
|
duplicates merge inside a scan before the snapshot is returned.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -395,6 +395,7 @@ def _deterministic_extractors() -> list:
|
||||||
return [
|
return [
|
||||||
_extract_repo_metadata,
|
_extract_repo_metadata,
|
||||||
_extract_text_metadata,
|
_extract_text_metadata,
|
||||||
|
_extract_ui_metadata,
|
||||||
_extract_fabric_declarations,
|
_extract_fabric_declarations,
|
||||||
_extract_python_package,
|
_extract_python_package,
|
||||||
_extract_node_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:
|
def _extract_fabric_declarations(context: ScanContext) -> None:
|
||||||
declarations: list[tuple[Path, dict[str, Any]]] = []
|
declarations: list[tuple[Path, dict[str, Any]]] = []
|
||||||
for path in declaration_files(context.repo_path):
|
for path in declaration_files(context.repo_path):
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,17 @@ def draft202012_validator(schema_path: Path) -> jsonschema.Draft202012Validator:
|
||||||
registry = schema_registry(schema_path.parent)
|
registry = schema_registry(schema_path.parent)
|
||||||
if registry is None:
|
if registry is None:
|
||||||
return _legacy_validator(schema_path, schema)
|
return _legacy_validator(schema_path, schema)
|
||||||
return jsonschema.Draft202012Validator(
|
try:
|
||||||
schema,
|
return jsonschema.Draft202012Validator(
|
||||||
registry=registry,
|
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(
|
def _legacy_validator(
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,25 @@ def test_graph_explorer_collapses_discovered_repository_nodes() -> None:
|
||||||
assert declares_package["data"]["displayOnly"] is False
|
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:
|
def test_graph_explorer_surfaces_repository_source_references() -> None:
|
||||||
graph = {
|
graph = {
|
||||||
"apiVersion": "railiance.fabric/v1alpha1",
|
"apiVersion": "railiance.fabric/v1alpha1",
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,12 @@ def test_scan_repo_emits_schema_valid_deterministic_snapshot(tmp_path: Path) ->
|
||||||
"readme_title",
|
"readme_title",
|
||||||
"intent_present",
|
"intent_present",
|
||||||
"scope_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"):
|
for collection_name in ("nodes", "edges", "attributes"):
|
||||||
stable_keys = [item["stable_key"] for item in candidates[collection_name]]
|
stable_keys = [item["stable_key"] for item in candidates[collection_name]]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue