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
|
||||
- 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue