feat(project-engine-client): add GET /v1/url/resolve to the vendored client + mock#1771
Conversation
…client + mock (serenity-docs#25 §1) Model the Semrush Project Engine URL-canonicalization endpoint the live frontend calls before writing a brand URL, so downstream consumers (api-service brand-URL write path, migration CLI) can resolve a raw URL to its canonical form and stop creating duplicate/mismatched brand-URL benchmarks. - overlay CR16: add model.UrlResolveResponse schema + GET /v1/url/resolve path (query primary_url required+minLength:1, 200/400/401), mirroring CR1/CR8; regen types.ts + pydantic via npm run generate (also catches up a stale CR14a existing_count in the pydantic artifacts). - mock/url-resolve.js: pure canonicalizer (strips scheme+www, apex=last-two-labels, rejects IP/single-label/empty-label, preserves path/subdomain), exposed on $.context.resolveUrl like tag-id.js/parent-id.js. - createUrlResolveMock factory (default = live invalid/empty shape) + counterfact route mock/counterfact/routes/v1/url/resolve.js. - docs/mock-usage.md endpoint inventory updated. Live contract captured 2026-07-03 against prod (serenity-docs#25 §0). is_valid:false returns empty strings at HTTP 200 (never an error); an empty primary_url= 200s is_valid:false in the mock (Counterfact enforces required presence, not minLength) — documented divergence. Tests: unit (100% branch), factory/context/type coverage, 3 e2e. 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-structured addition following established patterns, no blocking issues.
Complexity: HIGH - medium diff; API surface signal.
Changes: Adds the Semrush Project Engine GET /v1/url/resolve endpoint to the vendored client via spec overlay CR16, generated types, a pure mock canonicalizer, and comprehensive tests (16 files).
Non-blocking (4): minor issues and suggestions
- nit: No test pinning query-string/fragment stripping behavior -
mock/url-resolve.js:76usesparsed.pathnamewhich excludes query/hash by design, but no test provesresolveUrl('https://lovesac.com/products?ref=123#top')strips them. One assertion would document the decision and catch a future refactor to.hrefor.path. - nit: No test for port stripping -
mock/url-resolve.js:57uses.hostname(not.host), so ports are implicitly dropped. A test forresolveUrl('https://www.lovesac.com:8443/path')would pin this. - nit: Trailing-slash semantics undocumented -
https://lovesac.com/products/preserves the trailing slash sinceparsed.pathnamereturns/products/which is not/. Whether live preserves or strips is unstated; a test documenting the choice prevents surprise. - suggestion: Add a composition-seam assertion (
createUrlResolveMock(resolveUrl('garbage'))yields{ domain: '', primary_url: '', is_valid: false }) to lock the contract between the pure function and the factory without booting the server. The route handler (the glue) is coverage-excluded, so this is the cheapest way to catch a factory-default drift.
Note: CI "Test" check is still pending - confirm it passes before merge.
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 53s | Cost: $5.37 | Commit: 91c42bdbc3e98ce0590b67d33c25fe6c70584cc1
If this code review was useful, please react with 👍. Otherwise, react with 👎.
…Bot review on #1771 - assert query-string + fragment are stripped (host + path only) - assert an explicit port is dropped (hostname, not host) - pin trailing-slash preservation (mock choice; live unverified) - add the resolveUrl → createUrlResolveMock composition-seam assertion (the coverage-excluded route glue) for both a valid and an invalid input Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This PR will trigger a minor release when merged. |
|
Addressed the review's 4 non-blocking items in MysticatBot:
CI "Test" check: will confirm green before considering this ready. |
There was a problem hiding this comment.
Hey @rainer-friederich,
Verdict: Approve - all four prior nits addressed cleanly; no new concerns.
Complexity: HIGH - large diff; API surface signal.
Changes: Adds the Semrush Project Engine GET /v1/url/resolve endpoint to the vendored client via spec overlay CR16, generated types, a pure mock canonicalizer, and comprehensive tests (16 files).
Note: CI "Test" and "E2E" checks are still pending - confirm green before merge.
Previously flagged, now resolved
- Query-string/fragment stripping now pinned by test (
url-resolve.test.js:59) - Port stripping now pinned by test (
url-resolve.test.js:66) - Trailing-slash semantics documented by test (
url-resolve.test.js:73) - Composition-seam assertion added for valid and invalid paths (
url-resolve.test.js:80)
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 9m 12s | Cost: $2.94 | Commit: cb636efdb0db6a2cb863e7b14da8321d27818d64
If this code review was useful, please react with 👍. Otherwise, react with 👎.
## [@adobe/spacecat-shared-project-engine-client-v1.8.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-project-engine-client-v1.7.1...@adobe/spacecat-shared-project-engine-client-v1.8.0) (2026-07-03) ### Features * **project-engine-client:** add GET /v1/url/resolve to the vendored client + mock ([#1771](#1771)) ([e8f22b7](e8f22b7)), closes [#737](#737) [serenity-docs#25](https://github.com/adobe/serenity-docs/issues/25) [serenity-docs#25](https://github.com/adobe/serenity-docs/issues/25)
|
🎉 This PR is included in version @adobe/spacecat-shared-project-engine-client-v1.8.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
… writing them (#2748) <!-- mysticat-pr-skill --> ## 1. Abstract Resolves a brand's website URLs to Semrush's canonical stored form (via the Project Engine `url/resolve` endpoint) before writing them to an AIO benchmark, on both the market-create and brand-edit paths. This is §2 of the cross-repo brand-URL normalization work. ## 2. Reasoning Brand URLs were pushed to Semrush verbatim (e.g. `https://www.lovesac.com`). Semrush stores brand URLs in a canonical, scheme-less form (`lovesac.com`) and its own frontend normalizes first by calling `url/resolve` before writing. Sending the raw URL creates a duplicate/mismatched brand-URL entry that does not line up with the canonical benchmark Semrush already holds (`https://www.lovesac.com` and `lovesac.com` are two distinct entries). Reported by the Semrush team; the endpoint had to be modeled in the vendored client first (shipped as PE-client 1.8.0), and this change is the consumer side. ## 3. High-level overview of the changes Before: every brand-URL entry (website, social, earned) was pushed to the own-brand benchmark exactly as stored. After: website-type entries are first canonicalized through Semrush's `url/resolve`, so the value written matches what Semrush already holds. - Bumps `@adobe/spacecat-shared-project-engine-client` to 1.8.0, which adds the `url/resolve` endpoint to the typed client and its mock. - Adds a `resolveUrl` transport method (a typed GET of `url/resolve`). - Adds a `resolveWebsiteEntries` step that runs after entries are collected and before they are written, at both write paths: - CREATE (market subworkspace create) — best-effort, unchanged failure semantics. - EDIT (brand edit re-sync) — resolves the desired set before diffing it against the benchmark's live URLs, so both sides of the diff are in the same canonical form. This makes the re-sync idempotent; a benchmark still holding a legacy raw URL is migrated once (delete raw, create canonical) and is stable thereafter. - Only website-type entries are resolved. Social and earned entries are identity handles (e.g. an Instagram profile URL), not brand domains, so they pass through unchanged. - `url/resolve` returns `is_valid: false` with empty strings (at HTTP 200) for unresolvable input; on that the raw (already https-filtered) URL is kept — the empty value is never written. A transport error (non-2xx) is left to propagate, so the existing best-effort (create) vs hard-fail (edit) behaviour is preserved. - Competitor URLs are out of scope — they already go through a separate host-normalization path. Note: the existing `toEntry` https-only guard is intentionally left unchanged. Resolution is a post-collection step, so `toEntry` only ever validates the raw stored inputs; the scheme-less canonical values it produces are never fed back through it. ## 4. Required information - Spec: adobe/serenity-docs#25 - Other: PE-client endpoint + mock (the §1 prerequisite, merged/released as 1.8.0) adobe/spacecat-shared#1771 ## 5. Affected / used mysticat-workspace projects - `spacecat-shared` (`spacecat-shared-project-engine-client`) — consumed: this PR depends on the `url/resolve` endpoint + mock added there and released as 1.8.0 (the dependency bump is in this PR). - `mysticat-data-service` — related, not touched here: the Serenity migration CLI (§3) will make the same `resolve_url` change on its own write path; it is a separate follow-up on that repo. ## 6. Additional information outside the code - Ran the serenity integration suite (`test/it/postgres/serenity.test.js`) locally against the freshly-published 1.8.0 Project Engine mock container (the harness pins the mock image tag to the installed client version). The full suite passed and the brand create/edit write paths exercised the new `resolveUrl` call end-to-end through the typed client and the real mock, with no failures. - Confirmed the 1.8.0 mock image (which contains the `url/resolve` route) is published to GHCR and pulls cleanly, so CI's IT job resolves the same tag. ## 7. Test plan - (a) Local: unit tests for the new `resolveWebsiteEntries` helper (website canonicalization, social/earned passthrough, `is_valid:false` fallback, www-vs-apex de-dup, transport-error propagation) and for both write paths (create attaches the canonical value; edit diffs on the canonical form — idempotent, plus the one-time legacy-URL migration). Serenity IT run locally against the 1.8.0 mock, green. - (b) Per-env: after merge, verify on dev by editing a Serenity brand's website URL (e.g. add `https://www.<brand>.com`) and confirming the AIO benchmark stores the canonical scheme-less form (`<brand>.com`) rather than a second www-prefixed entry, and that a subsequent no-op edit does not churn (no create/delete on re-sync). No schema or public-API change to verify. ## 8. Deployment & merge order - Depends on the §1 PE-client release: adobe/spacecat-shared#1771 (merged, published as 1.8.0) — this PR pins that version, so it can merge now. - Related follow-up (not blocking this PR): the migration CLI §3 in `mysticat-data-service`. Ordered sequence: §1 (done) → this PR (§2) → §3 migration CLI, independently. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…10, slim CR16 (#1772) <!-- mysticat-pr-skill --> ## 1. Abstract Re-vendors the refreshed Semrush Project Engine public swagger in the project-engine-client package and reconciles the correction overlay against it, retiring the corrections the refresh has caught up on. ## 2. Reasoning Semrush shipped a new revision of the Project Engine public swagger. Several of our overlay corrections (which align the vendored spec with the live API) exist only because the vendored spec lagged the live API; once upstream ships the same shape, the correction becomes a redundant no-op that the overlay freshness gate rejects. Re-vendoring lets us drop that now-dead maintenance and pull in the new upstream surface, while keeping the corrections upstream still hasn't fixed. ## 3. High-level overview of the changes The overlay drops from 16 corrections to 14; the vendored swagger and the generated TS/pydantic types are regenerated from the new upstream revision. - CR9 (`AISettings.primary_url`) — now present upstream, dropped. - CR10 (`AIOBenchmarkWithCounters.primary_url` + `root_domain`) — now present upstream, dropped. - CR16 — `GET /v1/url/resolve` and its response schema (`model.ResolveURLResponse`) are now vendored natively. The correction collapses from "add the path + add a custom schema" to a single action marking the three always-present fields (`domain`, `primary_url`, `is_valid`) `required` (upstream still omits that, and the mock always populates them — the same faithful-`required` convention as CR5). The internal type reference is renamed `model.UrlResolveResponse` -> `model.ResolveURLResponse`. - CR1–CR8 and CR11–CR15 are unchanged — the refresh did not fix them. The refresh also brings new upstream surface into the generated types: new endpoints (`managed-ai-models`, `crawls/stop`, `apply_interactive_unignore_issue_rules`), new fields (`primary_url` on `ProjectRequest`/`ProjectUpdateRequest`/`AISettings`, `main_brand`+`primary_url` on `AIOBenchmarkRequest`), a breaking reshape of `ApplyInteractiveIgnoreIssueRulesRequest`, and removal of the Site-Intelligence `allow_more_parallel_instances` endpoint + `SIProjectSettingsRequest`. None of the removed/reshaped shapes are referenced by the mock or client source, so nothing in this package breaks. The renamed resolve response type is internal to this package: the merged consumer keys off the path string `/v1/url/resolve` (openapi-fetch), not the type name, so it is unaffected and will pick up the new types on the next client release. ## 4. Required information - Spec: adobe/serenity-docs#25 - Other: consumer already merged — adobe/spacecat-api-service#2748 ; original CR16 endpoint + mock (released as 1.8.0) — #1771 ## 5. Affected / used mysticat-workspace projects - `spacecat-api-service` — consumed / contract: uses `url/resolve` via the typed client (merged in PR 2748). This PR renames the resolve response type internally; the consumer keys off the path, not the type name, so it is not broken and only re-bumps the client on the next release. - `mysticat-data-service` — related, not touched: the Serenity migration CLI performs the same `url/resolve` canonicalization on its own write path. ## 6. Additional information outside the code - Ran `npm run generate` end to end against the re-vendored spec: swagger2openapi conversion, the overlay apply (all remaining actions applied, none reported stale), and the TS + pydantic codegen all completed. - Booted the Counterfact mock and exercised the resolve endpoint through the typed client (canonicalization, `is_valid:false` empty-string fallback at HTTP 200, and the missing-vs-empty `primary_url` behaviour), confirming Counterfact response validation accepts the now-`required` `model.ResolveURLResponse`. ## 7. Test plan - (a) Local: the overlay freshness gate (asserts every remaining correction still applies and still changes the converted spec), the mock factory/context/url-resolve unit tests, and the resolve e2e against the booted mock were run locally and are green. Type-check against the regenerated types passes. - (b) Per-env: no service or public-API change here — this is a shared client package. After merge and release, bump `@adobe/spacecat-shared-project-engine-client` in the consumers and confirm `url/resolve` still resolves against the released mock image (the api-service serenity IT harness pins the mock tag to the installed client version). ## 8. Deployment & merge order - Related (not blocking): adobe/spacecat-api-service#2748 — the resolve consumer, already merged on client 1.8.0. It keeps working on 1.8.0; it picks up the renamed resolve type only when re-bumped to the release this PR produces. Ordered sequence: merge + release this PR, then re-bump the client in consumers at their own pace. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1. Abstract
Adds the Semrush Project Engine
GET /v1/url/resolveendpoint to the vendoredspacecat-shared-project-engine-client— its generated types, the spec overlay, and the stateful Counterfact mock. This is §1 of the cross-repo brand-URL normalization spec.2. Reasoning
When we add a brand URL to a Semrush AIO project we currently send the raw URL verbatim (e.g.
https://www.lovesac.com). Semrush stores brand URLs in a canonical, scheme-less form (lovesac.com) and its own frontend normalizes first by calling the Project Engine URL-resolve endpoint before writing. Sending the raw URL produces a duplicate/mismatched brand-URL entry that does not line up with the canonical benchmark Semrush already holds. Fixing that requires the endpoint to exist in the vendored typed client first — this PR is that prerequisite. Reported by the Semrush team in the Elements-wrappers review thread (2026-07-02/03); the live contract was captured directly against prod.3. High-level overview of the changes
Models a live endpoint that is absent from the vendored swagger, following the established overlay + regenerate + mock pattern (precedent CR1/CR8). No consumer-facing client code changes — the client is a thin generic
openapi-fetchwrapper, so once the path is in the generated types a caller just doesclient.GET('/v1/url/resolve', { params: { query: { primary_url } } }).model.UrlResolveResponseschema ({ domain, primary_url, is_valid }, all required) and theGET /v1/url/resolvepath (query paramprimary_urlrequired,200/400/401responses). Regenerating from the overlay updatessrc/generated/types.tsand the pydantic models; the regen also catches up a previously-stale pydantic artifact (a CR14 field that had only been regenerated into the TS types).primary_url. It strips the scheme and a leadingwww.while preserving any other subdomain and the path (http://www.lovesac.com/products→lovesac.com/products;blog.hubspot.comstaysblog.hubspot.com);domainis the registrable apex (subdomain stripped too). Unresolvable/garbage input returns{ domain: '', primary_url: '', is_valid: false }at HTTP200— never an error — matching the live trap that consumers must checkis_valid.mock/url-resolve.js) exposed on the mock context like the existingtagId/parentIdFieldhelpers, rather than logic buried in the coverage-excluded route handler.docs/mock-usage.mdupdated to list the new route.Two intentional, documented mock simplifications: the apex is the last two dot-labels (so a multi-part public suffix like
example.co.ukcollapses toco.uk, unlike live's PSL), and an emptyprimary_url=returns200 is_valid:falserather than400(Counterfact enforces therequiredpresence — a missing param still400s — but not the stringminLength).4. Required information
5. Affected / used mysticat-workspace projects
spacecat-api-service— will consume this endpoint (§2 of the spec): a newresolveUrltransport method + resolve-before-write on the brand-URL CREATE/EDIT paths. Blocked on this package being released (contract dependency).mysticat-data-service— the Serenity migration CLI (§3, on thefeat/customer_onboardingbranch / Add Page Authentication Token Retrieval Functionality #737) will add the sameresolve_urlcall. Also blocked on this release.6. Additional information outside the code
npm run test:e2e) and drove the new/v1/url/resolveroute end-to-end through the realcreateSerenityProjectEngineApiClient— verified the canonical case, the subdomain/path-preserving case, theis_valid:falsegarbage case (HTTP 200), and the missing-vs-emptyprimary_urlrequest-validation behaviour.https://adobe-hackathon.semrush.com) with a real IMS bearer (serenity-docs#25 §0) — method, param, response shape, error codes, and theis_valid:falseempty-string trap are ground truth, not assumptions.7. Test plan
MOCK_E2E=1end-to-end suite driving the booted mock through the typed client — all exercised locally.@adobe/spacecat-shared-project-engine-clientand callingclient.GET('/v1/url/resolve', { params: { query: { primary_url } } })against the published mock image in their integration tests.8. Deployment & merge order
relatedand neither can build/test until this version publishes.Ordered sequence: merge + release this PR → api-service §2 + migration §3 bump the dep and proceed in parallel.
🤖 Generated with Claude Code