# Requirements — fix mutation-path COM lifecycle

Authored 2026-04-29; updated 2026-04-30 after retest with the typelib
fix in place. The original `TYPE_E_LIBNOTREGISTERED` error has been
**resolved** — `objects checkout` now succeeds. A second mutation-path
defect has been uncovered in the same retest. This doc covers both;
the typelib regression is preserved as historical context (✅ shipped)
and the new defect is the active blocker.

## ✅ Already fixed (2026-04-30)

| Command | Mode | Status |
|---|---|---|
| `objects checkout` | session + one-shot | ✅ `CheckOut: True` |
| `objects checkin` | session + one-shot | ✅ `CheckIn: True` |
| `object uda add` (first call) | session + one-shot | ✅ `Add UDA: OK` (but see active defect below) |

The publisher-policy + typed-call fix used for `object snapshot` is now
applied to the mutation surface. Thank you.

## 🔴 Active defect — `object uda add` doesn't `Save` + `CheckIn`

### Symptom

After a successful `object uda add`, the template ends up in a
phantom checked-out state that persists across CLI processes and
across helper-process kills:

```text
$cli session start --node localhost --galaxy ZB
$cli template derive --galaxy ZB --name '$UserDefined' --new-name '$X' --confirm --confirm-target '$UserDefined'
# CreateTemplate: OK

$cli objects checkout --galaxy ZB --type template --name '$X' --confirm --confirm-target '$X'
# CheckOut: True

$cli object uda add --galaxy ZB --type template --name '$X' --uda A --data-type MxFloat --category MxCategoryWriteable_USC --security MxSecurityOperate --confirm --confirm-target '$X'
# Add UDA: OK   ← lies; nothing persists

$cli object uda add --galaxy ZB --type template --name '$X' --uda B --data-type MxFloat --category MxCategoryWriteable_USC --security MxSecurityTune --confirm --confirm-target '$X'
# Error: Add UDA failed: ID=cmdObjectIsCheckedOutToSomeoneElse (302)
```

### Two consequences

1. **The "Add UDA: OK" return is a lie.** Subsequent
   `object snapshot --llm-json` on the same template shows
   `"AttributeValues": []` and `"CheckoutStatus": "notCheckedOut"`. The
   first AddUDA's mutation never persisted in the export-package
   fallback the snapshot reads from.
2. **The phantom checkout survives process death.** Killing all
   `GRAccessApp.exe` helpers (12+ accumulated over an hour of testing)
   does not release the lock. `object snapshot` still says
   `notCheckedOut` while AddUDA still says `cmdObjectIsCheckedOutToSomeoneElse`.
   The two views are inconsistent — likely the GR repo's checkout
   table holds a row that the AddUDA path checks but the snapshot path
   doesn't.

### Root cause hypothesis

`GRAccessCommandDispatcher.cs:507-515` (the `case "add":` arm of
`ExecuteUda`) calls `obj.AddUDA(...)` directly:

```csharp
case "add":
    obj.AddUDA(
        Arg(args, "uda"),
        EnumValue<MxDataType>(Arg(args, "data-type")),
        UdaCategory(Arg(args, "category")),
        EnumValue<MxSecurityClassification>(Arg(args, "security")),
        BoolArg(args, "is-array"),
        IntArg(args, "array-count", 0));
    return CommandSummary(obj, "Add UDA");
```

There is no `obj.Save()` and no implicit checkin. The GRAccess
mutation lifecycle is:

```
CheckOut  →  AddUDA / DeleteUDA / etc.  →  Save  →  CheckIn
```

Skipping `Save` leaves the change in the helper process's COM-memory
only — when that process dies, the change vanishes. Skipping
`CheckIn` keeps the GR repo's checkout row alive past the process
death, which is what blocks every subsequent mutation.

The same defect almost certainly affects:

- `object uda delete`, `object uda rename`, `object uda update`
- `object extension add`, `object extension delete`, `object extension rename`
- `object attribute set`, `object attribute security`, `object attribute lock`, `object attribute buffer`
- `object scripts set`
- Anything else under `ExecuteUda` / `ExecuteAttribute` / `ExecuteExtension` / `ExecuteScript` that mutates without an explicit Save+CheckIn.

### Recommended fix

Two options, in order of preference:

#### Option A — wrap each mutation in atomic CheckOut + Save + CheckIn

The dispatcher takes responsibility for the lifecycle:

