Skip to content
Open
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
164 changes: 164 additions & 0 deletions .github/skills/adding-a-new-class/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
name: adding-a-new-class
description: Add an entirely new class to MasterLCModel.xml, including module placement, class numbering, owner property, version bump, data migration, code regeneration, and tests. Use when the user asks to add a new class, entity, or object type to the LCM data model.
---

# Adding a New Class to the Model

This guide covers adding an entirely new class to `MasterLCModel.xml`. This is less common than adding properties and more involved -- it touches the model, requires a migration, may need an owner property on an existing class, and may need hand-written partial class logic.

## Prerequisites

- Read the Critical Rules section in AGENTS.md
- Know the parent (base) class (typically `CmObject` for simple classes)
- Know whether the class requires an owner and which class will own it
- Know the properties the new class needs

## Steps

### 1. Determine Class Placement

Classes live inside `<CellarModule>` elements in `MasterLCModel.xml`. The main modules are:

| Module | id | num | Contains |
|--------|----|-----|----------|
| Cellar | CellarModule | 0 | Core classes (CmObject, CmPossibility, StText, etc.) |
| Scripture | Scripture | 3 | Scripture classes |
| LangProj | LangProj | 6 | LangProject and related |
| Ling | Ling | 5 | Linguistic classes (LexEntry, LexSense, Morph*, Wfi*, etc.) |
| Notebook | Notebook | 24 | Notebook classes |

Choose the module that best fits your class. Most new classes go in `Ling` (module 5).

### 2. Determine the Class Number

Within the module, find the highest existing `num` attribute on `<class>` elements and use the next integer.

The class ID (used in code as `kClassId`) is formed by combining the module number and the class number. For example, in module `5` (Ling), class number `134` would have class ID `5134`.

### 3. Add the Class to MasterLCModel.xml

File: `src/SIL.LCModel/MasterLCModel.xml`

```xml
<class num="134" id="NewClassName" abstract="false" abbr="ncn" base="CmObject" depth="0">
<comment>
<para>Description of the new class. No newlines inside para elements.</para>
</comment>
<props>
<basic num="1" id="Name" sig="MultiUnicode"/>
<basic num="2" id="Description" sig="MultiString"/>
<!-- Add more properties as needed -->
</props>
</class>
```

Key attributes:
- `abstract`: Set to `true` if only subclasses should be instantiated
- `base`: Parent class. Use `CmObject` unless inheriting from something more specific
- `depth`: Depth in the inheritance tree from `CmObject` (0 = direct child of CmObject)
- `abbr`: Short abbreviation for the class
- `owner`: `required` (default), `optional`, or `none`. Use `none` for unowned classes like `LexEntry`

### 4. Add an Owning Property to the Owner Class

Unless `owner="none"`, you need a property on the owning class that references the new class. Find the owner class in the XML and add an owning property:

```xml
<owning num="NEXT_NUM" id="NewClassInstances" card="seq" sig="NewClassName">
<comment>
<para>Owns instances of NewClassName.</para>
</comment>
</owning>
```

### 5. Increment the Model Version

Update the `version` attribute on `<EntireModel>` and add a change history entry.

### 6. Write the Data Migration

Create: `src/SIL.LCModel/DomainServices/DataMigration/DataMigration7000073.cs`

For a new class that doesn't exist in any data yet, a minimal migration suffices:

```csharp
using System.Xml.Linq;

namespace SIL.LCModel.DomainServices.DataMigration
{
internal class DataMigration7000073 : IDataMigration
{
public void PerformMigration(IDomainObjectDTORepository repoDto)
{
DataMigrationServices.CheckVersionNumber(repoDto, 7000072);
// New class added to model. No existing data to migrate.
DataMigrationServices.IncrementVersionNumber(repoDto);
}
}
}
```

If the new class needs default instances created (e.g., a new possibility list), create them in the migration using `DataMigrationServices.CreatePossibilityList()` or raw XML construction. See `DataMigration7000069.cs` for examples of creating new lists and objects.

### 7. Register the Migration

File: `src/SIL.LCModel/DomainServices/DataMigration/LcmDataMigrationManager.cs`

```csharp
m_individualMigrations.Add(7000073, new DataMigration7000073());
```

### 8. Rebuild

```
dotnet build --configuration Release
```

