App shell with sidebar and theme switch; attribute-set management; project rename; new pages for projects, runbooks and activity
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 295952@bnt-lap001 Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
This commit is contained in:
parent
a6ee6a0a6e
commit
6579618737
9 changed files with 663 additions and 13 deletions
|
|
@ -144,3 +144,88 @@ def check_value(pack: P.Pack, name: str, value) -> str | None:
|
|||
except (ValueError, KeyError) as e:
|
||||
return str(e)
|
||||
return None
|
||||
|
||||
|
||||
# ---- managing attribute sets ---------------------------------------------------------------------------------------
|
||||
|
||||
import re as _re
|
||||
|
||||
SET_ID = _re.compile(r"[a-z0-9]+([.-][a-z0-9]+)*")
|
||||
ATTR_NAME = _re.compile(r"[a-z][a-z0-9_]*")
|
||||
|
||||
|
||||
def used_by(reg: Registry, set_id: str) -> dict[str, list[str]]:
|
||||
"""Who reuses an attribute set: projects and templates (also through targets), and sets that extend it."""
|
||||
out = {"projects": [], "templates": [], "sets": []}
|
||||
for kind, docs in (("projects", reg.projects), ("templates", reg.templates)):
|
||||
for did, d in docs.items():
|
||||
refs = list(d.data.get("attribute_sets", []))
|
||||
for t in d.data.get("targets", {}).values():
|
||||
refs += t.get("attribute_sets", [])
|
||||
if set_id in refs:
|
||||
out[kind].append(did)
|
||||
for sid, d in reg.sets.items():
|
||||
if sid != set_id and set_id in d.data.get("extends", []):
|
||||
out["sets"].append(sid)
|
||||
return out
|
||||
|
||||
|
||||
def _would_cycle(reg: Registry, set_id: str, extends: list[str]) -> bool:
|
||||
def reaches(start: str, seen: set) -> bool:
|
||||
if start == set_id:
|
||||
return True
|
||||
if start in seen or start not in reg.sets:
|
||||
return False
|
||||
seen.add(start)
|
||||
return any(reaches(x, seen) for x in reg.sets[start].data.get("extends", []))
|
||||
return any(reaches(x, set()) for x in extends)
|
||||
|
||||
|
||||
def save_set(reg: Registry, root: Path, set_id: str, title: str, extends: list[str],
|
||||
attributes: dict[str, dict], existing_path: Path | None = None) -> Path:
|
||||
"""Write an attribute set file. `attributes` maps name -> {value, requirement, note} (any part may be empty).
|
||||
|
||||
Validates the id, title, names, secrets, that extended sets exist and that nothing becomes circular.
|
||||
"""
|
||||
if not SET_ID.fullmatch(set_id or ""):
|
||||
raise ValueError("the id is lower-case letters, digits, dots and hyphens")
|
||||
if len((title or "").strip()) < 3:
|
||||
raise ValueError("the title needs at least 3 characters")
|
||||
extends = [x for x in extends if x]
|
||||
for x in extends:
|
||||
if x == set_id:
|
||||
raise ValueError("a set cannot extend itself")
|
||||
if x not in reg.sets:
|
||||
raise ValueError(f"unknown attribute set {x} in extends")
|
||||
if _would_cycle(reg, set_id, extends):
|
||||
raise ValueError("that extends list would make the sets circular")
|
||||
out: dict = {}
|
||||
for name, a in attributes.items():
|
||||
if not ATTR_NAME.fullmatch(name):
|
||||
raise ValueError(f"bad attribute name {name!r}: lower-case letters, digits and underscores, starting with a letter")
|
||||
value = a.get("value")
|
||||
value = None if value is None or str(value).strip() == "" else str(value).strip()
|
||||
req = a.get("requirement") or None
|
||||
if req not in (None, "required", "optional"):
|
||||
raise ValueError(f"{name}: requirement is required or optional")
|
||||
note = (a.get("note") or "").strip() or None
|
||||
if value is None and req is None and note is None:
|
||||
raise ValueError(f"{name}: give a value, a requirement or a note")
|
||||
for text in (value or "", note or ""):
|
||||
if SECRET_RE.search(text):
|
||||
raise ValueError(f"{name}: looks like a secret; attribute sets are never secret")
|
||||
entry = {k: v for k, v in (("value", value), ("requirement", req), ("note", note)) if v is not None}
|
||||
out[name] = entry["value"] if list(entry) == ["value"] else entry
|
||||
if SECRET_RE.search(title):
|
||||
raise ValueError("the title looks like a secret")
|
||||
doc: dict = {"spec": "attribute-set/v0.1", "id": set_id, "title": title.strip()}
|
||||
if extends:
|
||||
doc["extends"] = extends
|
||||
doc["attributes"] = out
|
||||
errs = _schema_errors(doc, "attribute-set.schema.json")
|
||||
if errs:
|
||||
raise ValueError(errs[0])
|
||||
path = existing_path or (Path(root) / "attribute-sets" / f"{set_id}.yaml")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(yaml.safe_dump(doc, sort_keys=False))
|
||||
return path
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue