feat(project-engine-client): URL-safe tag ids + id-based prompt-write mock routes#1764
Conversation
… 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>
There was a problem hiding this comment.
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- thecreateProjectTagsrow 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- thereturn { 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 oncontext- it is now duplicated acrossprompts.js,prompts/tags.js, andprompts/tagged.js. - suggestion: Communicate the
POST /aio/tagsbehavior 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 👎.
…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>
|
This PR will trigger a minor release when merged. |
|
Addressed MysticatBot's non-blocking review (4616818458) in
All fixes verified: lint, |
There was a problem hiding this comment.
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 👎.
## [@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)
|
🎉 This PR is included in version @adobe/spacecat-shared-project-engine-client-v1.7.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
…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>
## [@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)
… 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>
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 likecategory:Running Shoesproducedtag-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 againstadobe-hackathon.semrush.comon 2026-07-02 (see the implementation plan linked below) surfaced live behavior the mock needs to mirror:POST /aio/tagsis 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-<url-encoded-name>id with an opaquetag-<first-16-hex-of-sha256(name)>id. Still a pure function ofname(needed so a standalone tag and the same tag embedded on a prompt correlate, and soops.tags.upsertManystays idempotent-by-name), but never needs encoding as a path segment or query value.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) andPUT /aio/prompts/tags(batch tag-reference update,replace: falsemerges references onto a prompt,replace: truereplaces the full set).POST /aio/tagsnow 500s on a same-name/same-parent duplicate create, matching live — previously the mock silently reused the existing tag.ops.tags.upsertManynow 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
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): thePOST /aio/promptsresponse 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 thePOST /aio/tagssame-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