@@ -316,76 +316,76 @@ func (b *Backend) GetModuleByName(name string) (*model.Module, error) {
316316 return b .reader .GetModuleByName (name )
317317}
318318
319+ // sessionDMPrefix prefixes the synthetic domain-model ID handed out for a
320+ // session-created module; the suffix is the module name.
321+ const sessionDMPrefix = "mcp~dm~"
322+
323+ // effectiveDomainModel is the SINGLE seam for reading a module's domain model:
324+ // the live view that accounts for writes made this session. Every domain-model
325+ // read funnels through it, so none can forget the dirty routing — the omission
326+ // that made ListDomainModels return stale entities and broke CREATE ASSOCIATION
327+ // for an existing module.
328+ //
329+ // Routing, in order:
330+ // - session-created module (no on-disk domain model): reconstruct from Studio
331+ // Pro with a synthetic DM ID encoding the module name (so
332+ // moduleNameForDomainModel can resolve it back);
333+ // - dirty existing module (written this session): reconstruct from Studio Pro;
334+ // - otherwise: the on-disk reader copy (last-saved state).
335+ func (b * Backend ) effectiveDomainModel (moduleID model.ID ) (* domainmodel.DomainModel , error ) {
336+ for _ , m := range b .sessionModules {
337+ if m .ID == moduleID {
338+ return b .reconstructDomainModelFromPED (m .Name , model .ID (sessionDMPrefix + m .Name ), moduleID )
339+ }
340+ }
341+ localDM , err := b .reader .GetDomainModel (moduleID )
342+ if err != nil {
343+ return nil , err
344+ }
345+ if mod , merr := b .reader .GetModule (moduleID ); merr == nil && b .dirty [mod .Name ] {
346+ if recon , rerr := b .reconstructDomainModelFromPED (mod .Name , localDM .ID , localDM .ContainerID ); rerr == nil {
347+ return recon , nil // best-effort: fall back to the local copy on failure
348+ }
349+ }
350+ return localDM , nil
351+ }
352+
353+ // ListDomainModels returns every module's live domain model (dirty routing via
354+ // effectiveDomainModel), plus session-created modules that have no on-disk copy.
319355func (b * Backend ) ListDomainModels () ([]* domainmodel.DomainModel , error ) {
320356 local , err := b .reader .ListDomainModels ()
321357 if err != nil {
322358 return nil , err
323359 }
324360 out := make ([]* domainmodel.DomainModel , 0 , len (local )+ len (b .sessionModules ))
325- // A module written this session must be reconstructed from Studio Pro's live
326- // model here too, with the SAME dirty routing GetDomainModel applies — otherwise
327- // the stale on-disk snapshot hides entities/associations created earlier in the
328- // same run. This is what made CREATE ASSOCIATION fail when its from/to entities
329- // were just created in an existing module: findEntity lists via this method, and
330- // it previously reconstructed only session-created modules, not dirty existing
331- // ones. Best-effort: keep the local copy if reconstruction fails.
332361 for _ , dm := range local {
333- if mod , merr := b .reader .GetModule (dm .ContainerID ); merr == nil && b .dirty [mod .Name ] {
334- if recon , rerr := b .reconstructDomainModel (mod .Name , dm .ContainerID ); rerr == nil {
335- out = append (out , recon )
336- continue
337- }
362+ eff , eerr := b .effectiveDomainModel (dm .ContainerID )
363+ if eerr != nil {
364+ eff = dm // best-effort: keep the on-disk copy if the live view fails
338365 }
339- out = append (out , dm )
366+ out = append (out , eff )
340367 }
341- // Session-created modules have no on-disk domain model; reconstruct each so
342- // references into a freshly created module resolve in the same run.
343368 for _ , m := range b .sessionModules {
344- if dm , derr := b .reconstructDomainModelFromPED ( m . Name , model . ID ( sessionDMPrefix + m . Name ), m .ID ); derr == nil {
369+ if dm , derr := b .effectiveDomainModel ( m .ID ); derr == nil {
345370 out = append (out , dm )
346371 }
347372 }
348373 return out , nil
349374}
350375
351- // GetDomainModel returns a module's domain model. If the module was written
352- // this session (dirty), it is reconstructed from Studio Pro's live in-memory
353- // model so in-session edits are visible; otherwise it comes from the local
354- // reader (last-saved state).
376+ // GetDomainModel returns a module's live domain model (see effectiveDomainModel).
355377func (b * Backend ) GetDomainModel (moduleID model.ID ) (* domainmodel.DomainModel , error ) {
356- // A module created over MCP this session has no on-disk domain model for the
357- // reader to read (the reader is a pre-create snapshot). Reconstruct its live
358- // entities from Studio Pro with a synthetic domain-model ID (encoding the
359- // module name, so moduleNameForDomainModel can resolve it back) — so both
360- // "create entity X.Foo" and references to X's entities (e.g. a microflow
361- // parameter or workflow context typed X.Foo) resolve within the same run.
362- for _ , m := range b .sessionModules {
363- if m .ID == moduleID {
364- return b .reconstructDomainModelFromPED (m .Name , model .ID (sessionDMPrefix + m .Name ), moduleID )
365- }
366- }
367- mod , err := b .reader .GetModule (moduleID )
368- if err == nil && b .dirty [mod .Name ] {
369- return b .reconstructDomainModel (mod .Name , moduleID )
370- }
371- return b .reader .GetDomainModel (moduleID )
378+ return b .effectiveDomainModel (moduleID )
372379}
373380
374- // sessionDMPrefix prefixes the synthetic domain-model ID handed out for a
375- // session-created module; the suffix is the module name.
376- const sessionDMPrefix = "mcp~dm~"
377-
378381// GetDomainModelByID mirrors GetDomainModel but is keyed by the domain model's
379382// own ID; it resolves the owning module and applies the same dirty routing.
380383func (b * Backend ) GetDomainModelByID (id model.ID ) (* domainmodel.DomainModel , error ) {
381384 localDM , err := b .reader .GetDomainModelByID (id )
382385 if err != nil {
383386 return nil , err
384387 }
385- if mod , err := b .reader .GetModule (localDM .ContainerID ); err == nil && b .dirty [mod .Name ] {
386- return b .reconstructDomainModel (mod .Name , localDM .ContainerID )
387- }
388- return localDM , nil
388+ return b .effectiveDomainModel (localDM .ContainerID )
389389}
390390
391391// ReconcileMemberAccesses is a no-op for the MCP backend. It is the executor's
0 commit comments