Skip to Content
Jumentix DocsReferenceService Management Collaboration and Packaging

Service Management Collaboration and Packaging

This is the E8 document of the Service Management E1–E8 documentation chain (JUM-494 ) — the last link of the chain and the epic’s terminal documentation gate under Requirement 094: the Linear Project cannot be set to Completed while this Issue is in any state other than completed.

It documents the collaboration and packaging lane exactly as it shipped:

  • Collaboration — the multi-user shared catalog (JUM-491 ): a contract-first Catalogs backend module, optimistic concurrency per catalog record, tombstone deletion with restore, and a designer-side sync client that converges by document read-back.
  • Domain-package versioning (JUM-492 ): exported domain packages are versioned data with a dependency graph and deterministic, explainable conflict resolution on import.
  • Packaging (JUM-493 ): the framework-free designer core is the versioned @jumentix/designer-core package under packages/designer-core/ — manifest, deterministic build, type declarations, DOM-free proof and a consumer smoke test against the built artifact. Publishing is dry-run only (Requirement 070): the publish path is verified, never fired.

Because this is the terminal gate, its content reflects what was delivered, including everything carried over — each descope below is named, not silently dropped. The chain-closure section at the end is the Req 094 evidence: it names the E1–E8 chain completely and records its gate state.

This document is the English reference. A versão em português está em SERVICE-MANAGEMENT-COLLABORATION-PACKAGING.pt-BR.md.

Collaboration: the shared catalog (JUM-491)

Everything before JUM-491 treats the designer as a single-browser tool: Cana persists the designer document in the browser’s IndexedDB, and JUM-485 synchronizes it across that browser’s tabs. JUM-491 turns the designer into a multi-user system: a team shares one catalog of domain designs through the backend — also the only continuous second copy of a user’s work on different hardware (Cana has no fallback; export is manual).

The full mechanism is owned by the dedicated Shared Catalog Sync document (architecture, OAS contract, authorization model, convergence proof, verification commands). This document states what the delivery means for the epic — the concurrency unit, the rejection path, the deletion semantics and the convergence rule — once, and defers to it rather than maintaining a second, drifting explanation.

What shipped

  • A contract-first Catalogs backend module (apps/service-management-api/src/modules/Catalogs/domain/Model/Catalog.ts), hexagonal like the Users reference module: domain aggregate (version bump, tombstone, restore), pure CatalogAuthorizationPolicy, use cases (CatalogUseCases.ts), the optimistic-concurrency enforcement point (CatalogDataRepository.ts), the OAS-validated CatalogController and composition (composeCatalogsServices.ts). Six operations on /catalogs in the canonical spec/1.0.0.yml — list, create, get, update, delete, restore — enforced by bun run oas:check-routes.
  • Optimistic concurrency per catalog record. The record (one shared domain design) is the declared concurrency unit: coarse enough that a relationship spanning two entities always has a consistent version to check, fine enough that teammates never block each other across domains. The server-managed version token starts at 1 on create and bumps on every write; a stale write is rejected with a reviewable 409 whose metadata carries catalogId, expectedVersion, currentVersion and the current record — the loser’s edit is never discarded.
  • Deletion is a tombstone, recoverable. Delete sets deletedAt and bumps the version so the deletion propagates on read-back; a locally-dirty copy raises a deleted-remotely conflict instead of vanishing; POST /catalogs/{id}/restore recovers the record.
  • Server-side authorization, TENANT-RBAC aligned. The role matrix gains catalog scopes (admin: read/create/update/delete; user: read/create/update — team members edit, only admins delete), and the tenant policy binds the catalog to exactly one organization. A client cannot grant itself access — proven by positive/negative integration tests. See Tenant and RBAC Authorization Contract.
  • Mediator events. Every successful write publishes catalogs.catalog.created | updated | deleted | restored with { id, organization, version, actor } — the same version token the API enforces — on the message mediator (CatalogService.ts); publication never breaks the primary write.
  • A DOM-free designer sync client (apps/service-management/src/state/catalogSyncClient.js): a sibling consumer of the same Cana committed-event stream that JUM-485’s tab sync subscribes to. Local commits schedule a debounced push of dirty shared domains (dirtiness decided by the durable marker domain.context.catalog = { id, version, contentHash }, carried additively per JUM-492’s pattern); convergence is a polled document read-back diffed by (id, version) — Cana’s resync rule applied across the network: a gap is a reload signal, never an event replay, because Cana cursors are per client instance and mean nothing across machines. Remote changes cross the same applyRemoteDocument one-path as tab sync, so undo isolation, selection reconciliation and redo truncation are identical.
  • A real convergence proof. catalogSync.integration.test.ts boots the real Express backend (real JWT auth, real mediator) and runs two real designer clients over real fetch; one is partitioned behind a real ECONNREFUSED, both keep editing, and on heal the read-back converges them — the partitioned edit survives as a reviewable conflict, resolved explicitly (take-server / take-local, where take-local is a deliberate new write against the server’s current version, never a blind overwrite).

