Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion commands/tech/generate_dr_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,36 @@
# colorize=True)
logger.add("debug_log", rotation="1 day", retention="1 week", compression="zip", level="TRACE", format=log_format,
colorize=True)
# ---------------------------------------------------------------------------
# See generate_md_cmd_templates.py's identical helper for the full rationale:
# relationship-only Update commands (their processor's
# supports_target_element_lookup() returns False) must not get the generic
# Referenceable upsert attributes (GUID, Qualified Name, Status, ...) --
# `command_verb in ["Create", "Update"]` alone can't distinguish them from a
# genuine element Update, and the compact spec's `upsert` flag means
# something else entirely (Create<->Update variant-name auto-generation).
_dispatcher_processors: Optional[dict] = None


def _get_dispatcher_processors() -> dict:
global _dispatcher_processors
if _dispatcher_processors is None:
from md_processing.dr_egeria import setup_dispatcher
_dispatcher_processors = setup_dispatcher(None).processors
return _dispatcher_processors


def _update_targets_referenceable_element(command_name: str) -> bool:
try:
processor_cls = _get_dispatcher_processors().get(command_name)
if processor_cls is None or "supports_target_element_lookup" not in vars(processor_cls):
return True
return bool(processor_cls.supports_target_element_lookup(None))
except Exception as e:
logger.warning(f"Could not determine target-element support for '{command_name}': {e}")
return True


def get_iso8601_datetime():
"""Returns the current date and time in ISO 8601 format."""
return datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
Expand Down Expand Up @@ -79,7 +109,7 @@ def _extract_help_fields(command: dict, client: Optional[ServerClient] = None):
command_spec = get_command_spec(command)
verb = command_spec.get('verb', None)
from md_processing.md_processing_utils.md_processing_constants import LINK_VERBS
if verb in ["Create", "Update"]:
if verb == "Create" or (verb == "Update" and _update_targets_referenceable_element(command)):
distinguished_attributes = command_spec.get('Attributes', [])
attributes = add_default_upsert_attributes(distinguished_attributes)
elif verb in LINK_VERBS:
Expand Down
50 changes: 48 additions & 2 deletions commands/tech/generate_md_cmd_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,51 @@

LEVEL_ORDER = ["Common", "Domain", "Basic", "Advanced", "Expert", "Invisible"]

# ---------------------------------------------------------------------------
# Relationship-only Update commands must not get the generic Referenceable
# upsert attributes (GUID, Qualified Name, Status, Category, ...) injected --
# they update a relationship's own properties by its own GUID, not a
# Referenceable element's. Confirmed 2026-09-16: the prior `command_verb in
# ["Create", "Update"]` gate here matched every "Update" command regardless
# of target, which is a different (broader) signal than compact_loader.py's
# actual runtime gate (`spec.get("upsert")` -- that flag means "does this
# command's Create variant auto-generate an Update synonym", not "does it
# target a Referenceable element", so it can't be reused here either: ~50
# legitimate element Create commands have upsert=False and still need these
# defaults). The processor's own `supports_target_element_lookup()` override
# (see AsyncBaseCommandProcessor and its relationship-only subclass
# overrides -- LineageLinkProcessor, GovernanceLinkProcessor, etc.) is the
# actual runtime source of truth for this distinction, so this generator
# reads it directly from the registered dispatcher rather than re-deriving
# a second, possibly-diverging heuristic from the compact spec.
_dispatcher_processors: Optional[dict] = None


def _get_dispatcher_processors() -> dict:
global _dispatcher_processors
if _dispatcher_processors is None:
from md_processing.dr_egeria import setup_dispatcher
# No live calls happen at registration time -- a dummy client is fine,
# this dispatcher is only ever used here to inspect registered classes.
_dispatcher_processors = setup_dispatcher(None).processors
return _dispatcher_processors


def _update_targets_referenceable_element(command_name: str) -> bool:
"""True unless the command's registered processor declares itself
relationship-only via `supports_target_element_lookup() -> False`.
Defaults to True (preserve prior behaviour) if the command isn't
registered or the check can't be made -- never silently strips an
element command's attributes."""
try:
processor_cls = _get_dispatcher_processors().get(command_name)
if processor_cls is None or "supports_target_element_lookup" not in vars(processor_cls):
return True
return bool(processor_cls.supports_target_element_lookup(None))
except Exception as e:
logger.warning(f"Could not determine target-element support for '{command_name}': {e}")
return True


