Service Management Contract Parity Guarantees
This is the E4 document of the Service Management E1–E8 documentation chain (JUM-479 ). It documents what the designer’s contract exports guarantee — as the code behaves today, after the contract-parity lane (JUM-474 , JUM-475 , JUM-476 , JUM-477 , JUM-478 ) and its verification machinery (JUM-470 , JUM-471 ) landed.
The guarantee is a promise to the boilerplate, not a feature list: an artifact exported from the designer can be consumed by the boilerplate without hand-editing. Each guarantee below names the check that proves it, so a reader can verify rather than trust, and a maintainer who breaks one fails a suite — not a review opinion.
The export surface itself (eight exporters, their formats and the export quality gate) is pinned by Requirement 126, Contract 3 — this document links to it rather than duplicating it. Usage-level walkthroughs live in Domain Designer Features and Usage (sections 10.1–10.2).
Guarantee 1 — the OAS 3.1 export is boilerplate-consumable (JUM-474)
Builder: buildOasDocument in
packages/designer-core/src/exporters/designerExporters.js.
The document declares openapi: '3.1.0' — 3.1, not 3.0 — because that is
the version the boilerplate consumes. The canonical
spec/1.0.0.yml declares 3.1.0, and the entity
model is legislated OpenAPI 3.1 end to end by
Requirement 026
(3.1 is the JSON Schema 2020-12-aligned line; the designer’s JSON Schema
export targets the same draft). An export that matched the canonical
version’s syntax but not its version would be a contract the consumer has to
translate — so the export emits exactly the dialect the boilerplate’s gates
already validate.
The exported document is guaranteed to satisfy
Requirement 036,
the port-object discipline enforced on the canonical spec by
ci-cd/check-oas-route-resolution.js:
- Canonical verb
operationIds. Every operation carries anoperationIdon the canonical verb scheme (getAll*/create*/get*ById/update*/delete*), qualified by the schema name so ids stay unique across domains (getAllBilling_Invoice). - The
$refdiscipline. Request bodies referenceRequestCreate<Schema>/RequestUpdate<Schema>port input objects via$ref; every 2xx response references the entity schema, its<Schema>ArrayOfwrapper, or the sharedResourceDeleteResponse— never an inline schema. Every referenced schema carries a non-emptydescription. - Canonical error codes. Error responses use the ERROR-CONTRACTS-AND-RESPONSES status set (400/401/403/404/409) with its canonical descriptions.
- Port wrappers are marked, not hidden. Derived port input/output
wrappers carry
'x-port-object': true, which is what lets the OAS importer skip them (see Guarantee 4).
Proven by:
designerOasCompliance.test.ts,
which imports validatePortObjectContracts and resolveSchemaByRef from
the real checker (not a copy) and applies them to a document exported from
a UI-style model — the export cannot drift from the gate without failing the
suite. The name-collision half of the guarantee (two names that tokenize to
the same schema/route, e.g. Foo Bar vs Foo-Bar, are export-gate-blocking
errors rather than silent overwrites) lives in
modelValidation.js
and is pinned by
modelValidation.test.ts.
Guarantee 2 — AsyncAPI 3.0 per transport and a canonical proto (JUM-475)
Builders:
packages/designer-core/src/exporters/asyncApiExporters.js.
- One file per transport, canonical naming. The export emits
<version>.websocket.ymland<version>.grpc.yml, matching thespec/asyncapi/directory one-for-one — never a single combined document, never the 2.x publish/subscribe shape. Under 3.0, channels holdmessagesand the top-leveloperationsmap carriesaction: send|receive(responsecontracts are received; every other type is sent) plus channel/message$refs. - The same
$refpayload discipline as OAS. Message payloads live once undercomponents.schemasand messages reference them — identical payloads share one schema entry instead of being inlined per message. - Every exported document validates against
validateAsyncApi30Document(asyncApi30Validation.js), the in-repo structural validator for the 3.0 shape (the repository does not depend on@asyncapi/parser). The canonicalspec/asyncapi/files pass the same rules — drop-in shape parity between what the designer emits and what the boilerplate ships. - The gRPC proto export reproduces the canonical envelope. proto3,
package
realtime, serviceAsyncApiGateway, envelope messagesAsyncApiRequest/AsyncApiResponse, one rpc/message pair per designer message contract. For a contract-less model the emitted proto is byte-identical to the checked-inspec/asyncapi/async-api.proto.
Proven by:
designerAsyncApiExport.test.ts
— including the byte-identity assertion and the test that runs the validator
over the canonical files themselves, so the canonical documents and the
export drift together or fail together.
Guarantee 3 — the codegen bundle is deliverable code (JUM-476)
Builder:
packages/designer-core/src/codegen/hexagonalCodegen.js,
consumed by both the boilerplate-bundle exporter
(buildBoilerplateBundleDocument, artifact kind: 'boilerplate-bundle',
version: '2.0.0') and the designer’s Code Preview pane — same builder, so
preview and bundle cannot drift apart.
- The layout is hexagonal and mirrors the migrated Users module
(
src/modules/<Domain>/withdomain/{Entity,Model,security},application/{ports,use-cases},adapters/in/http/controllers,adapters/out/persistence,composition/, andevents/contracts/only when message contracts exist — see HEXAGONAL-FEATURE-DRIVEN-MIGRATION). - Contract shapes are consumed, never re-derived. Field types come from
the Guarantee-1 OAS component schemas, HTTP routes from its paths and
operationIds, event channels from the Guarantee-2 AsyncAPI channels. A contract change regenerates the code; the code never forks the contract. - The output passes the repository’s own architecture checks. Every
generated controller passes
validateControllerFilefromci-cd/check-hexagonal-boundaries.js, and the generated import graph only points inward (domain ← application ← adapters ← composition). - The output compiles. The emitted file set compiles under
tsc --strict— the suite writes the bundle to a temp directory and runs the real compiler over it.
What the developer still writes. The bundle is a runnable skeleton, not a
service: the persistence adapter is an in-memory Map mirror of
UserDataRepository.ts that you swap for the real store client, the
composition root must be wired into the service bootstrap, and any business
rule beyond the canonical CRUD verbs is yours. The guarantee covers the
boundary — layout, contracts, compilation, architecture checks — not the
application logic.
Proven by:
hexagonalCodegen.test.ts.
Guarantee 4 — round-trip fidelity, with the honest boundaries (JUM-471, JUM-478)
Suite:
designerRoundTrip.test.ts.
The property under test is the crossing itself — export → import → compare —
so an exporter-drops-field + importer-ignores-field cancellation cannot hide
behind fixed expected outputs.
Symmetric crossings (lossless, deep-equal asserted)
- JSON (
buildJsonExportDocument→buildStateFromSuiteExportovernormalizeStatePayload): the versioned full-suite document (JUM-547/JUM-736) —domains,relationships,view,interfaces,serviceConfiguration,codeWorkspaceanddeploymentsround-trip deep-equal, and the export is idempotent. The boundary is documented and asserted: selections andidCounterare not part of the document and are recomputed on import, andruntimeEnvironmentcrosses as the environment selection only (see the JUM-547 section below). Pre-JUM-547 domain-only documents ({ domains, relationships, view }, nokind/version) import cleanly with the missing sections defaulted; a document with an unknown top-level section, a newer majorversion, or akindother thanservice-management-suitefails clearly instead of half-importing. - Domain package (
buildDomainPackageDocument→buildDomainFromPackage): a package round-trips deep-equal into an empty model, stamped with provenance (JUM-492): the v2 document carries apackageblock ({ name, version, dependencies }), and imported content recordscontext.provenance/meta.provenance({ package, version }). Re-imports are version-aware: the same version with equal content is a no-op, the same version with different content and downgrades are refused, and a newer version merges deterministically — additive/metadata changes apply, and removals, narrowings, RBAC and invariants keep the existing content and are listed in the merge preview for a user decision (Requirement 126 Contract 3). Dependency ranges are resolved against the installed-package registry; missing or incompatible dependencies are reported, and a cycle the incoming package would close is refused. The JUM-617 id recomputation still guards the append path (a different package with colliding ids).
The OAS crossing: fixed point, empty loss list
OAS is narrower than the internal model, so the OAS crossing was the lossy one. JUM-478 drove the model-level loss list from 21 diff paths to zero, and the suite asserts the exact empty list — a field silently joining (or rejoining) it fails the suite:
- Export → import → export reaches a fixed point; the first-crossing
document diff is exactly
[](asserted withtoStrictEqual, not eyeballed). - What OAS cannot express natively crosses as agreed
x-extensions the importer normalizes back intoentity.meta:x-aggregate-root,x-invariants,x-rbac(emitted only when the policy diverges from the designer default — an absentx-rbacnormalizes back to exactly that default),x-fieldless: true(an empty field set survives instead of gaining the importer’s defaultid/createdAt/updatedAt), and per-fieldx-field-flags: { pk, fk, unique }(emitted only when the flags diverge from the importer’s name heuristic:id→ PK/unique,*Id→ FK). Message contracts and composition (oneOf/allOf/anyOf,discriminator,x-external-refs) cross the same way. - Relationships cross as top-level
x-relationsrows keyed by schema name ({ name, fromSchema, toSchema, fromCardinality, toCardinality }) — never by model id, because the importer recomputes ids and ids in the document would break the fixed point. Rows whose endpoints did not import are dropped, the same rulenormalizeStatePayloadapplies to dangling model ids. - The port-object wrappers are skipped by design — and that is not a
loss.
RequestCreate*/RequestUpdate*/*ArrayOf/ResourceDeleteResponseare derived artifacts, not model state; re-importing them would fabricate phantom entities. Designer-exported documents mark them with'x-port-object': true.
The boundaries that remain (named, asserted, not swept away)
For designer-exported documents the model-level crossing still does not carry, by design:
- Domain bounded-context blocks (
domain.context) and canvas layout (positions, colors) — they have no extension carriage; import recomputes them. Domain and entity names survive. - The typed-format normalization: typed fields (
uuid/date/datetime) come back carrying the canonical format the exporter derived from their type. The suite asserts this as the transformationapplyDocumentedOasFormatNormalization, so it is part of the contract, not an accident. - View state (zoom, filters, the export-gate toggle) is JSON-export content, not OAS content — the OAS importer never restored it.
For foreign documents — OAS files the designer did not emit, such as the
canonical spec/1.0.0.yml — the importer recognizes
port objects by convention (the same naming/description rules: Request*,
*ArrayOf, ResourceDeleteResponse, “Port input/output object” descriptions
that are not <Name> resource entity contracts, and non-object schemas), and
the named remaining losses are:
- the
example/default/minItems/maxItemsfacets — the designer field model has no slot for them; - array item
$reflinkages — value-object references flatten to theitemsTypevocabulary; - legacy (non-canonical)
operationIds — import keeps no operationIds, so re-export regenerates them on the canonical verb scheme.
The canonical import itself is pinned: spec/1.0.0.yml (OpenAPI 3.1.0, 33
operationIds) imports as one Imported domain with exactly the six contract
schemas (Document, Email, Address, Phone, User, Organization) —
no phantom port-object entities, no relationships — with full meta
normalization, and re-exports to a fixed point whose entity schemas keep the
source’s properties and required sets and whose paths carry the five
canonical CRUD operations per schema.
One-way exporters
Markdown, JSON Schema, AsyncAPI and the boilerplate bundle have no importer.
Their suites assert structural invariants instead of a crossing: every
domain/entity/field/relationship renders; one JSON Schema definition per
entity with required ⊆ properties and additionalProperties: false; one
AsyncAPI 3.0 operation per contract per transport with shared payload refs;
one bundle module per domain with the hexagonal file set.
Where the gates enforce all of this
- The export quality gate (Requirement 126, Contract 3): with
view.exportBlockCriticaltrue (the default), every exporter refuses to run whilemodelValidation.jsreports anyerror-severity issue — which includes the OAS name-collision rule and unenforceable RBAC roles (see below). The gate’s DOM half iscanExportModelinapps/service-management/script.js. - The unit suites are the enforcement. All suites named above run under
Bun through the mapped runner
(
ci-cd/run-unit-tests.js, which refuses a run that discovers zero tests) insidebun run test:unit— a cell of every branch quality gate (ci-cd/run-branch-quality-gate.jsselects it for direct pushes todev, and the full matrix for pull requests intodevandmain). - The canonical-spec gates stay green on the boilerplate side:
bun run oas:check-routes(Req 036 onspec/) andbun run arch:check-boundariesare cells ofci:gate, so a canonical contract the designer targets cannot silently stop being what the export was shaped against.
RBAC alignment and the divergence it reconciled (JUM-477)
The per-entity RBAC editor is aligned to the
Tenant and RBAC Authorization Contract
through
src/model/rbacContract.js,
a designer-side mirror of the Users domain implementation (Rbac.ts,
TenantAuthorizationPolicy.ts). The reconciliation found a real divergence,
recorded here rather than quietly fixed:
- The designer persisted
tenantScopedas a free per-rule flag; the runtime has no such knob. Tenant scoping in the runtime is derived from the role set (shouldRequireOrganization: normalizedadmin/userroles constrain the principal to its organization;superadminand legacy direct scopes keep a global boundary). A storedtenantScopedvalue that contradicted the roles was a configuration the boilerplate would silently not honour. The editor now derivestenantScopedfrom the selected roles (the checkbox is read-only and previews the derived value), and stored policies are repaired to the derived value on load — a compatible extension under Requirement 126 Contract 2, with the stored shape unchanged. - The principal vocabulary is closed. Only the normalized tenant roles
(
superadmin,admin,user) and the legacy direct scopes are enforceable; anything else is rejected at edit time and reported as anerrorby model validation, so the export gate blocks it instead of exporting a policy the runtime would drop.
Proven by:
rbacContract.test.ts,
which pins the mirror against Rbac.ts itself — if the domain vocabulary
drifts, the suite fails. This is also why x-rbac round-trips losslessly
(Guarantee 4): the exported policy is the normalized, enforceable one, and
the importer rebuilds it against the same contract.
Full-suite export and the runtimeEnvironment decision (JUM-547, landed)
Export and import now carry all five tabs, not just the domain model. The
JSON export (domain-designer.json) is the versioned full-suite document:
{ kind: "service-management-suite", version: "2.0.0", domains, relationships, interfaces, serviceConfiguration, runtimeEnvironment, codeWorkspace, deployments, view } —
the same sections the pinned service-management.v1 document persists in
Cana (Requirement 126, Contract 2), minus the session selections and
idCounter. A model designed across all five tabs exports and re-imports
with every tab intact; a bundle exported before this change (the domain-only
shape, no kind/version) imports cleanly with the missing sections
defaulted, and a bundle with an unknown section or a newer major version
fails clearly rather than half-succeeding.
The recorded decision is runtimeEnvironment’s treatment
(JUM-547 ,
Requirement 126 Contract 3): it mirrors real .env contents, so an export
bundle containing the values would be a file that can carry configuration
off the machine. Of the three candidate positions — export the selection
only; export values restricted to the editable tier; omit the section
entirely — the landed stance is the first: the bundle carries the
environment selection (environment, fileName) but never values, and
import restores the selection while preserving the local machine’s values.
The runtime environment is a property of where the designer is running; the
selection is design metadata worth sharing. Since no values cross, no
secret can leave in a bundle — the guarantee the third position was
preferred for, kept without losing the selection. The suite proves it by
asserting the wire document contains no value string.
Proven by:
designerRoundTrip.test.ts
(full-suite deep-equal, values-never-cross, backward/forward compatibility)
and
designerExporters.test.ts
(document shape).
References
- OAS exporter/importer:
packages/designer-core/src/exporters/designerExporters.js,designerImporters.js - AsyncAPI/proto exporters:
packages/designer-core/src/exporters/asyncApiExporters.js; validator:asyncApi30Validation.js - Codegen:
packages/designer-core/src/codegen/hexagonalCodegen.js - Model validation / export gate:
modelValidation.js,apps/service-management/script.js - RBAC mirror:
src/model/rbacContract.js; contract: Tenant and RBAC Authorization Contract - Suites:
designerRoundTrip.test.ts,designerPackageVersioning.test.ts,designerOasCompliance.test.ts,designerAsyncApiExport.test.ts,hexagonalCodegen.test.ts,rbacContract.test.ts,modelValidation.test.ts - Gates:
ci-cd/check-oas-route-resolution.js,ci-cd/check-hexagonal-boundaries.js,ci-cd/run-unit-tests.js - Canonical targets:
spec/1.0.0.yml,spec/asyncapi/,spec/asyncapi/1.0.0.grpc.yml,spec/asyncapi/async-api.proto - Requirements: Requirement 036 (port objects), Requirement 026 (OAS 3.1 entity compliance), Requirement 126, Contract 3 (ownership and public contracts, Contracts 2–3)
- Sibling E-chain documents: Service Management Application, Service Management Module Architecture and IDesignerStore Port Contract, Domain Designer Features and Usage
- Linear: JUM-474 , JUM-475 , JUM-476 , JUM-477 , JUM-478 , JUM-470 , JUM-471 , JUM-547 , JUM-492