The code generator will produce:
- A `NewClassNameTags` constants class (class ID, field IDs)
- An `INewClassName` interface
- An `INewClassNameFactory` factory interface and implementation
- An `INewClassNameRepository` repository interface and implementation
- A concrete `NewClassName` class in `DomainImpl/GeneratedClasses.cs`
- StructureMap registrations in `GeneratedServiceLocatorBootstrapper.cs`

### 9. Add Hand-Written Extensions (if needed)

If the class needs business logic, create a partial class in `src/SIL.LCModel/DomainImpl/`:

```csharp
namespace SIL.LCModel.DomainImpl
{
internal partial class NewClassName
{
// Virtual properties, convenience methods, overrides, etc.
}
}
```

Place it in the appropriate `Overrides*.cs` file or create a new one if it doesn't fit existing files.

### 10. Update BootstrapNewLanguageProject (if needed)

If the new class needs default instances in every new project, update `src/SIL.LCModel/DomainServices/BootstrapNewLanguageProject.cs` to create them.

### 11. Write Tests

Create migration tests (use the `writing-a-data-migration` skill) and API tests using `MemoryOnlyBackendProviderTestBase` (use the `writing-tests` skill).

## Checklist

- [ ] Class added to correct `<CellarModule>` in `MasterLCModel.xml`
- [ ] Unique `num` within the module
- [ ] `base` class set correctly
- [ ] `owner` attribute set (or left as default `required`)
- [ ] Owning property added to the owner class (unless `owner="none"`)
- [ ] `version` attribute incremented on `<EntireModel>`
- [ ] Change history entry added
- [ ] Migration class created and registered in `LcmDataMigrationManager`
- [ ] Build succeeds and code regenerates correctly
- [ ] Hand-written partial class added if business logic needed
- [ ] `BootstrapNewLanguageProject` updated if default instances needed
- [ ] Tests written
- [ ] Generated files NOT manually edited
194 changes: 194 additions & 0 deletions .github/skills/adding-a-property/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
---
name: adding-a-property
description: Add a new persisted property to an existing class in MasterLCModel.xml, including model version bump, data migration, code regeneration, and tests. Use when the user asks to add a field, property, or attribute to an LCM model class.
---

# Adding a Property to an Existing Class

This guide covers adding a new persisted property to an existing class in the LCM model. This is one of the most common and most dangerous changes -- it touches the XML model, requires a data migration, and triggers code regeneration.

If you need a computed/derived property that is NOT persisted, use the `adding-a-virtual-property` skill instead.

## Prerequisites

- Read the Critical Rules section in AGENTS.md
- Know the target class name (e.g., `LexSense`)
- Know the property type (`basic`, `owning`, or `rel`) and signature

## Steps

### 1. Edit MasterLCModel.xml

File: `src/SIL.LCModel/MasterLCModel.xml`

Find the target class and add the property inside its `<props>` element. Choose the next available `num` for that class (check existing properties).

**Basic property example** (adding a `MultiString` field):
```xml
<basic num="37" id="NewFieldName" sig="MultiString">
<comment>
<para>Description of the field. No newlines inside para elements.</para>
</comment>
</basic>
```

**Owning property example** (adding an owning sequence):
```xml
<owning num="37" id="OwnedThings" card="seq" sig="TargetClassName">
<comment>
<para>Description of owned objects.</para>
</comment>
</owning>
```

**Reference property example** (adding a reference collection):
```xml
<rel num="37" id="RelatedItems" card="col" sig="CmPossibility">
<comment>
<para>Description of referenced objects.</para>
</comment>
</rel>
```

Property type signatures for `<basic>`:
- `Integer`, `Boolean`, `String`, `Unicode`, `MultiString`, `MultiUnicode`
- `Time`, `GenDate`, `Binary`, `Guid`, `TextPropBinary`

Cardinality values for `<owning>` and `<rel>`:
- `atomic` -- zero or one target
- `seq` -- ordered list
- `col` -- unordered collection

### 2. Increment the Model Version

In the same file (`MasterLCModel.xml`), update the `version` attribute on the root `<EntireModel>` element:

```xml
<EntireModel version="7000073">
```

Add a change history entry just below the existing ones:

```xml
DD Month YYYY (7000073): Added NewFieldName to ClassName. Brief description.
```

### 3. Write the Data Migration

Create: `src/SIL.LCModel/DomainServices/DataMigration/DataMigration7000073.cs`