```csharp
case "add":
    obj.CheckOut();
    try
    {
        obj.AddUDA(
            Arg(args, "uda"),
            EnumValue<MxDataType>(Arg(args, "data-type")),
            UdaCategory(Arg(args, "category")),
            EnumValue<MxSecurityClassification>(Arg(args, "security")),
            BoolArg(args, "is-array"),
            IntArg(args, "array-count", 0));
        obj.Save();
        obj.CheckIn(string.Empty);
    }
    catch
    {
        try { obj.UndoCheckOut(); } catch { /* best effort */ }
        throw;
    }
    return CommandSummary(obj, "Add UDA");
```

This makes each `object uda add` self-contained — no `objects checkout`
required from the caller. Mirrors the GUI's Edit→Save→Check-in flow.
**Downside:** breaks the "do many edits, then one CheckIn" batch
pattern that the existing `objects checkout` + `objects checkin`
commands suggest is supported.

#### Option B — keep the batch model, add Save only

The dispatcher keeps the explicit-CheckOut-and-CheckIn workflow but
calls `Save()` after each mutation so the change is persisted even
within an open checkout:

```csharp
case "add":
    obj.AddUDA(...);
    obj.Save();
    return CommandSummary(obj, "Add UDA");
```

Caller is still expected to do `objects checkout` once, run all the
mutations, then `objects checkin`. The phantom-checkout symptom goes
away on process exit because the explicit `objects checkin` always
runs the `CheckIn`. **Downside:** the "first AddUDA OK then second
fails" pattern means `obj.Save()` mid-checkout might itself release
the checkout — needs verification against the live GRAccess.

I recommend **Option A** for `object uda add` and the other
single-shot mutation commands, and keeping the explicit
`objects checkout` / `objects checkin` for callers that genuinely want
the batch model. That way `graccess object uda add ...` "just works"
in a script without a checkout dance.

### Acceptance criteria (re-stated)

The script in the original "Acceptance criteria" section below must
run end-to-end without phantom-checkout failures. Repeat-runs (delete
the sandbox, re-derive, re-add UDAs) must produce identical results.
A `object snapshot --llm-json` after the AddUDA must include the new
UDA in `AttributeValues` (or wherever the dispatcher surfaces them in
its snapshot).

## ✅ Default-value fixes (2026-04-30)

The two bad-default complaints from the 2026-04-29 doc are still
worth tracking — please verify these in the same fix:

### `--category` default for `object uda add`

Currently `MxCategoryWriteable_C`, which is **not** in the
`MxAttributeCategory` enum. Valid members:

```
MxCategoryWriteable_U
MxCategoryWriteable_S
MxCategoryWriteable_US
MxCategoryWriteable_UC
MxCategoryWriteable_USC                     ← recommended new default
MxCategoryWriteable_UC_Lockable
MxCategoryWriteable_USC_Lockable
MxCategoryWriteable_C_Lockable
```

### `--force-option` default for `template delete`

Currently `galaxy_DeleteIfNoInstances`, which is not in the
`EForceDeleteTemplateOption` enum. Valid members:

```
dontForceTemplateDelete                     ← recommended new default
cascadeDeleteDontUndeploy
```

## 🚿 Helper process leak (lower priority)

Each CLI invocation spawns a `GRAccessApp.exe` helper that survives
the CLI process. After 12+ invocations during a single rig-setup
session there were 12 stale helpers consuming memory. They don't
appear to cause bugs (the phantom checkout persists even after they
are killed), but they do leak.

The CLI should `Marshal.ReleaseComObject` / `Marshal.FinalReleaseComObject`
the GRAccess COM root on exit, which should let the helper terminate.

## Acceptance script

Repeated from the 2026-04-29 doc — this script must run end-to-end on
this dev box's live `ZB` galaxy after both the typelib + lifecycle
fixes are in:

