You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Elevate @adobe/spacecat-shared-project-engine-client from a generic openapi-fetch passthrough into a real domain client: named operation methods plus a canonical domain model that owns the dimension-root tag/prompt semantics. Today the client exposes only typed HTTP verbs, so every consumer re-implements the same domain logic — the dimension-root model is currently maintained in three places. Making the client "real" lets those consumers delete their copies and depend on one authoritative implementation.
What the client is today (v1.10.0)
packages/spacecat-shared-project-engine-client exports exactly one thing (src/index.js):
createSerenityProjectEngineApiClient(options) → Client<paths> — a ~135-line wrapper (src/client.js + src/internal.js) around openapi-fetch over the generated src/generated/types.ts.
There are no named methods (createProject(), listTags(), reshapeTags()…), no domain model (Tag, dimension = path[0], the four roots, the closed intent/source/type taxonomies), and no helpers (path assembly, snake↔camel normalisation, dedupe/lookup, draft-vs-published tag reconciliation). Consumers call raw client.GET('/v1/...') / client.PATCH(...) with snake_case wire shapes and build all domain meaning themselves.
The cost: the domain model is triplicated
The same dimension-root logic is hand-maintained in three independent places:
spacecat-api-service (WP2, feat(serenity): resolve tag dimensions from the tag tree, not a name prefix spacecat-api-service#2788): src/support/serenity/handlers/prompts.js — buildTagsOf(item) (maps wire parent_id/path → {id, name, parentId, path}, root-first path excluding the tag) and buildPromptDto(geoTargetId, languageCode, item); plus a hand-rolled src/support/serenity/rest-transport.js that wraps the client because the client is not domain-aware.
project-elmo-ui (WP3, https://github.com/adobe/project-elmo-ui/pull/2392): src/utils/serenityTags.ts — a full domain module: CATEGORY_DIMENSION/INTENT_DIMENSION/SOURCE_DIMENSION/TYPE_DIMENSION, VALUE_DEPTH/SUBCATEGORY_DEPTH, tagDimension, tagDepth, parentTagName, findDimensionRoot, dedupeTagsByName, findPromptTag, promptTagValue, collectTagValues, tagIdIndex, lookupTagId, categoryFilterTagIds, buildPromptTagIds, findTagIdByName, plus the CSV dimension mapping.
mysticat-data-service (WP4, Python): scripts/serenity_migration/tags.py — DIMENSION_ROOTS, CLOSED_DIMENSION_VALUES, the Tag model, legacy_prefix, bare_tag_name, prompt_tag_keys, build_prompt_tags, resolve_prompt_tag_ids.
A new intent value, a renamed dimension, or a change to path shaping has to land in all three or they silently diverge. The model of record is the dimension-root spec (https://github.com/adobe/serenity-docs/pull/39), but nothing enforces that the three implementations agree with it or each other.
Scope: what "a real client" means here
Named operation methods for the surface consumers actually use — projects (list/create/rename/delete/publish), aio tags (list/create/patch = rename/re-parent/delete), aio prompts (by_tags/create-by-ids/delete-by-ids), ai_models, benchmarks, brand_urls — instead of raw client.GET('/path'). Keep the generated typed verbs exposed as an escape hatch for un-modelled endpoints; do not regress that.
A canonical domain model owning the dimension-root semantics: the four roots (category, intent, source, type), dimension = path[0], tag depth, the closed intent/source/type value taxonomies, path handling (root-first, excluding the tag), and the snake_case-wire ↔ camelCase-domain boundary ({id, name, parentId, path}). This absorbs api-service's buildTagsOf/buildPromptDto and elmo's serenityTags.ts.
Reshape / draft-vs-published helpers for the patterns consumers keep re-deriving: tag reshape (create roots, demote legacy roots via PATCH preserving id + subtree), and reading/reconciling the draft tree vs the published tree (the divergence that the migration CLI's reconcile path depends on). The raw client only passes a draft flag through; a real client should offer the reconciliation primitive.
Single source of truth for the taxonomy. The four roots + closed values should live in one declarative place the client owns (and that the Python side can mirror), so TS and Python cannot drift. This is the "domain model SSOT" follow-up flagged as out of scope in the companion issue (see below); it lands here.
Consumer migration (acceptance): api-service drops buildTagsOf/buildPromptDto/rest-transport.js domain logic and elmo drops the domain half of serenityTags.ts, both using the client's domain methods/model instead. (elmo's pure view concerns — CSV headers, filter-id assembly for its pickers — can stay UI-side; the tag semantics move to the client.)
Relationship to the companion issue
This is the domain-model half; the transport/publishing half for Python is tracked in #1805 (publish a Python client + types at transport parity). Together they define the target: the TS client owns the domain model; both TS and Python clients are real, published, and generated from the one spec (spec/projectengine_swagger_public.yaml). The taxonomy SSOT introduced here should be consumable by the Python client so tags.py's hand-encoded copy can be retired too.
Out of scope
Transport behaviour (auth/retry/base-URL) is already correct in src/client.js / src/internal.js — not this issue.
Regenerating/publishing the Python artifacts — that is issue 1805.
Workspace provisioning (/enterprise/users/api) is @adobe/spacecat-shared-user-manager-client, a separate package with the same "not a real client" shape — companion work if desired.
Summary
Elevate
@adobe/spacecat-shared-project-engine-clientfrom a genericopenapi-fetchpassthrough into a real domain client: named operation methods plus a canonical domain model that owns the dimension-root tag/prompt semantics. Today the client exposes only typed HTTP verbs, so every consumer re-implements the same domain logic — the dimension-root model is currently maintained in three places. Making the client "real" lets those consumers delete their copies and depend on one authoritative implementation.What the client is today (v1.10.0)
packages/spacecat-shared-project-engine-clientexports exactly one thing (src/index.js):createSerenityProjectEngineApiClient(options)→Client<paths>— a ~135-line wrapper (src/client.js+src/internal.js) aroundopenapi-fetchover the generatedsrc/generated/types.ts.There are no named methods (
createProject(),listTags(),reshapeTags()…), no domain model (Tag,dimension = path[0], the four roots, the closedintent/source/typetaxonomies), and no helpers (path assembly, snake↔camel normalisation, dedupe/lookup, draft-vs-published tag reconciliation). Consumers call rawclient.GET('/v1/...')/client.PATCH(...)with snake_case wire shapes and build all domain meaning themselves.The cost: the domain model is triplicated
The same dimension-root logic is hand-maintained in three independent places:
src/support/serenity/handlers/prompts.js—buildTagsOf(item)(maps wireparent_id/path→{id, name, parentId, path}, root-first path excluding the tag) andbuildPromptDto(geoTargetId, languageCode, item); plus a hand-rolledsrc/support/serenity/rest-transport.jsthat wraps the client because the client is not domain-aware.src/utils/serenityTags.ts— a full domain module:CATEGORY_DIMENSION/INTENT_DIMENSION/SOURCE_DIMENSION/TYPE_DIMENSION,VALUE_DEPTH/SUBCATEGORY_DEPTH,tagDimension,tagDepth,parentTagName,findDimensionRoot,dedupeTagsByName,findPromptTag,promptTagValue,collectTagValues,tagIdIndex,lookupTagId,categoryFilterTagIds,buildPromptTagIds,findTagIdByName, plus the CSV dimension mapping.scripts/serenity_migration/tags.py—DIMENSION_ROOTS,CLOSED_DIMENSION_VALUES, theTagmodel,legacy_prefix,bare_tag_name,prompt_tag_keys,build_prompt_tags,resolve_prompt_tag_ids.A new intent value, a renamed dimension, or a change to path shaping has to land in all three or they silently diverge. The model of record is the dimension-root spec (https://github.com/adobe/serenity-docs/pull/39), but nothing enforces that the three implementations agree with it or each other.
Scope: what "a real client" means here
client.GET('/path'). Keep the generated typed verbs exposed as an escape hatch for un-modelled endpoints; do not regress that.category,intent,source,type),dimension = path[0], tag depth, the closedintent/source/typevalue taxonomies, path handling (root-first, excluding the tag), and the snake_case-wire ↔ camelCase-domain boundary ({id, name, parentId, path}). This absorbs api-service'sbuildTagsOf/buildPromptDtoand elmo'sserenityTags.ts.draftflag through; a real client should offer the reconciliation primitive.buildTagsOf/buildPromptDto/rest-transport.jsdomain logic and elmo drops the domain half ofserenityTags.ts, both using the client's domain methods/model instead. (elmo's pure view concerns — CSV headers, filter-id assembly for its pickers — can stay UI-side; the tag semantics move to the client.)Relationship to the companion issue
This is the domain-model half; the transport/publishing half for Python is tracked in #1805 (publish a Python client + types at transport parity). Together they define the target: the TS client owns the domain model; both TS and Python clients are real, published, and generated from the one spec (
spec/projectengine_swagger_public.yaml). The taxonomy SSOT introduced here should be consumable by the Python client sotags.py's hand-encoded copy can be retired too.Out of scope
src/client.js/src/internal.js— not this issue./enterprise/users/api) is@adobe/spacecat-shared-user-manager-client, a separate package with the same "not a real client" shape — companion work if desired.Pointers
packages/spacecat-shared-project-engine-client—src/index.js,src/client.js,src/generated/types.ts; specspec/projectengine_swagger_public.yaml.src/support/serenity/handlers/prompts.js(buildTagsOf,buildPromptDto) +src/support/serenity/rest-transport.js; elmosrc/utils/serenityTags.ts.