**For new optional properties with safe defaults (most common case)**, existing data doesn't need modification. But you still need the migration class:

```csharp
// Copyright (c) YYYY SIL International
// This software is licensed under the LGPL, version 2.1 or later
// (http://www.gnu.org/licenses/lgpl-2.1.html)

namespace SIL.LCModel.DomainServices.DataMigration
{
internal class DataMigration7000073 : IDataMigration
{
public void PerformMigration(IDomainObjectDTORepository repoDto)
{
DataMigrationServices.CheckVersionNumber(repoDto, 7000072);
// New optional property with safe default; no data changes needed.
DataMigrationServices.IncrementVersionNumber(repoDto);
}
}
}
```

**IMPORTANT**: If you are adding a C# value type property (int, bool, GenDate, DateTime), you MUST add an explicit XML element with the default value to every existing instance. See the WARNING at the top of `MasterLCModel.xml`. Example migration that adds a default value:

```csharp
public void PerformMigration(IDomainObjectDTORepository repoDto)
{
DataMigrationServices.CheckVersionNumber(repoDto, 7000072);

foreach (var dto in repoDto.AllInstancesWithSubclasses("TargetClass"))
{
var element = XElement.Parse(dto.Xml);
if (element.Element("NewBoolField") == null)
{
element.Add(new XElement("NewBoolField", new XAttribute("val", "False")));
DataMigrationServices.UpdateDTO(repoDto, dto, element.ToString());
}
}

DataMigrationServices.IncrementVersionNumber(repoDto);
}
```

### 4. Register the Migration

File: `src/SIL.LCModel/DomainServices/DataMigration/LcmDataMigrationManager.cs`

Add a line in the constructor, after the last existing entry:

```csharp
m_individualMigrations.Add(7000073, new DataMigration7000073());
```

If no data changes are needed, you can use `m_bumpNumberOnlyMigration` instead:
```csharp
m_individualMigrations.Add(7000073, m_bumpNumberOnlyMigration);
```
In this case you do NOT need to create a `DataMigration7000073.cs` file.

### 5. Rebuild to Regenerate Code

```
dotnet build --configuration Release
```

This triggers `GenerateModel` which regenerates all 9 `Generated*.cs` files from the updated `MasterLCModel.xml`. The new property will appear in the generated constants, interfaces, class implementations, etc.

### 6. Add Hand-Written Logic (if needed)

If the property needs custom logic beyond what the generator provides (computed side effects, validation, etc.), add it to the appropriate `Overrides*.cs` partial class in `src/SIL.LCModel/DomainImpl/`.

### 7. Write Tests

**Migration test**: Create `tests/SIL.LCModel.Tests/DomainServices/DataMigration/DataMigration7000073Tests.cs`

```csharp
using System.Xml.Linq;
using NUnit.Framework;

namespace SIL.LCModel.DomainServices.DataMigration
{
[TestFixture]
public class DataMigration7000073Tests : DataMigrationTestsBase
{
[Test]
public void DataMigration7000073Test()
{
// Parse test data XML (create a matching .xml file in the test data directory)
var dtos = DataMigrationTestServices.ParseProjectFile("DataMigration7000073.xml");
var mockMdc = new MockMDCForDataMigration();
IDomainObjectDTORepository dtoRepos = new DomainObjectDtoRepository(
7000072, dtos, mockMdc, null, TestDirectoryFinder.LcmDirectories);

m_dataMigrationManager.PerformMigration(dtoRepos, 7000073, new DummyProgressDlg());

// Assert the migration results
Assert.AreEqual(7000073, dtoRepos.CurrentModelVersion);
// Add specific assertions for your migration...
}
}
}
```

**API test**: For testing property access via the LCM API, inherit from `MemoryOnlyBackendProviderTestBase`. See the `writing-tests` skill.

## Checklist

- [ ] Property added to `MasterLCModel.xml` with correct `num`, `id`, `sig`, and (if relational) `card`
- [ ] `version` attribute incremented on `<EntireModel>`
- [ ] Change history comment added
- [ ] Migration class created OR `m_bumpNumberOnlyMigration` used
- [ ] Migration registered in `LcmDataMigrationManager` constructor
- [ ] If adding a C# value type: migration writes explicit defaults to all existing instances
- [ ] Build succeeds (`dotnet build --configuration Release`)
- [ ] Migration test written
- [ ] Generated files NOT manually edited
Loading
Loading