agentleFS
lasplasp/adamant-skills/adamant-framework-internals/SKILL.md

adamant-framework-internals

Skill4 starsChanged 3 months agoApache-2.0View on GitHub

Deep-dive into Adamant framework internals for debugging code generation bugs, model lifecycle issues, and assembly load problems

The file

Show its rules and text

We split this skill into the rules it gives an agent

16 rules. About 0 are common, 3 are distinctive, and 13 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 13

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill when debugging code generation misbehavior, fixing bugs in framework Python models, or understanding the assembly loading …

    line 8
  2. Very specific:

    All paths relative to $ADAMANT_DIR (the adamant/ repo root).

    line 12
  3. Very specific:

    When a component ships its own gen/models/ directory, those Python files override the generic framework models for that component's YAML …

    line 28
  4. Very specific:

    Rule: Always use is (Python identity operator) when comparing component model objects that should represent specific assembly instances:

    line 54
  5. Very specific:

    After connect(components) resolves the connection:

    line 90
  6. Distinctive:

    When a component needs to resolve types from connected peers:

    line 136
  7. Very specific:

    When a component resolves types from its own init parameters (e.g., parameters_packets.py):

    line 158
  8. Very specific:

    When an override loads a sub-model during set_assembly(), that sub-model's final() won't be called automatically.

    line 190
  9. Very specific:

    Call super().set_assembly(assembly) (order matters: before if you need base data first, after if you're replacing entities)

    line 202
  10. Very specific:

    Always use is for instance comparisons.

    line 203
  11. Very specific:

    Call redo.redo_ifchange(path) after resolving model paths (avoids stale cache.

    line 204
  12. Very specific:

    Find the Python model class: Look for gen/models/ in the component's source tree.

    line 231
  13. Very specific:

    For connection-tracing bugs: Check all == comparisons on component objects -- replace with is.

    line 235
  14. Very specific:

    For type resolution bugs: Verify the model name/path being looked up, check model_types filter, ensure redo_ifchange doesn't create …

    line 237
  15. Distinctive:

    Add temporary debug prints: if needed:

    line 239
  16. Very specific:

    For connector resolution issues: Check gen/models/submodels/connector.py -- connect_to() handles compatibility validation, generic type …

    line 248

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Adamant Framework Internals

Use this skill when debugging code generation misbehavior, fixing bugs in framework Python models, or understanding the assembly loading lifecycle. Not needed for normal component or assembly development.

Key Framework Files

All paths relative to $ADAMANT_DIR (the adamant/ repo root).

Core Model Infrastructure

FileRole
gen/models/base.pyBase class for all model objects. Defines __eq__, __hash__, __new__, caching, full_filename, get_dependencies()
gen/models/assembly.pyAssembly model: loads components, wires connections, calls set_assembly() per instance, generates IDs
gen/models/component.py (component)Component model: reads *.component.yaml, manages submodels via set_component_instance_data, handles per-instance mutation
gen/models/component.py (component_submodel)Base class for all component submodels (packets, events, commands, etc.): load_component(), set_component(), set_assembly(), final()
gen/models/packets.pyGeneric packet suite model (base class for custom packet models)
redo/database/model_cache_database.pySQLite pickle cache: store_model (pickle.dumps), get_model (pickle.loads), session ID tracking via ADAMANT_SESSION_ID
redo/util/model_loader.pytry_load_model_by_name(), load_model(), get_model_file_path() -- path resolution via model_database scanning BUILD_ROOTS

Component-Specific Model Overrides

When a component ships its own gen/models/ directory, those Python files override the generic framework models for that component's YAML files. The build path uniqueness rule applies: the first match wins; project overrides framework.

To find all custom model overrides in the codebase:

find $ADAMANT_DIR/src -name "*.py" -path "*/gen/models/*"

Three override patterns exist:

  1. Submodel override (~15 files): Inherits from packets, commands, data_products, or faults. Overrides set_assembly() to resolve per-instance types from assembly state. May also override set_component() or final().
  2. Custom data structure (~6 files): Defines standalone classes (e.g., routing tables, parameter tables, product packets) without inheriting standard submodels. Loaded by other overrides, not directly by the framework.
  3. Multi-method override: Some overrides need both set_component() (for component-scope data) and set_assembly() (for assembly-scope data). Example: task_watchdog_faults.py.

Python Model Object Identity -- Critical Pitfall

The base model class (gen/models/base.py) defines a custom __eq__ that compares by filename, not by Python object identity:

def __eq__(self, other):
    return self and other and self.full_filename == other.full_filename
```ada

`__hash__` also uses `full_filename`. All instances of the same component type compare as equal under `==` and hash identically.

**Rule:** Always use `is` (Python identity operator) when comparing component model objects that should represent specific assembly instances:

```python
# WRONG -- True for ALL Parameter_Store instances:
conn.to_component == self.component

# CORRECT -- True only for this specific instance:
conn.to_component is self.component

Why is Works: Pickle Memo Preservation

The SQLite cache always returns fresh Python objects via pickle.loads. There is no in-memory object cache -- the session ID check only skips timestamp validation, not deserialization.

Object identity (is) is preserved within a single pickle graph: a component and all its submodels are pickled together. After unpickling, submodel.component is the exact same Python object as the component returned by load_component(), because pickle's memo table preserves circular references.

Connection to_component/from_component references are set by looking up assembly.components[instance_name] -- the same dictionary that holds the component objects. So conn.to_component is assembly.components[name] is always true for resolved connections.

Additional base.py Methods for Debugging

  • get_dependencies() -- returns model dependency list; useful for understanding cache invalidation
  • save_to_cache() -- shows when models get stored
  • __repr__() / __str__() -- shows basename in debug output
  • warning() / warn() -- model-specific error reporting

Connection Model

Connections are defined in assembly.py as connection objects:

class connection(object):
    def __init__(self, filename, data):
        self.connected = False   # Set True after successful connect()
        self.ignored = False     # Set True for "ignore" connections
```ada

After `connect(components)` resolves the connection:
- `from_component` / `to_component` -- Python object references into `assembly.components`
- `from_connector` / `to_connector` -- Connector objects from the component
- `from_index` / `to_index` -- Array index (default 1) for arrayed connectors
- `from_name` / `to_name` -- Formatted `"component.connector[index]"` strings
- `name` -- Combined `"from_name-to_name"`
- `description` -- Optional connection description from YAML

**Connection resolution**: `connect()` extracts component/connector names from YAML data, looks up objects in the `components` dict, calls `from_connector.connect_to()` for bidirectional linking, then sets `connected = True`.

**Subassembly connections**: Already `connected` when merged into parent assembly. Their `to_component` references point to the same Python objects as `assembly.components[name]` because subassembly components are grafted by direct reference (not copied).

## Assembly Load Sequence

1. **`super().load()`** -- deserializes component instances and raw connections from YAML/cache.

2. **Subassemblies loaded recursively** -- with `is_subassembly=True` (suppresses `set_assembly()`). Components and connections grafted into parent by direct reference. Duplicate instance names across subassemblies are validated.

3. **`connection.connect(self.components)`** -- for unconnected, non-ignored connections only. Resolves YAML stubs into live Python object references.

4. **`set_component_instance_data(instance_name, data)`** -- mutates each component in-place: sets `instance_name`, applies parameter overrides, resolves generic types for connectors, calculates queue sizes, handles task instance data.

5. **`component.set_assembly(assembly)`** -- iterates `assembly.components.values()`, calling `set_assembly` which propagates to all submodels. Assembly submodels also get `set_assembly()`. Connections are fully resolved at this point.

6. **Component categorization** -- populates `component_kind_dict` (active/passive/queued/init/commands/etc.), assigns task priorities and ranks, gathers generic type includes.

7. **`_generate_component_ids()`** -- assigns unique IDs to all events, commands, data products, parameters, and faults across all components. Only runs when `not shallow_load`.

8. **`_load_complex_types()`** -- builds dependency-ordered type dictionaries from component `complex_types`.

9. **`final()`** -- post-processing hooks. Called on assembly, then per-component, then per-submodel. Use for logic that needs IDs (prefer `set_assembly()` when IDs aren't needed).

**Key insight:** By step 5, `assembly.connections` is fully populated and object identity is stable. Custom `set_assembly()` implementations can safely trace the connection graph using `is`. IDs are NOT available until step 7.

## Model Loader

`try_load_model_by_name(name, model_types)` resolves a model name to a file path via `model_database` (built by scanning BUILD_ROOTS), then calls `load_model()` which imports the Python class from the file extension and instantiates it.

`get_model_file_path(name, model_types)` returns a single path or None; errors if multiple matches found. Used in custom overrides to resolve dependent model paths. Always call `redo.redo_ifchange(path)` after resolving to declare a build dependency.

## Custom `set_assembly()` Pattern

Two common patterns in custom overrides:

### Pattern 1: Connection Tracing (per-instance resolution)

When a component needs to resolve types from connected peers:

```python
class my_component_packets(packets):
    def submodel_name(self):
        return "packets"

    def set_assembly(self, assembly):
        self.assembly = assembly
        for conn in self.assembly.connections:
            if (
                conn.to_component is self.component   # THIS instance (use 'is')
                and conn.to_connector.name == "Expected_Connector"
            ):
                peer = conn.from_component
                # Use peer to resolve types...
                break
        super(my_component_packets, self).set_assembly(assembly)
```ada

### Pattern 2: Init Parameter Resolution (type lookup)

When a component resolves types from its own init parameters (e.g., `parameters_packets.py`):

```python
class my_component_packets(packets):
    def submodel_name(self):
        return "packets"

    def set_assembly(self, assembly):
        self.assembly = assembly
        for key, pkt in self.entities.items():
            if pkt.name == "Target_Packet":
                # Resolve type from init parameters
                table_ref = self.component.init.get_parameter_value("Table_Config")
                model_path = model_loader.get_model_file_path(table_ref, model_types=[...])
                redo.redo_ifchange(model_path)
                resolved_model = model_loader.load_model(model_path)
                # Replace packet entity with resolved type...
        super(my_component_packets, self).set_assembly(assembly)
```ada

### Pattern 3: Overriding `set_component()` to Suppress Defaults

Some overrides need to prevent the base class from running default logic (e.g., adding ID bases). Override `set_component()` with a variant that skips the unwanted step:

```python
def set_component(self, component):
    # Call variant that skips ID base assignment
    self._set_component_no_id_bases(component)

Pattern 4: Overriding final() for Post-ID Processing

When an override loads a sub-model during set_assembly(), that sub-model's final() won't be called automatically. Override final() to propagate:

def final(self):
    self.resolved_sub_model.final()

final() is called after _generate_component_ids() -- use it for logic that needs entity IDs.

Rules for all custom overrides:

  • Place in src/components/<name>/gen/models/<override>.py
  • Class name must match file name (Python module convention)
  • Call super().set_assembly(assembly) (order matters: before if you need base data first, after if you're replacing entities)
  • Always use is for instance comparisons; never ==
  • Call redo.redo_ifchange(path) after resolving model paths (avoids stale cache; beware circular dependencies)
  • Register new types in BOTH self.type_models AND self.component.complex_types

Debugging Code Generation Bugs

Symptom Taxonomy

SymptomLikely Root Cause
All instances of a type produce same generated output== used instead of is in instance comparison
Wrong component's data for one assembly instanceConnection trace finds first match, not correct match
Wrong type resolved for packet/data productInit parameter resolution logic error; check set_assembly() type lookup
set_assembly receives stale/incorrect assemblySubassembly merge order; check assembly.py grafting
ModelException "could not find X" despite X existingWrong connector name, wrong model_types filter, or circular redo_ifchange
Correct in isolation but wrong in multi-instance case__eq__ filename comparison silently matching wrong instance
Connection resolution fails despite correct YAMLConnector name mismatch, generic type incompatibility, or arrayed index out of range

Investigation Workflow

  1. Identify the generator or model responsible for wrong output. Check build/src/ for the generated file. Trace to the generator:
    from database.generator_database import generator_database
    with generator_database() as db:
        module_name, module_file, class_name, input_file = db.get_generator("path/to/output.ads")
    # module_name: Python module, class_name: generator class, input_file: source YAML
  2. Find the Python model class. Look for gen/models/ in the component's source tree. If absent, the generic framework model applies.
  3. Read set_assembly() in the model class. Two patterns: connection tracing (uses assembly.connections) or init parameter resolution (uses self.component.init).
  4. For connection-tracing bugs: Check all == comparisons on component objects -- replace with is. Verify connector names match component YAML definitions exactly.
  5. For type resolution bugs: Verify the model name/path being looked up, check model_types filter, ensure redo_ifchange doesn't create circular dependencies.
  6. Add temporary debug prints if needed:
    import sys
    print(f"[DEBUG] instance={self.component.instance_name}", file=sys.stderr)
    for conn in self.assembly.connections:
        if conn.to_component is self.component:
            print(f"[DEBUG]   {conn.from_component.instance_name} -> {conn.to_connector.name}", file=sys.stderr)
  7. For connector resolution issues: Check gen/models/submodels/connector.py -- connect_to() handles compatibility validation, generic type resolution (set_type_generic()), and arrayed connector indexing. Connection failures often stem from wrong connector names or incompatible types.
  8. Always redo clear_cache before testing any model fix. Stale pickled objects mask code changes. Cache vs. model debugging: If the problem disappears after redo clear_cache + rebuild, investigate missing redo_ifchange calls in custom overrides or stale dependency declarations in get_dependencies(). If it persists, the bug is in model code.

References

  • references/override-examples.md -- annotated real override implementations (4 patterns + common mistakes)

When NOT to Use This Skill

  • Normal component development: use adamant-component-dev
  • Assembly wiring: use adamant-assembly-dev
  • Build commands and code gen pipeline overview: use adamant-build-system
  • Type definitions: use adamant-type-system

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.