Skip to content

feat(project-engine-client): URL-safe tag ids + id-based prompt-write mock routes#1764

Merged
rainer-friederich merged 3 commits into
mainfrom
feat/aligned_sub_categories
Jul 2, 2026
Merged

feat(project-engine-client): URL-safe tag ids + id-based prompt-write mock routes#1764
rainer-friederich merged 3 commits into
mainfrom
feat/aligned_sub_categories

Conversation

@rainer-friederich

Copy link
Copy Markdown
Contributor

1. Abstract

Fixes the Project Engine mock's tag-id generation to be URL-safe (issue #1760) and adds the two id-based prompt-write mock routes (POST /aio/prompts, PUT /aio/prompts/tags) needed by the cross-repo sub-categories ID-based alignment program.

2. Reasoning

The mock derived a tag's id as tag-${encodeURIComponent(name)}, so a name like category:Running Shoes produced tag-category%3ARunning%20Shoes. That leaks into a URL path segment (PATCH /aio/tags/{tag_id}) or a query value (GET /aio/tags?parent_id=<id>), breaking round-trips for any consumer that builds real HTTP requests against the mock. Separately, two upstream endpoints the alignment program depends on — id-based prompt create and batch tag-reference update — had no mock coverage at all, and a set of production probes run against adobe-hackathon.semrush.com on 2026-07-02 (see the implementation plan linked below) surfaced live behavior the mock needs to mirror: POST /aio/tags is NOT idempotent on a same-name/same-parent duplicate (it 500s), and the id-based create/update endpoints have specific response shapes and merge/replace semantics that differ from what the vendored swagger documents.

