Manage kits: reconcile the schema on save, and a dialog to repin builds

Placing a palette component no longer edits <schema> straight away. The kit is
noted as pending, its manifest is compiled so the box can be painted, and the
status bar says the kit will be declared on save. The <schema> block is then
reconciled when the file is written, in the one place that can report the whole
kit list the file will carry, with each kit's version and checksum.

What a kit's manifest depends on counts as used too, transitively, so placing a
component can bring in more than one entry: hvac pulls in types, ontrolPion
pulls in ontrolPlatform.

New Kits > Manage Kits... dialog lists every kit in three groups -- needed by
the app, declared but unused, and merely installed -- with the pinned build, the
newest installed build, and an Action dropdown of every build a kit ships.
The tick means the schema declares the kit: grey and fixed for kits the app
needs, unticked to remove, ticked to add at the newest build. "Upgrade all kits
to latest version" moves every declared kit; "Update kits" applies the ticks and
the Action column, additions and removals included.

Every schema change is applied as a batch, then verified: a missing manifest or
a component type that stops resolving reverts the whole block and reports why,
and a kit the app needs cannot be removed. Changes are one undo step, which
recompiles the slot registry in both directions since a repin can change a
component's slots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 12:06:25 +03:00
co-authored by Claude Opus 5
parent b286b208da
commit 01dadea610
8 changed files with 851 additions and 45 deletions
+34
View File
@@ -5,6 +5,8 @@ import xml.etree.ElementTree as ET
import app_state
_dependency_cache = {} # manifest path -> tuple of kit names it depends on
class MissingManifestError(Exception):
"""A kit the file needs has no manifest under the Sedona home folder.
@@ -42,6 +44,38 @@ def describe_missing_manifests(error):
return "\n".join(lines)
def read_kit_dependencies(kit_name, checksum=None):
"""The kits a manifest declares its kit was built against, by name only.
A manifest lists them as <depend on="sys 1+"/>; the version bound is dropped here,
since the schema block pins a build by checksum and has nowhere to record a range.
An unreadable or absent manifest yields no dependencies rather than an error: the
callers use this to widen a list, and a kit whose manifest is missing is already
reported by whatever needed its types.
"""
manifest_path = find_manifest_path(kit_name, checksum)
if not manifest_path:
return ()
if manifest_path in _dependency_cache:
return _dependency_cache[manifest_path]
dependencies = []
try:
root = ET.parse(manifest_path).getroot()
depends_element = root.find("depends")
if depends_element is not None:
for depend in depends_element.findall("depend"):
on = (depend.get("on") or "").strip()
if on:
dependencies.append(on.split()[0])
except Exception as error:
print(f"Cannot read dependencies of {manifest_path}: {error}")
_dependency_cache[manifest_path] = tuple(dependencies)
return _dependency_cache[manifest_path]
def read_summary_facet(slot_element):
"""Reads a slot's '@summary' facet. Absent means summary, matching manifest convention."""
facets = slot_element.find("facets")