uofa-demo / src /uofa_cli /paths.py
system's picture
system HF Staff
CI: sync Gap-Finder Space (cdc3eff)
0cc7125 verified
Raw
History Blame Contribute Delete
23.9 kB
"""Auto-discovery of UofA repo assets (spec files, JAR, keys, packs)."""
from __future__ import annotations
import functools
import json
import os
import shutil
from pathlib import Path
_MARKER = Path("spec") / "schemas" / "uofa_shacl.ttl"
_PACK_MARKER = Path("packs") / "core" / "pack.json"
_repo_root_cache = None
# Interface versions the core provides. A pack capability declares the interface
# + version it implements; the loader enforces major-version compatibility (Β§7).
# All four capability legs are recognized at the load gate: detection (P2),
# measurement (Β§3/P3), reference (Β§3a/P4), guardrail (Β§6/P6).
CORE_INTERFACE_VERSIONS: dict[str, str] = {
"detection": "1.0",
"measurement": "1.0",
"reference": "1.0",
"guardrail": "1.0",
}
def resolve_active_packs(args=None) -> list[str]:
"""The active pack set for this invocation β€” the P2d explicit-threading accessor.
Reads ``args.active_packs`` (set once by the CLI entry point) when present;
otherwise defaults to the open-core baseline pack ``vv40``. There is no
process global (removed in P2d-3) β€” commands resolve here and thread the
result down explicitly.
"""
explicit = getattr(args, "active_packs", None)
return list(explicit) if explicit else ["vv40"]
def find_repo_root(override: str = None) -> Path:
"""Find the UofA repo root by walking up from cwd looking for markers."""
global _repo_root_cache
if override:
root = Path(override)
if (root / _PACK_MARKER).exists() or (root / _MARKER).exists():
_repo_root_cache = root
return root
raise FileNotFoundError(
f"Not a UofA repo: {root} (missing {_PACK_MARKER} and {_MARKER})"
)
if _repo_root_cache:
return _repo_root_cache
# Walk up from cwd
current = Path.cwd()
for parent in [current, *current.parents]:
if (parent / _PACK_MARKER).exists() or (parent / _MARKER).exists():
_repo_root_cache = parent
return parent
# Walk up from package location
pkg_dir = Path(__file__).parent
for parent in [pkg_dir, *pkg_dir.parents]:
if (parent / _PACK_MARKER).exists() or (parent / _MARKER).exists():
_repo_root_cache = parent
return parent
# Wheel-bundled snapshot: pyproject.toml force-includes packs/ + spec/
# under <package>/_data/repo/ so installed wheels work from any cwd.
bundled = pkg_dir / "_data" / "repo"
if (bundled / _PACK_MARKER).exists() or (bundled / _MARKER).exists():
_repo_root_cache = bundled
return bundled
raise FileNotFoundError(
"Could not find UofA repo root. "
"Run from inside the repo or pass --repo-root PATH."
)
# ── Pack resolution ──────────────────────────────────────────
def pack_dir(pack_name: str = None, root: Path = None, active: list[str] = None) -> Path:
"""Return the directory for the given pack.
``pack_name`` wins when given; otherwise the first ``active`` pack (or
``core``) is used. ``active`` is the explicit active-pack set (P2d threading);
when None it defaults to the open-core baseline pack ``vv40``.
"""
root = root or find_repo_root()
if active is None:
active = ["vv40"]
name = (pack_name or active[0]) if active else "core"
return root / "packs" / name
def pack_manifest(pack_name: str = None, root: Path = None) -> dict:
"""Load and return the pack manifest (pack.json). Plain loader β€” no validation.
Validation happens once at the load gate (``validate_active_packs``), not on
every access, so this stays a cheap reader.
"""
manifest_path = pack_dir(pack_name, root) / "pack.json"
if not manifest_path.exists():
raise FileNotFoundError(f"Pack manifest not found: {manifest_path}")
return json.loads(manifest_path.read_text())
def pack_manifest_schema_path(root: Path = None) -> Path:
"""Path to the pack-manifest JSON Schema (the Β§7 compatibility contract)."""
root = root or find_repo_root()
return root / "specs" / "pack_manifest_schema.json"
@functools.lru_cache(maxsize=4)
def _manifest_schema(schema_path_str: str) -> dict:
return json.loads(Path(schema_path_str).read_text(encoding="utf-8"))
def validate_pack_manifest(manifest: dict, pack_name: str, root: Path = None) -> None:
"""Validate a pack manifest against the pack-manifest JSON Schema. Raises ValueError.
Pack-shaped architecture Β§7: real load-time enforcement replacing the old
bare ``json.loads`` + directory-exists check. Legacy-tolerant during the
migration (the schema still accepts the pre-``capabilities`` flat fields), so
unmigrated packs validate unchanged.
"""
import jsonschema
schema = _manifest_schema(str(pack_manifest_schema_path(root)))
try:
jsonschema.validate(manifest, schema)
except jsonschema.ValidationError as exc:
loc = "/".join(str(p) for p in exc.path) or "(root)"
raise ValueError(
f"Pack '{pack_name}' manifest is invalid at {loc}: {exc.message}"
) from exc
def detection_config(manifest: dict) -> dict:
"""Detection config (shapes/rules/oos/derivations/patternIds) from a manifest.
Reads the detection capability's payload. Every pack is migrated to the
``capabilities[]`` shape (the legacy flat-field fallback was removed in P2c
drop-shim), so a pack with no detection capability returns all-None. The one
place that knows the detection-payload shape, used by every loader /
resolver / info-command.
"""
for cap in manifest.get("capabilities", []):
if cap.get("leg") == "detection":
payload = cap.get("payload") or {}
return {
"shapes": payload.get("shapes"),
"rules": payload.get("rules"),
"oos": payload.get("oos"),
"derivations": payload.get("derivations"),
"patternIds": payload.get("patternIds"),
"factorFocus": payload.get("factorFocus"),
}
return {"shapes": None, "rules": None, "oos": None, "derivations": None,
"patternIds": None, "factorFocus": None}
@functools.lru_cache(maxsize=4)
def _patternid_pack_index_cached(root_str: str) -> tuple[tuple[str, str], ...]:
root = Path(root_str)
index: dict[str, str] = {}
for name in list_packs(root):
try:
manifest = pack_manifest(name, root=root)
except FileNotFoundError:
continue
for pid in detection_config(manifest).get("patternIds") or []:
index.setdefault(pid, name) # first declarer wins (core owns reused base ids)
return tuple(index.items())
def patternid_pack_index(root: Path = None) -> dict[str, str]:
"""``{patternId: owning detection pack}`` built from the loaded manifests.
The provenance-attribution index (Β§5/Β§7.3): records which detection pack
contributes each weakener patternId, so reasoned output can stamp which pack
fired which weakener. Same data the loader uses (``detection_config`` payloads).
A patternId reused from core resolves to ``core`` (first declarer wins β€”
matching the base-vocabulary semantics in ``_enforce_pack_compatibility``).
Returns ``{}`` if the repo root can't be resolved.
"""
try:
root = root or find_repo_root()
except FileNotFoundError:
return {}
return dict(_patternid_pack_index_cached(str(root)))
@functools.lru_cache(maxsize=16)
def _factor_focus_index_cached(root_str: str, packs_key: str) -> tuple[tuple[str, tuple[str, ...]], ...]:
root = Path(root_str)
merged: dict[str, list[str]] = {}
for name in packs_key.split(","):
try:
manifest = pack_manifest(name, root=root)
except FileNotFoundError:
continue
focus = detection_config(manifest).get("factorFocus") or {}
for pid, names in focus.items():
bucket = merged.setdefault(pid, [])
for fac in names or []:
if fac not in bucket: # union across packs, order-preserving
bucket.append(fac)
return tuple((pid, tuple(names)) for pid, names in merged.items())
def factor_focus_index(packs: list[str] = None, root: Path = None) -> dict[str, list[str]]:
"""``{patternId: [credibility factor name, ...]}`` merged from manifests.
The semantic weakener→factor map (declared in each detection payload's
``factorFocus``): which credibility factor(s) a weakener implicates when its
``affectedNode`` is a validation-result/COU node rather than a factor IRI, so
a concern can demote the factor it bears on. Merged over ``core`` + the active
packs (union, order-preserving), so a pack augments core (e.g. NASA adds
``Data pedigree`` to core's ``W-PROV-01`` focus). Callers filter the result to
factors expected for the bundle's pack. Returns ``{}`` if root can't resolve.
"""
try:
root = root or find_repo_root()
except FileNotFoundError:
return {}
active = packs or ["vv40"]
# core first so it owns the base mapping; dedup while preserving order.
ordered = ["core"] + [p for p in active if p != "core"]
seen, packs_norm = set(), []
for p in ordered:
if p not in seen:
seen.add(p)
packs_norm.append(p)
return {pid: list(names) for pid, names in _factor_focus_index_cached(str(root), ",".join(packs_norm))}
def list_packs(root: Path = None) -> list[str]:
"""Return names of all installed packs (directories under packs/ with pack.json)."""
root = root or find_repo_root()
packs_root = root / "packs"
if not packs_root.exists():
return []
return sorted(
d.name for d in packs_root.iterdir()
if d.is_dir() and (d / "pack.json").exists()
)
# ── Asset paths (pack-aware) ────────────────────────────────
def shacl_schema(root: Path = None) -> Path:
"""Return core SHACL shapes path (always loaded)."""
root = root or find_repo_root()
try:
manifest = pack_manifest("core", root=root)
shapes_rel = detection_config(manifest).get("shapes")
if shapes_rel:
pack_path = pack_dir("core", root=root) / shapes_rel
if pack_path.exists():
return pack_path
except (FileNotFoundError, KeyError):
pass
return root / "spec" / "schemas" / "uofa_shacl.ttl"
def _version_tuple(v: str) -> tuple[int, int, int]:
parts: list[int] = []
for p in str(v).split(".")[:3]:
digits = "".join(ch for ch in p if ch.isdigit())
parts.append(int(digits) if digits else 0)
while len(parts) < 3:
parts.append(0)
return parts[0], parts[1], parts[2]
def _satisfies(version: str, requirement: str) -> bool:
"""Minimal semver-range check: supports >=, <=, ==, >, < and a bare version (>=)."""
req = str(requirement).strip()
for op in (">=", "<=", "==", ">", "<"):
if req.startswith(op):
target, v = _version_tuple(req[len(op):]), _version_tuple(version)
return {">=": v >= target, "<=": v <= target, "==": v == target,
">": v > target, "<": v < target}[op]
return _version_tuple(version) >= _version_tuple(req)
def _enforce_pack_compatibility(manifests, core_version, available):
"""Β§7 cross-pack enforcement over [(name, manifest), ...] for core + active packs.
Checks core-version range, capability interface versions, declared
dependencies, and patternId collisions ACROSS NON-CORE packs (core's
patternIds are the reusable base vocabulary β€” iso42001 deliberately reuses
W-PROV-01/W-AR-02/W-AL-02, so a core↔pack overlap is not a collision). Raises
ValueError on the first incompatibility β€” loud, never silent.
"""
pattern_owner: dict[str, str] = {}
for name, m in manifests:
cc = m.get("coreCompatibility")
if cc and core_version and not _satisfies(core_version, cc):
raise ValueError(
f"Pack '{name}' requires core {cc} but the loaded core is {core_version}."
)
for dep in m.get("dependencies", []):
if dep.get("pack") not in available and dep.get("pack") != "core":
raise ValueError(
f"Pack '{name}' depends on pack '{dep.get('pack')}' which is not installed."
)
for cap in m.get("capabilities", []):
iface = cap.get("targetInterface")
core_iface_ver = CORE_INTERFACE_VERSIONS.get(iface)
if core_iface_ver is None:
raise ValueError(
f"Pack '{name}' capability '{cap.get('capabilityId')}' targets unknown "
f"interface '{iface}'. Core provides: {sorted(CORE_INTERFACE_VERSIONS)}."
)
if _version_tuple(cap.get("interfaceVersion", "0"))[0] != _version_tuple(core_iface_ver)[0]:
raise ValueError(
f"Pack '{name}' capability '{cap.get('capabilityId')}' needs {iface} "
f"v{cap.get('interfaceVersion')}; core provides v{core_iface_ver} (major mismatch)."
)
if name != "core":
for pid in (cap.get("payload") or {}).get("patternIds") or []:
if pattern_owner.get(pid, name) != name:
raise ValueError(
f"patternId '{pid}' is declared by both '{pattern_owner[pid]}' and "
f"'{name}' β€” collision across active packs."
)
pattern_owner[pid] = name
def validate_active_packs(root: Path = None, active: list[str] = None):
"""Validate core + all active packs at the load gate. Raises on first problem.
Pack-shaped Β§7 enforcement (was directory-exists only): each pack must exist,
its manifest must conform to the schema, AND the active set must be mutually
compatible β€” core-version range, capability interface versions, declared
dependencies, and no patternId collisions across active packs. A missing pack
raises FileNotFoundError; anything else raises ValueError β€” loud failure,
never silent degradation. ``active`` is the explicit active set (P2d); None
defaults to the open-core baseline pack ``vv40``.
"""
root = root or find_repo_root()
if active is None:
active = ["vv40"]
available = list_packs(root)
core_version = None
manifests: list[tuple[str, dict]] = []
seen: set[str] = set()
for pack_name in ["core", *active]:
if pack_name in seen:
continue
seen.add(pack_name)
if pack_name != "core" and pack_name not in available:
raise FileNotFoundError(
f"Pack '{pack_name}' not found. "
f"Available packs: {', '.join(available)}"
)
manifest = pack_manifest(pack_name, root=root)
validate_pack_manifest(manifest, pack_name, root=root)
manifests.append((pack_name, manifest))
if pack_name == "core":
core_version = manifest.get("version")
_enforce_pack_compatibility(manifests, core_version, available)
def all_shacl_schemas(root: Path = None, active: list[str] = None) -> list[Path]:
"""Return SHACL shape file paths for core + all active packs.
``active`` is the explicit active set (P2d); None defaults to the open-core
baseline pack ``vv40``.
"""
root = root or find_repo_root()
if active is None:
active = ["vv40"]
validate_active_packs(root, active=active)
paths_list = [shacl_schema(root)]
for pack_name in active:
if pack_name == "core":
continue
try:
manifest = pack_manifest(pack_name, root=root)
shapes_rel = detection_config(manifest).get("shapes")
if shapes_rel:
shapes_path = pack_dir(pack_name, root=root) / shapes_rel
if shapes_path.exists():
paths_list.append(shapes_path)
except (FileNotFoundError, KeyError):
pass
return paths_list
def context_file(root: Path = None) -> Path:
root = root or find_repo_root()
return root / "spec" / "context" / "v0.5.jsonld"
_BUNDLED_JAR_NAME = "uofa-weakener-engine-0.1.0.jar"
def _package_dir() -> Path:
"""Directory containing this package (the installed/editable uofa_cli/)."""
return Path(__file__).parent
def bundled_jar() -> Path | None:
"""Return the JAR bundled inside the wheel, or None if not present.
Populated at wheel-build time by hatch_build.py when UOFA_BUNDLE_JAR=1.
Editable installs from a source checkout will not have it; callers fall
back to the Maven-built JAR under src/weakener-engine/target/.
"""
p = _package_dir() / "_engine" / _BUNDLED_JAR_NAME
return p if p.exists() else None
def jar_path(root: Path = None) -> Path:
bundled = bundled_jar()
if bundled is not None:
return bundled
root = root or find_repo_root()
return root / "src" / "weakener-engine" / "target" / _BUNDLED_JAR_NAME
def bundled_jre_executable() -> Path | None:
"""Return the bundled JRE's java binary, or None if not present.
Populated at wheel-build time by hatch_build.py when
UOFA_BUNDLE_PLATFORM=<tag> is set. Editable installs from a source
checkout will not have it; callers fall back to the system PATH.
"""
base = _package_dir() / "_runtime" / "jre"
if not base.exists():
return None
binary = base / "bin" / ("java.exe" if os.name == "nt" else "java")
return binary if binary.exists() else None
def java_executable() -> str:
"""Return the path to a usable java binary.
Resolution order:
1. Bundled JRE inside the wheel (preferred for pip installs).
2. System ``java`` on PATH (fallback for source-tree dev work).
Raises FileNotFoundError if neither is available. Returns a string so
callers can drop it directly into a subprocess argv.
"""
bundled = bundled_jre_executable()
if bundled is not None:
return str(bundled)
on_path = shutil.which("java")
if on_path:
return on_path
raise FileNotFoundError(
"Java not found. Install Java 17+ (https://adoptium.net/) "
"or use a UofA wheel that bundles a JRE."
)
def engine_dir(root: Path = None) -> Path:
root = root or find_repo_root()
return root / "src" / "weakener-engine"
def rules_file(input_path: Path = None, root: Path = None) -> Path:
"""Find rules file: same dir as input, then parent dir, then core pack rules dir."""
if input_path:
local = input_path.parent / "uofa_weakener.rules"
if local.exists():
return local
parent = input_path.parent.parent / "uofa_weakener.rules"
if parent.exists():
return parent
# Core pack rules
root = root or find_repo_root()
try:
manifest = pack_manifest("core", root=root)
rules_rel = detection_config(manifest).get("rules")
if rules_rel:
pack_path = pack_dir("core", root=root) / rules_rel
if pack_path.exists():
return pack_path
except (FileNotFoundError, KeyError):
pass
return root / "packs" / "core" / "rules" / "uofa_weakener.rules"
def all_rules_files(input_path: Path = None, root: Path = None, active: list[str] = None) -> list[Path]:
"""Return rules file paths for core + all active packs.
``active`` is the explicit active set (P2d); None defaults to the open-core
baseline pack ``vv40``.
"""
root = root or find_repo_root()
if active is None:
active = ["vv40"]
paths_list = [rules_file(input_path, root)]
for pack_name in active:
if pack_name == "core":
continue
try:
manifest = pack_manifest(pack_name, root=root)
rules_rel = detection_config(manifest).get("rules")
if rules_rel:
rules_path = pack_dir(pack_name, root=root) / rules_rel
if rules_path.exists():
paths_list.append(rules_path)
except (FileNotFoundError, KeyError):
pass
return paths_list
def template_path(root: Path = None) -> Path:
"""Return the pack template path (for future uofa import)."""
root = root or find_repo_root()
try:
manifest = pack_manifest(root=root)
return pack_dir(root=root) / manifest.get("template", "")
except (FileNotFoundError, KeyError):
return pack_dir(root=root) / "templates"
def extract_prompt(root: Path = None) -> Path:
"""Return the pack extract prompt path (for future uofa extract)."""
root = root or find_repo_root()
try:
manifest = pack_manifest(root=root)
return pack_dir(root=root) / manifest.get("prompt", "")
except (FileNotFoundError, KeyError):
return pack_dir(root=root) / "prompts"
def default_pubkey(root: Path = None) -> Path:
root = root or find_repo_root()
return root / "keys" / "research.pub"
def templates_dir(root: Path = None) -> Path:
root = root or find_repo_root()
return root / "packs" / "core" / "templates"
def examples_dir(root: Path = None) -> Path:
"""Return the first pack examples directory found, for backward compat.
For scanning all pack examples, use all_example_dirs() instead.
"""
root = root or find_repo_root()
# Return first pack with examples/
packs_root = root / "packs"
for d in sorted(packs_root.iterdir()):
if d.is_dir() and (d / "examples").is_dir():
return d / "examples"
return packs_root / "vv40" / "examples"
def all_example_dirs(root: Path = None) -> list[Path]:
"""Return all pack example directories."""
root = root or find_repo_root()
packs_root = root / "packs"
dirs = []
for d in sorted(packs_root.iterdir()):
if d.is_dir() and (d / "examples").is_dir():
dirs.append(d / "examples")
return dirs
# ── Project root detection (uofa.toml) ─────────────────────
def find_project_root(start: Path = None) -> Path | None:
"""Walk up from start (default: cwd) looking for uofa.toml.
Returns the directory containing uofa.toml, or None if not found.
"""
current = (start or Path.cwd()).resolve()
while True:
if (current / "uofa.toml").is_file():
return current
parent = current.parent
if parent == current:
return None
current = parent
def load_project_config(project_root: Path) -> dict:
"""Load and parse uofa.toml from a project root.
Returns a flat dict with resolved values.
"""
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib # type: ignore[no-redef]
toml_path = project_root / "uofa.toml"
with open(toml_path, "rb") as f:
raw = tomllib.load(f)
project = raw.get("project", {})
paths_section = raw.get("paths", {})
extract = raw.get("extract", {})
return {
"name": project.get("name", project_root.name),
"pack": project.get("pack", "vv40"),
"profile": project.get("profile", "complete"),
"output": project_root / paths_section.get("output", "."),
"evidence": project_root / paths_section.get("evidence", "evidence"),
"template": project_root / paths_section.get("template", "uofa-template.xlsx"),
"provider": extract.get("provider", "ollama"),
"model": extract.get("model", "llama3.2"),
}