3. High-level overview of the changes

  • Tag id (issue project-engine-client mock: AIO tag ids embed %3A (name-derived) and don't round-trip through URLs — blocks nested children-drill / PATCH-by-id IT #1760): replaced the URL-unsafe tag-<url-encoded-name> id with an opaque tag-<first-16-hex-of-sha256(name)> id. Still a pure function of name (needed so a standalone tag and the same tag embedded on a prompt correlate, and so ops.tags.upsertMany stays idempotent-by-name), but never needs encoding as a path segment or query value.
  • New routes: POST /aio/prompts (id-based prompt create, {items, tag_ids} → a paginated list-wrapper response, matching live rather than the vendored swagger's bare-object shape) and PUT /aio/prompts/tags (batch tag-reference update, replace: false merges references onto a prompt, replace: true replaces the full set).
  • POST /aio/tags now 500s on a same-name/same-parent duplicate create, matching live — previously the mock silently reused the existing tag. ops.tags.upsertMany now does an atomic resolve-before-create check itself (nothing is written if any tag in a batch collides), which is the same discipline every real consumer of this endpoint now has to follow. A same-name tag under a different parent still resolves to the same id and is reused rather than treated as a collision — the id is derived from the name alone (not parent-aware), which is a documented, deliberate limitation of this mock rather than of the live API.

4. Required information

5. Affected / used mysticat-workspace projects

  • spacecat-api-service — consumed: its serenity proxy IT/e2e suite runs against this package's GHCR mock image; a future follow-up PR there is expected to exercise the two new id-based routes once this image is republished.
  • mysticat-data-service — contract: the serenity migration CLI now writes prompts via the id-based endpoints this mock models; it currently tests against an in-process fake, not this mock, so it is unaffected by this PR directly but shares the same upstream contract.

6. Additional information outside the code

The mock's new/changed behavior was checked against a set of live production probes run on 2026-07-02 against adobe-hackathon.semrush.com (documented in the serenity-docs implementation plan linked above): the POST /aio/prompts response shape, its draft-only visibility until publish, atomic failure on an unknown tag id, PUT /aio/prompts/tags's merge-vs-replace semantics and its silent-204-on-unknown-prompt-id behavior, and the POST /aio/tags same-name-collision 500. Each of those live-verified behaviors is what this PR's mock changes mirror.

7. Test plan

(a) Ran this package's unit suite and its e2e suite (which boots the mock and exercises every route over real HTTP, including the new/changed ones and the auth/quota surfaces) locally; also ran the package's lint and test:types (tsc @ts-check) gates.
(b) No environment-specific verification applies — this package isn't deployed as a service; its only downstream effect is the GHCR mock image consumed by other repos' IT suites, which needs a version bump + republish (this repo's normal release flow) before spacecat-api-service or any other consumer picks up these changes.

8. Deployment & merge order

🤖 Generated with Claude Code

… mock routes

Fixes issue #1760: the mock's tag id was tag-${encodeURIComponent(name)},
leaking %3A/%20 into a PATCH-by-id path segment or a parent_id query value.
Replaced with an opaque tag-<sha256(name) prefix> id — still a pure function
of name (needed for the standalone-tag/prompt-tag correlation and
ops.tags.upsertMany's idempotency-by-name), but URL-safe with no encoding.

Adds the two id-based prompt-write endpoints the nested-tags program needs:
POST /aio/prompts (id-based create, list-wrapper response per the live
gate-2 probe) and the new PUT /aio/prompts/tags (batch attach, merge vs
replace per gate-3).

Also flips POST /aio/tags to match live: a same-name/same-parent create is
NOT idempotent, it 500s (gate 7, verified live 2026-07-02). ops.tags.upsertMany
now does the atomic resolve-before-create check itself instead of silently
reusing an existing row — the same discipline every real consumer of this
endpoint (the api-service proxy, the migration CLI) now has to follow, so the
mock should enforce it too rather than mask a regression. Same-name-under-a-
different-parent still collapses onto the shared derived id (documented mock
limitation — the id is name-only, not parent-aware, to keep the
tagged.js/prompt correlation contract intact).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rainer-friederich rainer-friederich requested review from MysticatBot and removed request for MysticatBot July 2, 2026 10:46

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @rainer-friederich,

Verdict: Approve - well-scoped mock fidelity improvements with thorough live-verification discipline and strong test coverage.
Complexity: HIGH - large diff; API surface signal.
Changes: Replaces URL-unsafe tag-id encoding with an opaque SHA256-derived token, adds two id-based prompt-write mock routes (POST /aio/prompts, PUT /aio/prompts/tags), and makes POST /aio/tags collision-aware to match live behavior (11 files).

Non-blocking (5): minor issues and suggestions
  • nit: docs/mock-usage.md - the createProjectTags row documents same-name/same-parent collision but omits that intra-batch duplicates (same name twice in one request) also trigger the 500. Consider adding "or appears twice in the same batch".
  • suggestion: mock/counterfact/routes/.../prompts/tags.js:34 - the unknown-tag-reference fallback ({ id: tid, name: tid }) is untested. A unit test exercising this path would prevent a silent regression if the fallback is accidentally removed.
  • nit: mock/counterfact/routes/.../prompts/tags.js:51 - the return { status: 204 } raw bypass diverges from sibling DELETE handlers that use $.response[204]. Consistency across the route directory reduces cognitive load.
  • suggestion: Consider extracting the tag-resolution logic (build Map from ops.tags.list, validate ids, embed { id, name } pairs) into a shared helper on context - it is now duplicated across prompts.js, prompts/tags.js, and prompts/tagged.js.
  • suggestion: Communicate the POST /aio/tags behavior change (was idempotent, now 500s on collision) to downstream consumers via a changelog entry or release note. Any test setup that POSTs the same tag names twice will break on the new mock image.

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 2m 7s | Cost: $5.53 | Commit: aba6c337967492f8ab18853dd266640499500af6
If this code review was useful, please react with 👍. Otherwise, react with 👎.

@MysticatBot MysticatBot added ai-reviewed Reviewed by AI complexity:high High complexity PR labels Jul 2, 2026
…solvable-ref e2e coverage

Addresses two of MysticatBot's review 4616818458 suggestions on PR #1764:
mock-usage.md now documents that an intra-batch duplicate name also 500s
(not just a same-parent duplicate against an existing tag), and a new e2e
test exercises PUT /aio/prompts/tags' unresolvable-reference fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

@rainer-friederich

Copy link
Copy Markdown
Contributor Author

Addressed MysticatBot's non-blocking review (4616818458) in 90a1c5af.

  • nit: mock-usage.md didn't mention intra-batch duplicates also 500 — done, added "or appears twice in the same batch".
  • suggestion: unresolvable-reference fallback in prompts/tags.js was untested — done, added an e2e test covering it (a reference id with no matching tag still embeds and correlates via by_tags).
  • suggestion: the { status: 204 } bypass in prompts/tags.js "diverges from sibling DELETE handlers using $.response[204]" — declined, this is inaccurate. Every sibling DELETE handler in this routes directory (prompts.js, tags.js) also uses the raw { status: 204 } bypass; $.response[204] doesn't appear anywhere in mock/counterfact/routes/. My new file matches the established convention, not diverges from it.
  • suggestion: extract the tag-resolution logic into a shared context helper — declined. The actual overlap is a one-line Map build (prompts.js and prompts/tags.js only — prompts/tagged.js resolves by NAME via context.tagId() directly, it doesn't build this map at all, so the "3 files" framing overstates the duplication). The two callers' resolution semantics also diverge meaningfully (atomic-500-on-unknown vs soft {id, name:id} fallback), so a shared helper would need mode parameters to cover both — more complexity than the ~1 duplicated line it would remove.
  • suggestion: communicate the POST /aio/tags behavior change via a changelog/release note — declined as redundant with this repo's existing convention: semantic-release auto-generates CHANGELOG.md from conventional commit messages, and the original commit's body (aba6c337) already documents this exact behavior change in detail, which is what feeds that pipeline.

All fixes verified: lint, test:types, and both suites (unit 212 tests / e2e 62 tests, including the new one) green with 100% coverage.

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @rainer-friederich,

Verdict: Approve - prior non-blocking suggestions addressed or validly declined; no new concerns from the follow-up commits.
Complexity: HIGH - large diff; API surface signal.
Changes: Follow-up commits add the intra-batch duplicate mention to mock-usage docs and a new e2e test covering unresolvable tag-reference fallback (11 files).

Previously flagged, now resolved

  • Intra-batch duplicate mention added to docs/mock-usage.md (nit addressed in 90a1c5a)
  • Unresolvable reference fallback now has e2e coverage via the new "PUT embeds an unresolvable reference id" test (suggestion addressed in 90a1c5a)
  • { status: 204 } consistency concern withdrawn - verified that all sibling DELETE handlers in the routes directory use the same raw bypass pattern; the prior suggestion was based on an inaccurate premise
  • Tag-resolution helper extraction withdrawn - the overlap is narrower than stated (2 files, 1 shared line, diverging semantics) and a helper with mode params would add more complexity than it removes
  • Changelog/release-note suggestion withdrawn - semantic-release auto-generates CHANGELOG from conventional commit messages, which already document the behavior change in detail

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 7m 54s | Cost: $1.79 | Commit: ecadc46f6f685513a86161f361bb592db566f14b
If this code review was useful, please react with 👍. Otherwise, react with 👎.

@rainer-friederich rainer-friederich merged commit 97b9079 into main Jul 2, 2026
8 checks passed
@rainer-friederich rainer-friederich deleted the feat/aligned_sub_categories branch July 2, 2026 11:19
solaris007 pushed a commit that referenced this pull request Jul 2, 2026
## [@adobe/spacecat-shared-project-engine-client-v1.7.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-project-engine-client-v1.6.0...@adobe/spacecat-shared-project-engine-client-v1.7.0) (2026-07-02)

### Features

* **project-engine-client:** URL-safe tag ids + id-based prompt-write mock routes ([#1764](#1764)) ([97b9079](97b9079)), closes [#1760](#1760) [#1760](#1760)
@solaris007

Copy link
Copy Markdown
Member

🎉 This PR is included in version @adobe/spacecat-shared-project-engine-client-v1.7.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

rainer-friederich added a commit that referenced this pull request Jul 2, 2026
…e (CR14) (#1765)

<!-- mysticat-pr-skill -->

## 1. Abstract
Corrects the vendored Semrush swagger's response type for `POST
/v2/.../aio/prompts` (id-based prompt create) via the spec overlay, and
switches the mock's handler for that route from a raw response bypass to
Counterfact's normal validated response path.

## 2. Reasoning
While building PR #1764 (the id-based prompt-write mock routes), a live
probe against `adobe-hackathon.semrush.com` confirmed `POST
/aio/prompts` actually returns a paginated list wrapper — `{page, total,
items: [{id, name}], existing_count}` — but the vendored public swagger
types its 200 response as a bare `model.StringIDName` (`{id, name}`), a
spec/live drift. That PR shipped a raw `{status, body, contentType}`
bypass to return the correct live shape, since going through
Counterfact's normal validated path would have failed against the wrong
schema. This PR closes that gap properly via the package's existing
spec-correction mechanism instead of leaving the bypass as the permanent
shape.

## 3. High-level overview of the changes
- Adds correction **CR14** to `spec/overlays/corrections.yaml`: retypes
`aio-create-prompt-v2`'s 200 response from `model.StringIDName` to the
already-vendored `model.StringIDNameListResponse` wrapper (extended with
an `existing_count` field it was missing), following the same
remove-then-add pattern already used by CR7/CR11 for other endpoints
whose response type needed replacing rather than merging.
- Regenerates `src/generated/types.ts` from the corrected spec.
- Updates the mock's `POST /v2/.../aio/prompts` handler to return its
200 and 500 responses through `$.response[...].json(...)`
(Counterfact-validated) instead of the raw bypass, now that the schema
is correct; the 405 quota response stays a raw bypass since 405 isn't a
declared response for this operation (same pattern as every other
quota-gated route in this package).

## 4. Required information
- Spec: adobe/serenity-docs#24
- Other: #1764

## 6. Additional information outside the code
The corrected response shape was verified against the same 2026-07-02
live production probe session (against `adobe-hackathon.semrush.com`)
that PR #1764 was built from — a single-item `POST /aio/prompts` batch
returned `{page, total, items, existing_count}`, not a bare `{id,
name}`.

## 7. Test plan
(a) Ran this package's unit suite (includes the overlay freshness gate,
which asserts every correction — including the new CR14 actions — still
applies and is still necessary against the vendored spec) and its e2e
suite, which boots the mock and exercises the corrected route over real
HTTP through the now-validated response path; also ran lint and
`test:types`.
(b) No environment-specific verification applies — this package isn't
deployed as a service; consumers pick up the corrected type on their
next `@adobe/spacecat-shared-project-engine-client` version bump.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
solaris007 pushed a commit that referenced this pull request Jul 2, 2026
## [@adobe/spacecat-shared-project-engine-client-v1.7.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-project-engine-client-v1.7.0...@adobe/spacecat-shared-project-engine-client-v1.7.1) (2026-07-02)

### Bug Fixes

* **project-engine-client:** retype aio-create-prompt-v2's 200 response (CR14) ([#1765](#1765)) ([33315e1](33315e1)), closes [#1764](#1764) [#1764](#1764)
rainer-friederich added a commit to adobe/spacecat-api-service that referenced this pull request Jul 2, 2026
… promote-to-root (#2741)

<!-- mysticat-pr-skill -->

## 1. Abstract
Adds the id-based prompt-write path, closed-dimension tag resolution,
and promote-to-root support to the Serenity nested-category-tags
surface, building on the bare-name-children fix already merged in PR
#2737.

## 2. Reasoning
The nested-category alignment work (serenity-docs#24, extending issue
#21) needs a way to tag prompts by upstream tag **id** rather than by
name: a topic name is only unambiguous within its own category, so the
name-based prompt-tagging endpoint (`aio/prompts/tagged`) can't
reference a nested child at all — it's what forced the migration CLI
into a flat-tag-then-reparent workaround. This PR adds the id-based
upstream write path api-service needs to let a UI or migration tool tag
a prompt with a specific child id directly. Landing alongside it: a way
to resolve the ids of the fixed-vocabulary `source`/`intent`/`type` tags
(needed to build a full `tagIds` set for a prompt), and closing gate 1
from the same live-verification session (serenity-docs#24 §3.1) — an
explicit promote-to-root request was accepted by the parser but silently
dropped before reaching the transport layer.

## 3. High-level overview of the changes
- **Id-based prompt writes**:
`SerenityPromptInput`/`SerenityUpdatePromptRequest` gain an optional
`tagIds` field (upstream tag ids), mutually exclusive with the existing
`tags` (names) field. New transport methods `createPromptsByIds` (`POST
aio/prompts`) and `updatePromptTags` (`PUT aio/prompts/tags`) call the
corresponding upstream endpoints. Both prompt-write twin handler pairs
(flat + subworkspace, create + update) route through whichever field the
caller supplied.
- **Closed-dimension tag resolution**: `POST /serenity/tags` now also
accepts `type: source|intent|type` (previously only `category`/`topic`).
A closed-dimension create is resolve-before-create — if the project
already has that exact tag, its id comes back unchanged (`200`,
`created: false`) instead of attempting another create, since it's a
small fixed set many independent callers need the id of. Open-dimension
behavior (a duplicate name is a hard upstream error the caller must
avoid) is unchanged.
- **Promote-to-root** (`PATCH /serenity/tags/:tagId`): an explicit JSON
`null` `parentId` now reaches the upstream call as a literal `null`,
distinct from an omitted `parentId` (which preserves the tag's current
parent). Previously both were collapsed to the same "omit `parent_id`"
behavior, so a client request to remove a child's parent silently did
nothing.
- Un-skips two integration assertions the original PR (#2737) could only
validate against live Semrush at the time: drilling a parent's children
by id, and a full rename-by-id round trip against the mock — both are
now exercisable because the mock's tag ids became URL-safe in a
since-merged spacecat-shared fix.

## 4. Required information
- Spec: adobe/serenity-docs#24
- Other: #2737 ·
adobe/spacecat-shared#1764

## 5. Affected / used mysticat-workspace projects
- **spacecat-shared** — consumed: the
`@adobe/spacecat-shared-project-engine-client` mock (bumped to 1.7.0)
supplies the id-based prompt-write routes this PR's IT suite exercises.
- **project-elmo-ui** — contract: the nested-category UI work (issue
2245) is expected to call these new endpoints once it reaches the
prompt-authoring slice.

## 7. Test plan
(a) Ran the full serenity-scoped unit suite, the OpenAPI contract suite,
and the route-capability coverage test locally; also ran `tsc --noEmit`
and the OpenAPI spec linter. Could not run the integration suite locally
(it needs the docker-compose Postgres + Semrush mock infrastructure) —
the two new/updated IT test blocks (id-based prompt create + read-back,
closed-dimension resolve/reject, the un-skipped id-round-trip tag tests)
are new territory for this repo's local tooling and will be verified via
CI's `ci/it-postgres` job.
(b) On dev/stage: exercise `POST /serenity/tags` with `type: source`
twice for the same brand/market to confirm the second call returns the
same id with `created: false`; exercise `PATCH /serenity/tags/:tagId`
with `parentId: null` on a real nested child and confirm it lists as a
root afterward.

## 8. Deployment & merge order
- Related: #2737 —
already merged; this PR is a follow-up on top of it, per the original
sequencing (serenity-docs#24's implementation plan).
- Related: adobe/spacecat-shared#1764 — already
merged (published as
`@adobe/spacecat-shared-project-engine-client@1.7.0`, which this PR's
`package.json` now pins).
- No hard merge-order dependency: this PR's own code doesn't require
anything further from spacecat-shared to merge first (1.7.0 is already
released).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed Reviewed by AI complexity:high High complexity PR released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants