Skip to content

feat(project-engine-client): add GET /v1/url/resolve to the vendored client + mock#1771

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

feat(project-engine-client): add GET /v1/url/resolve to the vendored client + mock#1771
rainer-friederich merged 2 commits into
mainfrom
feat/resolve_urls

Conversation

@rainer-friederich

Copy link
Copy Markdown
Contributor

1. Abstract

Adds the Semrush Project Engine GET /v1/url/resolve endpoint to the vendored spacecat-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-fetch wrapper, so once the path is in the generated types a caller just does client.GET('/v1/url/resolve', { params: { query: { primary_url } } }).

  • Spec overlay (CR16): adds a model.UrlResolveResponse schema ({ domain, primary_url, is_valid }, all required) and the GET /v1/url/resolve path (query param primary_url required, 200/400/401 responses). Regenerating from the overlay updates src/generated/types.ts and 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).
  • Mock behaviour (before → after): the endpoint did not exist → the mock now canonicalizes primary_url. It strips the scheme and a leading www. while preserving any other subdomain and the path (http://www.lovesac.com/productslovesac.com/products; blog.hubspot.com stays blog.hubspot.com); domain is the registrable apex (subdomain stripped too). Unresolvable/garbage input returns { domain: '', primary_url: '', is_valid: false } at HTTP 200 — never an error — matching the live trap that consumers must check is_valid.
  • The canonicalization is a pure, unit-tested helper (mock/url-resolve.js) exposed on the mock context like the existing tagId/parentIdField helpers, rather than logic buried in the coverage-excluded route handler.
  • Endpoint inventory in docs/mock-usage.md updated 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.uk collapses to co.uk, unlike live's PSL), and an empty primary_url= returns 200 is_valid:false rather than 400 (Counterfact enforces the required presence — a missing param still 400s — but not the string minLength).

4. Required information

5. Affected / used mysticat-workspace projects

  • spacecat-api-service — will consume this endpoint (§2 of the spec): a new resolveUrl transport 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 the feat/customer_onboarding branch / Add Page Authentication Token Retrieval Functionality #737) will add the same resolve_url call. Also blocked on this release.

6. Additional information outside the code

  • Booted the stateful Counterfact mock (npm run test:e2e) and drove the new /v1/url/resolve route end-to-end through the real createSerenityProjectEngineApiClient — verified the canonical case, the subdomain/path-preserving case, the is_valid:false garbage case (HTTP 200), and the missing-vs-empty primary_url request-validation behaviour.
  • The live contract behind the mock was captured directly against prod Project Engine (https://adobe-hackathon.semrush.com) with a real IMS bearer (serenity-docs#25 §0) — method, param, response shape, error codes, and the is_valid:false empty-string trap are ground truth, not assumptions.

7. Test plan

  • (a) Local: unit suite plus the offline overlay freshness gate (asserts CR16 both applies and is necessary against the vendored swagger), and the MOCK_E2E=1 end-to-end suite driving the booted mock through the typed client — all exercised locally.
  • (b) Per-env: this package ships no runtime service, so there is no eph/dev/stage/prod deploy to verify. After merge, semantic-release publishes a new npm version and the GHCR mock Docker image republishes on the release tag; consumers verify by bumping @adobe/spacecat-shared-project-engine-client and calling client.GET('/v1/url/resolve', { params: { query: { primary_url } } }) against the published mock image in their integration tests.

8. Deployment & merge order

  • This PR is the hard prerequisite for the rest of serenity-docs#25 — it must merge and release (npm + GHCR mock image) first.
  • After release: the api-service change (§2) bumps the dependency and adds the resolve-before-write; the migration CLI change (§3) does the same. Both are related and 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

…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>

@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-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:76 uses parsed.pathname which excludes query/hash by design, but no test proves resolveUrl('https://lovesac.com/products?ref=123#top') strips them. One assertion would document the decision and catch a future refactor to .href or .path.
  • nit: No test for port stripping - mock/url-resolve.js:57 uses .hostname (not .host), so ports are implicitly dropped. A test for resolveUrl('https://www.lovesac.com:8443/path') would pin this.
  • nit: Trailing-slash semantics undocumented - https://lovesac.com/products/ preserves the trailing slash since parsed.pathname returns /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 👎.

@MysticatBot MysticatBot added ai-reviewed Reviewed by AI complexity:high High complexity PR labels Jul 3, 2026
…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>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

This PR will trigger a minor release when merged.

@rainer-friederich

Copy link
Copy Markdown
Contributor Author

Addressed the review's 4 non-blocking items in cb636ef (all test-only — no behaviour change).

MysticatBot:

  • nit: query-string/fragment stripping untested — done. Added an assertion that https://lovesac.com/products?ref=123#toplovesac.com/products (query + fragment dropped by design, since the canonicalizer reads parsed.pathname).
  • nit: port stripping untested — done. Added https://www.lovesac.com:8443/pathlovesac.com/path, pinning the .hostname (not .host) choice.
  • nit: trailing-slash semantics undocumented — done. Added a test pinning the mock's behaviour (/products/ is preserved; only a bare root / is dropped), with a comment noting live trailing-slash handling is unverified so the test documents the mock's deliberate choice.
  • suggestion: composition-seam assertion — done. Added createUrlResolveMock(resolveUrl(...)) for both a valid and a garbage input, locking the pure-function ↔ factory-default contract that the coverage-excluded route handler relies on, without booting the server.

CI "Test" check: will confirm green before considering this ready.

@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 - 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 👎.

@rainer-friederich
rainer-friederich merged commit e8f22b7 into main Jul 3, 2026
8 checks passed
@rainer-friederich
rainer-friederich deleted the feat/resolve_urls branch July 3, 2026 10:04
solaris007 pushed a commit that referenced this pull request Jul 3, 2026
## [@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)
@solaris007

Copy link
Copy Markdown
Member

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

The release is available on:

Your semantic-release bot 📦🚀

rainer-friederich added a commit to adobe/spacecat-api-service that referenced this pull request Jul 6, 2026
… 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>
rainer-friederich added a commit that referenced this pull request Jul 6, 2026
…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>
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