What JUM-491 deliberately descoped (12-01 carry-over candidates)

Named in Shared Catalog Sync and restated here because the gate must see them:

  • WebSocket fan-out to browsers (push instead of poll): the broker adapters exist in packages/message-mediator, but no fan-out to designer clients is wired; poll-based read-back is correct under any broker choice.
  • Designer share/conflict UI chrome: the client module is DOM-free and composable; the share/unshare/conflict surfaces (and the token-provider UX) are a follow-up.
  • Driver-level native conditional writes: the repository’s read-check-write is the reference behavior; production drivers should later map the same check into IStoreMutationOptions.expectedVersion native conditionals.
  • Offline deletion-intent queue: an offline local deletion of a shared domain cannot be pushed; the surviving server record is re-admitted on read-back — the “committed document wins” boundary, session-scoped by design in this slice.

What this changes for the user — and what it does not

Before JUM-491, the designer was per-browser and export was the only way work left the machine — the E6 document, Service Management Cana Adoption, Migration and Offline Behaviour, owns that data story and this document does not restate it. The shared catalog adds the missing lane: work a user shares now lives on the server and converges across machines. Two honest boundaries remain, stated up front:

  • A sync target is not a backup. It propagates deletions; it does not replace the storage-quota/eviction policy or the offline durability contract.
  • Cana still has no fallback. When the catalog is unreachable the client declares it (degraded on the status surface) and keeps saving locally to Cana — it never silently degrades into a hidden single-user mode.

Domain-package versioning (JUM-492)

A domain package (the <domain>-package.json export) is versioned data, not code, and import is a deterministic policy instead of an unconditional append. The public contract is pinned in Requirement 126, Contract 3; the implementation lives in src/packages/packageVersioning.js (DOM-free), wired through the exporter (buildDomainPackageDocument) and the importer (designerImporters.js).

The versioned package document

Export emits the v2 shape { kind: "domain-package", version: "2.0.0", exportedAt, package: { name, version, dependencies: [{ name, range }] }, domain }. The package block declares identity from the domain’s context: packageName (falling back to the domain name), packageVersion (falling back to 1.0.0) and packageDependencies entries (name@range; a bare name is a presence-only dependency). Compatibility is explicit in both directions: legacy v1 documents keep importing with a synthesized 1.0.0 identity; a document whose major is newer than the importer’s, or a kind other than domain-package, is refused clearly.

Version semantics, redefined for a data model

The usual semver meanings do not map onto a data model, so Requirement 126 redefines them:

  • patch — documentation/metadata only (field descriptions, formats, constraints, domain context text, OAS composition hints);
  • minor — additive structure (a new entity, field or message contract; a required flag loosened);
  • major — removal or narrowing (a removed entity/field/contract, a field type or PK/FK/unique change, a required flag tightened, an RBAC or invariant change, an aggregate declaration change).

Provenance and the installed-package registry

Imported content is stamped: the domain carries context.provenance = { package, version } plus context.packageName/context.packageVersion, and every imported entity carries meta.provenance. The normalizers carry these fields additively — only when the source declares them — so pre-JUM-492 payloads are unchanged and provenance crosses storage, loads and the full-suite export intact. The installed-package registry derives from provenance only: a hand-built domain is never an installation, so importing a package named like a locally-built domain appends (with the JUM-617 id recomputation) instead of merging into unrelated content.

The dependency graph

Dependencies resolve transitively over the registry with the incoming package overlaid. Ranges follow the npm convention: */empty (any), exact 1.2.3, caret ^1.2.3 (same major; for 0.x, same minor), tilde ~1.2.3 (same major.minor); anything else is invalid and satisfies nothing, so it is reported rather than silently accepted. A missing or range-incompatible dependency is reported through the status region and the import proceeds — the designer reports, it is not the resolver. A cycle the incoming package would close is reported by name chain and the import is refused — cycles are detected, never entered.

