- Create docs/examples/example-addon/ with a complete, copy-paste-ready
addon skeleton demonstrating command registration, context injection,
dock panels, lifecycle hooks, and event bus subscription
- Add Examples section to docs/src/SUMMARY.md
- Add quick-start cross-reference in writing-an-addon.md
- Add Per-Document Origin API section to kcsdk-python.md covering
document_origin, set_document_origin, clear_document_origin, and
find_owning_origin with parameter tables and usage example
- Add 4 per-document functions to kindred_sdk wrappers table showing
the doc.Name extraction pattern
- Add Python Exposure subsection to cpp-origin-manager.md cross-
referencing both kcsdk and kindred_sdk layers
New docs/src/development/testing.md covering:
- Quick reference commands (pixi run test-kindred, pixi run test)
- Tier 1 pure Python tests: mock strategy, test file table, addon
loader test class breakdown, how to write a new test file
- Tier 2 FreeCAD headless tests: runner, current status
- C++ GoogleTest infrastructure: directory layout, test executables
- Decision guide for which tier to use
Add entry to docs/src/SUMMARY.md under Development section.
Add 71 tests covering the entire addon loader pipeline in a new
test_kindred_addon_loader.py file:
- TestAddonState (2): enum members and values
- TestValidVersion (11): _valid_version() regex matching
- TestParseVersionLoader (7): _parse_version() string-to-tuple
- TestAddonRegistry (10): register/get/filter/order/contexts
- TestParseManifest (13): XML parsing, field extraction, validation
errors for bad priority, bad version format, invalid context IDs
- TestValidateDependencies (4): cross-addon dependency checking
- TestValidateManifest (9): version bounds, paths, error accumulation
- TestScanAddons (6): directory walking at depth 1 and 2
- TestResolveLoadOrder (9): topological sort, priority ties, cycle
detection, legacy fallback
Update test runner discovery pattern from 'test_kindred_pure.py' to
'test_kindred_*.py' so both test files are auto-discovered.
All 110 tests pass (71 new + 39 existing).
Refs #396
- Change AddonManifest.error (str) to .errors (list[str]) so all
problems are accumulated in a single pass instead of masking
subsequent failures.
- Validate load_priority type, version string format (dotted-numeric),
and context ID syntax (alphanumeric + dots/underscores) during
parse_manifest().
- Add validate_dependencies() cross-addon check that verifies all
declared dependencies reference discovered addon names, called
between parsing and validation in the pipeline.
- Rewrite validate_manifest() to run all checks (version bounds,
workbench path, Init.py presence) and collect errors instead of
early-returning on the first failure.
- Simplify resolve_load_order() by removing the inline
unknown-dependency check (now handled earlier by
validate_dependencies()).
- Update _print_load_summary() to join multiple errors with semicolons.
Closes#388
Expose the existing C++ per-document origin tracking through the kcsdk
pybind11 module and kindred_sdk Python package.
New kcsdk functions (accept document name string):
- document_origin(doc_name) — get origin via originForDocument()
- set_document_origin(doc_name, origin_id) — explicit association
- clear_document_origin(doc_name) — clear explicit association
- find_owning_origin(doc_name) — ownership detection (no cache)
New kindred_sdk wrappers (accept App.Document object):
- document_origin(doc)
- set_document_origin(doc, origin_id)
- clear_document_origin(doc)
- find_owning_origin(doc)
Replaces the flat context model with a tree-structured hierarchy:
- ContextDefinition gains parentId field for declaring parent-child
relationships between contexts
- Resolver builds a context stack by walking parentId links from
leaf to root, verifying each ancestor matches current state
- Breadcrumb is now auto-built from the stack — each level
contributes its expanded label and color, replacing all hardcoded
special cases
- EditingContext gains stack field (QStringList, root to leaf)
Transition guards (#386):
- addTransitionGuard() / removeTransitionGuard() on resolver
- Guards run synchronously before applyContext(); first rejection
cancels the transition and emits contextTransitionBlocked signal
- Full SDK/pybind11/Python bindings
Breadcrumb injection (#387):
- injectBreadcrumb() / removeBreadcrumbInjection() on resolver
- Addons can append segments to any context's breadcrumb display
- Active only when the target context is in the current stack
- Full SDK/pybind11/Python bindings
Built-in parent assignments:
- partdesign.body → partdesign.workbench
- partdesign.feature → partdesign.body
- partdesign.in_assembly → assembly.edit
- sketcher.edit → partdesign.body
- assembly.idle → assembly.workbench
- assembly.edit → assembly.idle
- Workbench-level and root contexts have no parent
SDK surface:
- Types.h: parentId on ContextDef, stack on ContextSnapshot
- SDKRegistry: guard/injection delegation, snapshotFromGui helper
- kcsdk_py: parent_id param, context_stack(), guard/injection bindings
- kindred_sdk: context_stack(), add/remove_transition_guard(),
inject/remove_breadcrumb_injection(), parent_id on register_context()
Closes#385, closes#386, closes#387
Add kindred_sdk.addon_diagnostics() returning per-addon load state,
timing, and error info as a list of dicts. Reads name, state,
load_time_ms, and error from AddonManifest via AddonRegistry.
Add _print_load_summary() to addon_loader.py that prints a formatted
summary table to the console after each load phase (Init.py and
InitGui.py), replacing interleaved individual log lines with a
consolidated view.
Closes#390
Add kindred_sdk.addon_resource(name, relative_path) that resolves
bundled asset paths relative to an addon's root directory. Looks up
the addon manifest from AddonRegistry for the install root, joins
the relative path, and validates existence on disk.
Raises LookupError if the addon is not registered, FileNotFoundError
if the resolved path does not exist.
Closes#389
C++ layer:
- EditingContextResolver::registeredContexts() returns static metadata
(id, labelTemplate, color, priority) for all registered contexts
- SDKRegistry::registeredContexts() wraps with Qt-to-std conversion
- kcsdk.available_contexts() pybind11 binding returns list of dicts
Python layer:
- kindred_sdk.available_contexts() wraps kcsdk binding
- kindred_sdk.context_history(limit=10) returns recent transitions
from a 50-entry ring buffer tracked in lifecycle.py, with timestamps
Closes#383
Expose EditingContextResolver::contextChanged to Python addons via
two-layer design:
C++ layer:
- SDKRegistry::onContextChanged() stores callbacks and lazily connects
to the Qt signal on first registration
- pybind11 binding kcsdk.on_context_changed() wraps Python callables
with GIL-safe invocation
Python layer:
- kindred_sdk.on_context_enter(context_id, callback) subscribes to
context activation ("*" wildcard supported)
- kindred_sdk.on_context_exit(context_id, callback) subscribes to
context deactivation
- Internal tracking of previous context derives enter/exit transitions
- Emits context.enter / context.exit on the SDK event bus
Closes#381
New module kindred_sdk/events.py provides lightweight publish-subscribe
so addons can signal each other without direct imports.
New public API:
- sdk.on(event, handler) — subscribe to a named event
- sdk.off(event, handler) — unsubscribe
- sdk.emit(event, data) — publish event with dict payload
Pure Python, synchronous dispatch, snapshot-safe iteration.
Handlers that raise are logged and skipped without breaking the chain.
New module kindred_sdk/registry.py wraps FreeCAD.KindredAddons so
addons use a stable SDK import instead of reaching into FreeCAD
internals directly.
New public API:
- sdk.is_addon_loaded(name) — boolean check
- sdk.addon_version(name) — version string or None
- sdk.loaded_addons() — list of loaded addon names in load order
Add .mailmap to parent repo and all submodules (silo, gears, datums,
solver) to normalize historical author/committer display.
Maps stale identities (forbes-0023, josephforbes23, Zoe Forbes, admin)
and old emails to: forbes <contact@kindred-systems.com>
rattler-build's source.path copy does not include submodule contents —
directories like mods/silo/, mods/solver/, mods/gears/, and mods/datums/
end up empty in the work directory, causing cmake --install to fail
when it tries to install files from those paths.
Add a clone_if_empty helper to build.sh that detects empty submodule
directories and shallow-clones them from their remotes before CMake
runs. Add git to build requirements in recipe.yaml.
This fixes the release build (AppImage) failure where cmake --install
failed at the silo addon install step after successfully installing
the silo tree-node icons from src/Mod/Create/resources/.
Convert mods/datums/ from tracked files to a git submodule pointing
to https://git.kindred-systems.com/kindred/datums.git (branch: main).
Follows the same submodule pattern as silo, solver, and gears.
The datums remote repo was initialized from the existing tracked files
with the package.xml repository URL updated to point to the new repo.
CMake install targets in src/Mod/Create/CMakeLists.txt continue to
work unchanged since the files remain at the same paths.
Update silo submodule to include root-level package.xml, Init.py, and
InitGui.py following the standard addon layout used by sdk, gears,
datums, and solver.
Move five Silo-specific deferred setup functions from
src/Mod/Create/InitGui.py into the silo addon's own InitGui.py:
- Document observer registration (600ms)
- Auth dock panel via kindred_sdk.register_dock_panel() (2000ms)
- First-start settings check (3000ms)
- Activity dock panel via kindred_sdk.register_dock_panel() (4000ms)
- Remove duplicate origin registration (was 1500ms, already done in
SiloWorkbench.Initialize())
Update CMake install targets to include root-level silo files.
Create core InitGui.py now only handles kc_format (500ms) and
update_checker (10000ms).