```powershell
$cli = "C:\Users\dohertj2\Desktop\graccess\graccess_cli\src\ZB.MOM.WW.GRAccess.Cli\bin\x86\Debug\net48\ZB.MOM.WW.GRAccess.Cli.exe"

# 1. Derive sandbox template from $UserDefined
& $cli template derive --node localhost --galaxy ZB `
    --name '$UserDefined' --new-name '$OtOpcUaParityTest' `
    --confirm --confirm-target '$UserDefined'

# 2. Add four UDAs covering the parity matrix's write classifications
#    (Option A: each call is self-contained — no separate checkout/checkin)
& $cli object uda add --node localhost --galaxy ZB `
    --type template --name '$OtOpcUaParityTest' `
    --uda OperateValue --data-type MxFloat `
    --category MxCategoryWriteable_USC --security MxSecurityOperate `
    --confirm --confirm-target '$OtOpcUaParityTest'

& $cli object uda add --node localhost --galaxy ZB `
    --type template --name '$OtOpcUaParityTest' `
    --uda TuneValue --data-type MxFloat `
    --category MxCategoryWriteable_USC --security MxSecurityTune `
    --confirm --confirm-target '$OtOpcUaParityTest'

& $cli object uda add --node localhost --galaxy ZB `
    --type template --name '$OtOpcUaParityTest' `
    --uda ConfigValue --data-type MxFloat `
    --category MxCategoryWriteable_C_Lockable --security MxSecurityConfigure `
    --confirm --confirm-target '$OtOpcUaParityTest'

& $cli object uda add --node localhost --galaxy ZB `
    --type template --name '$OtOpcUaParityTest' `
    --uda Counter --data-type MxInteger `
    --category MxCategoryWriteable_USC --security MxSecurityFreeAccess `
    --confirm --confirm-target '$OtOpcUaParityTest'

# 3. Add an analog-limit alarm extension on Counter and a history extension
#    on OperateValue
& $cli object extension add --node localhost --galaxy ZB `
    --type template --name '$OtOpcUaParityTest' `
    --extension-type AnalogLimitAlarm --primitive Counter `
    --object-extension `
    --confirm --confirm-target '$OtOpcUaParityTest'

& $cli object extension add --node localhost --galaxy ZB `
    --type template --name '$OtOpcUaParityTest' `
    --extension-type HistoryExtension --primitive OperateValue `
    --object-extension `
    --confirm --confirm-target '$OtOpcUaParityTest'

# 4. Verify state — must show all 4 UDAs and both extensions
& $cli object snapshot --node localhost --galaxy ZB `
    --name '$OtOpcUaParityTest' --llm-json

# 5. Instantiate, assign, deploy
& $cli template instantiate --node localhost --galaxy ZB `
    --name '$OtOpcUaParityTest' --new-name 'OtOpcUaParityTest_001' `
    --confirm --confirm-target '$OtOpcUaParityTest'

& $cli instance assign-area --node localhost --galaxy ZB `
    --name 'OtOpcUaParityTest_001' --area 'DEV' `
    --confirm --confirm-target 'OtOpcUaParityTest_001'

& $cli instance assign-engine --node localhost --galaxy ZB `
    --name 'OtOpcUaParityTest_001' --engine 'DevAppEngine' `
    --confirm --confirm-target 'OtOpcUaParityTest_001'

& $cli instance deploy --node localhost --galaxy ZB `
    --name 'OtOpcUaParityTest_001' `
    --confirm --confirm-target 'OtOpcUaParityTest_001'

# 6. Cleanup (verify the delete path too)
& $cli instance delete --node localhost --galaxy ZB `
    --name 'OtOpcUaParityTest_001' `
    --confirm --confirm-target 'OtOpcUaParityTest_001'

& $cli template delete --node localhost --galaxy ZB `
    --name '$OtOpcUaParityTest' `
    --force-option dontForceTemplateDelete `
    --confirm --confirm-target '$OtOpcUaParityTest'
```

Pass criteria:

1. Every command exits with status 0.
2. Step 4's snapshot shows `OperateValue`, `TuneValue`, `ConfigValue`,
   `Counter` in `AttributeValues`, and the two extensions in
   `Extensions` (or wherever the dispatcher surfaces them).
3. Step 5's deploy succeeds and the instance is reachable from MxAccess
   (verifiable via the lmxopcua parity tests, or a manual
   `aaWindowViewer` browse).
4. Repeat the script — second run after cleanup must produce the same
   outputs (idempotency).

## Verification environment

- Windows 10, dev box `DESKTOP-6JL3KKO`
- ArchestrA System Platform installed with live `ZB` galaxy
  (single deployed `$WinPlatform` named `DevPlatform`, engine
  `DevAppEngine`, area `DEV`)
- `OtOpcUaGalaxyHost` Windows service running. Stop it before deploys
  if MxAccess sessions hold the platform; otherwise leave it running.
- graccess-cli x86 build at the path above.

## Consumer of this fix

The lmxopcua repo's parity rig
(`docs/v2/Galaxy.ParityRig.md` in the lmxopcua repo, branch
`v2-mxgw-integration`) wants to provision the sandbox from a script so
the seven scenario classes in `Driver.Galaxy.ParityTests` exercise
real attributes instead of skipping. The 17-test parity matrix is the
gate for retiring the legacy Galaxy.Host backend. Fixing the mutation
lifecycle here unblocks that workflow on every parity rig (dev box +
customer rigs) without requiring GUI clicks.