def _level_visible(attr_level: str, usage_level: str = "Advanced") -> bool:
"""Return True if attr_level should be rendered at the given usage_level ceiling.
Expand Down Expand Up @@ -302,8 +347,9 @@ def main():
from md_processing.md_processing_utils.md_processing_constants import LINK_VERBS
if command_verb in LINK_VERBS:
attributes = add_default_link_attributes(copy.deepcopy(distinguished_attributes))
elif command_verb in ["Create", "Update"]:
# Create, Update uses upsert defaults
elif command_verb == "Create" or (command_verb == "Update" and _update_targets_referenceable_element(command)):
# Create always targets a Referenceable element in this codebase (confirmed);
# Update only sometimes does -- see _update_targets_referenceable_element.
attributes = add_default_upsert_attributes(copy.deepcopy(distinguished_attributes))
else:
attributes = copy.deepcopy(distinguished_attributes)
Expand Down
4 changes: 3 additions & 1 deletion docs/dr_egeria_manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,9 @@ Dr.Egeria organizes its commands into "families," each corresponding to a specif
- **Solution Architect**: Manage solution blueprints, components, information supply chains, and design patterns (e.g., `Create Design Pattern`, `Link Nested Design Patterns`, `Link Specialized Design Patterns`, `Link Related Design Patterns`).
- **Concept Models** (0571, added 2026-08-21): `Create Concept Bead`, `Create Concept Bead Attribute`, `Create Concept Bead Relationship`, and 8 relationship commands — `Link Concept Design`, `Link Concept Bead Relationship End`, `Link Typed By Concept Bead`, `Link Is A Concept Bead`, `Link Concept Bead Attribute Link`, `Link Concept Bead Extension`, `Link Solution Component Port`, `Link Solution Port Delegation` (`Detach`/`Unlink`/`Remove` variants auto-derived from each `Link` command). Routed through the existing generic `SolutionArchitectProcessor`/`SolutionLinkProcessor` (new `om_type` branches, no new processor classes needed) — auto-registered via `register_solution_architect_processors()`'s family-driven loop, same as every other Solution Architect command.
- `Create Concept Model` (added 2026-08-21) creates the `ConceptModel` container `Link Concept Design` attaches elements to. No dedicated `/solution-architect/concept-models` REST endpoint exists yet, so this routes through the generic `Collection Manager` creation path instead — added `"Concept Model"` to `COLLECTION_SUBTYPES` (`md_processing_constants.py`), which per this repo's own convention is the entire wiring step (auto-routes to `CollectionManagerProcessor`'s existing generic-collection-subtype fallback, no bespoke pyegeria method needed). Live-verified end to end against a running 6.2-SNAPSHOT server: `Create Concept Model` → `Create Concept Bead` → `Link Concept Design`, with the `ConceptDesign` relationship confirmed present via direct fetch afterward.
- `Link Implemented By` (0737, added 2026-09-13): attaches a design object (Information Supply Chain, Solution Component, Governance Definition, ...) to its implementation via the `ImplementedBy` relationship — a cross-OMVS call to `governance_officer`'s `linkDesignToImplementation`/`detachDesignFromImplementation` endpoints (`Detach`/`Unlink`/`Remove` variants auto-derived, same mechanism as the Concept Model relationship commands above). `Create Information Supply Chain` also gained an `Implemented By` (Reference Name List) attribute, synced the same way as its existing `Nested Information Supply Chains`/`In Information Supply Chain` attributes. Investigating a user report that `Link Information Supply Chain Child` created `ImplementedBy` instead of `CollectionMembership` found the command already correct in the current code (confirmed live) — the report didn't reproduce, likely stale from an earlier release. Along the way, fixed a real latent bug: `_get_supply_chain_rel_elements()` read a nonexistent `implementedByList` field instead of the real `implementedBy` (confirmed against `AttributedMetadataElement.java` and a live element fetch) — this had silently made the `Implemented By` sync's as-is diff always see zero existing relationships.
- `Link Implemented By` (0737, added 2026-09-13): attaches a design object (Information Supply Chain, Solution Component, Governance Definition, ...) to its implementation via the `ImplementedBy` relationship — a cross-OMVS call to `governance_officer`'s `linkDesignToImplementation`/`detachDesignFromImplementation` endpoints (`Detach`/`Unlink`/`Remove` variants auto-derived, same mechanism as the Concept Model relationship commands above). Along the way, fixed a real latent bug: `_get_supply_chain_rel_elements()` read a nonexistent `implementedByList` field instead of the real `implementedBy` (confirmed against `AttributedMetadataElement.java` and a live element fetch) — this had silently made the `Implemented By` sync's as-is diff always see zero existing relationships.
- **Correction, 2026-09-16:** the paragraph above's `Create Information Supply Chain`/`Create Solution Component` sync was itself wrong — `In Information Supply Chain` (on `Create Solution Component`) and `Implemented By` (on `Create Information Supply Chain`) were both syncing via `ImplementedBy` (the mechanism `Link Implemented By` legitimately uses), when `InformationSupplyChain` is a Collection subtype and these two attributes are ordinary collection membership. Reported live: 71 components each carrying `In Information Supply Chain` fanned each chain out to 143 `ImplementedBy` links, alongside the 47 correct `CollectionMembership`s from explicit `Add Member` blocks. Both sync points (`SolutionComponentProcessor._sync_all_rels`'s "Supply Chains" block and `SupplyChainProcessor._sync_rels`'s "Implemented By" block, `solution_architect.py`) now call `_async_add_to_collection`/`_async_remove_from_collection` with `CollectionMembershipProperties`, matching every other ISC membership sync in this file. Pre-existing bad `ImplementedBy` links created before this fix are not auto-cleaned — the as-is fetch now reads `collectionMembers`, not `implementedBy`, so a re-sync no longer sees or touches them; removing them is a separate one-off cleanup against affected servers.
- **Lineage Linker** (added 2026-09-16): Create/update/detach the seven Lineage Linker OMVS relationship types (0750/0755/0770 — DataFlow, ControlFlow, ProcessCall, LineageMapping, DataMapping, UltimateSource, UltimateDestination) between two elements, e.g. `Link Data Flow`, `Update Process Call`, `Link Data Mapping`. One dedicated `Link`/`Update` command pair per type (each with only its own type's attributes — Guard/Mandatory Guard for `Control Flow`; Query/Query ID/Query Type for `Data Mapping`; One Way/Integration Style/Protocol/Frequency/Data Exchanged, reused from Solution Architect's `SolutionLinkingWire` attributes, for the rest), plus one shared `Unlink Lineage Relationship` command since detaching only needs the relationship's own GUID. Split from an earlier single generic command triple that used a `Lineage Relationship Type` selector attribute offering every type's attributes regardless of which type was picked. The split also surfaced and fixed a real gap: the pyegeria SDK's `DataFlowProperties`/`ProcessCallProperties`/`LineageMappingProperties`/`UltimateSourceProperties`/`UltimateDestinationProperties` (`pyegeria/omvs/lineage_linker.py`) were missing `oneWay`/`integrationStyle`/`protocol`/`frequency`/`dataExchanged` (and `ProcessCallProperties` was missing `lineNumber`) entirely — real fields on the Egeria DTOs (`DataLineageRelationshipProperties`/`ProcessCallProperties.java`) that had no home on the pydantic model, so a caller passing them would validate silently and have them dropped before serialization (the `PyegeriaModel` `extra='ignore'` gotcha — see the SDK section below).
- **Governance Officer**: Manage governance definitions, policies, and responsibilities.
- **Action Author**: Define governance action process flows — reusable single-step action types and multi-step processes — and wire them to the engines and elements that execute them, without writing code (e.g., `Create Governance Action Process`, `Create Governance Action Process Step`, `Link First Process Step`, `Link Next Process Step`, `Link Action to Action Executor`, `Link Action to Target`).
- **Curation**: Apply classifications and relationships to *existing* Referenceable elements after the fact — this family creates no elements of its own, it curates ones created elsewhere. Every command names a `Target Element` (Reference Name) and either a classification level/status or a second element to relate to.
Expand Down
Loading
Loading