Semantic conflict resolution

Re-importing an installed package resolves deterministically and explainably:

  • same version + equal content → no-op — idempotent re-import, proven by test;
  • same version + different content → refused (same-version-conflict, divergences listed — version immutability);
  • older version → refused (downgrade-rejected);
  • newer version → merge. Additive and metadata changes (patch/minor semantics — AUTO_MERGE_CLASSES) apply automatically. Removals, narrowings and always RBAC and invariants (REQUIRES_DECISION_CLASSES) keep the existing designer content and are listed in the merge preview, rendered on the schema-diff surface before anything changes; the merge applies only after the user explicitly accepts (a gated window.confirm — one of the destructive-action gates JUM-543 deliberately keeps). RBAC and invariants are always in the decision class: automatically resolving a security policy or a domain invariant is a decision a merge algorithm must not make. All outcomes surface through showStatus, never alert().

Entity matching inside a merge is by name, never by id: colliding ids are recomputed at import on the JUM-617  rule, so ids can never be the match key; new incoming entities receive collision-free ids through the importer’s uniqueId callback.

Proven by: designerPackageVersioning.test.ts (version parsing/ordering, range satisfaction, dependency parsing and transitive resolution, cycle detection, every conflict class), designerRoundTrip.test.ts (versioned export→import deep-equal with provenance, re-export fixed point, idempotent re-import, conflicting re-import refusal, deterministic merge with RBAC kept, compatible/incompatible dependency pairs, JUM-617 preserved on the append path) and designerExporters.test.ts (the v2 package document shape, pinned).

Packaging: the @jumentix designer core (JUM-493)

This lane shipped as a package with a verified dry run — never an automatic publish. This section previously recorded JUM-493 as descoped (the 12-01 decision); the package has since landed, and this document records the landed state. The framework-free designer core is now the versioned packages/designer-core/ package under the xpertminds organization — browser-safe ESM, zero runtime dependencies, MIT licensed, with provenance metadata pointing at its monorepo location.

  • The package is the canonical home, not a copy. The core modules moved from apps/service-management/src/ into packages/designer-core/src/ — the workspace boundary gate forbids a package importing from an app, so the dependency direction was inverted: the zero-build SPA now consumes the package through @jumentix/designer-core/… bare specifiers, resolved by the import map to a vendored tree (apps/service-management/vendor/designer-core/, containment-safe, synced by ci-cd/sync-service-management-designer-core.js — the same vendoring model as the Cana bundle) in the browser, and by the repo’s path mappings (tsconfig paths, Jest moduleNameMapper) straight to the canonical sources in tests. The shipped surface — the domain model and its normalizers, the validation/model-check engine, the exporters (JSON, Markdown, JSON Schema, AsyncAPI, boilerplate bundle, package, OAS), the importers, the schema-diff/merge-preview engine, the hexagonal codegen, and IDesignerStore as a type/contract only — is exactly the package’s src/ tree; the build (scripts/build.js) copies it verbatim into dist/ and generates type declarations from the JSDoc-annotated sources with the repo-pinned TypeScript compiler. Out, enforced by test: every DOM module (script.js, ui/, pwa/), the sync clients (state/designerSync.js, state/catalogSyncClient.js) and the storage adapters — neither LocalStorageDesignerStore nor CanaDesignerStore ships, and the package has no dependency on Cana.
  • The acceptance bar is met by proof, not by construction. Three suites under packages/designer-core/test/ pin the package: packaging.test.ts asserts the manifest (entry points at built output, types-first exports map, files, licence, sideEffects, dry-run-only scripts, in the style of the cana packaging suite), asserts the built file set is exactly the declared closure, and asserts the packed tarball contents via npm pack --dry-run --json; dom-free.test.ts scans the built artifact’s AST for any window, document, localStorage, indexedDB, alert() or FileReader/DOMParser reference and for any import crossing the package boundary; consumer-smoke.test.ts executes the issue’s acceptance test — it imports the built artifact in a separate non-DOM process (no document, no window, no localStorage) and runs a validate → export → re-import round trip on the sample model, deep-equal with a re-export fixed point.
  • The publish policy stands: dry-run only. Per Requirement 070 no automatic publish exists. The repository’s dry-run surface (bun run npm:publish:dry-run:packages, with npm:org:check:xpertminds for the org side) picks the package up like every other non-private workspace package and runs bun publish --dry-run --access public; prepublishOnly forces a clean rebuild first, so the dry run verifies a deterministic artifact whose contents the packaging suite has asserted.
  • Versioning policy. The package follows semver over its public barrel: patch for internal fixes, minor for additive exports, major for removed or narrowed surface. The data contracts it reads and writes (full-suite export, domain-package document) stay versioned in-payload under JUM-492’s policy (Requirement 126, Contract 3) — the package version does not restate them. JUM-492’s domain-package versioning builds on exactly this split.

The practical consequence for the user is unchanged from the E6 document: export is how work leaves the machine — as the full-suite document or as a versioned domain package — and the shared catalog (above) is the only continuous second copy. The package changes who can depend on the core, not how a designer user’s work is stored.

The full-suite export (JUM-547), the portable bundle

The JSON export is the versioned full-suite document ({ kind: "service-management-suite", version: "2.0.0", domains, relationships, interfaces, serviceConfiguration, runtimeEnvironment, codeWorkspace, deployments, view }) carrying all five tabs in a re-importable shape (JUM-547 ). One recorded security decision matters for packaging: the bundle carries the runtime environment selection only ({ environment, fileName }) — never values, because the values mirror real .env contents of the machine the designer runs on. No secret can leave in a bundle; on import the selection is restored and the local machine’s values are preserved. The contract is pinned in Requirement 126, Contract 3; the E5 document, Service Management Operations Console, owns the operations-console side of the runtime-environment story.

Chain closure: the E1–E8 gate evidence (Req 094)

This section is the epic’s documentation-gate evidence: the chain named completely, each link’s state, and the ownership boundaries that keep two documents from drifting over the same behaviour.

The published chain. The chain is named E1–E8; the project published seven dedicated documentation issues — E1 and E3 through E8. No E2 documentation issue exists in the project (an audit of the project’s issue list confirms none was ever created), so the gate closes on the seven published documents:

LinkIssueDocumentState
E1JUM-464 Runtime Environment ContractsDone
E3JUM-473 Service Management Module ArchitectureDone
E4JUM-479 Service Management Contract Parity GuaranteesDone
E5JUM-482 Service Management Operations ConsoleDone
E6JUM-487 Service Management Cana Adoption, Migration and Offline BehaviourDone
E7JUM-490 Service Management Design System and PWA ShellDone
E8JUM-494 this documentthis PR

Every link is published in EN and PT-BR, synchronized per Requirement 076 — neither artifact is a stub.

Consistency: one owner per behaviour, the others link. The chain was audited for duplicated, drifting description; where two documents touch the same behaviour, the ownership is:

  • The data story (where work lives, the one-way migration, offline data behaviour, export as the only recovery path) — owned by E6; E7 states the shell↔data boundary once and links, and this document links for the per-browser baseline the shared catalog extends.
  • The storage port contract and the DOM-free module boundary — owned by E3; this document links for the packaging boundary instead of re-deriving it.
  • The export contract and the domain-package contract — pinned by Requirement 126 (Contract 3), with the parity guarantees owned by E4; this document teaches the JUM-492 behaviour and cites the contract rather than restating it.
  • The env-file/enum semantics and the runtime-env API — owned by E1; E5 owns the operations-console surfaces that consume them, including the status-surface contract every document above references.
  • The shared-catalog mechanism — owned by Shared Catalog Sync (JUM-491’s dedicated document); this document states the delivery’s meaning for the epic and links.

Gate state, exactly. Under Req 094 the Project cannot be set to Completed until this Issue is completed. At this PR: E1, E3–E7 are Done; E8 is delivered by this PR and transitions only after it merges. No pending check is described as passing here: the JUM-491 artifacts this document links (SHARED-CATALOG-SYNC.md, catalogSyncClient.js, the Catalogs module, the convergence test) land with their own PR, and the JUM-493 packaging lane has since shipped as @jumentix/designer-core — dry-run only, per the section above — both stated, not smoothed over. Project-completion evidence per Req 094 (linking this Issue, its PR and commit evidence, and the documentation-integrity validation results) is recorded in the epic’s Project Updates feed per Requirement 102 when the gate closes.

What this document deliberately does not cover

References