Skip to content

docs(api-reference): add v2 service landing pages - #2833

Open
aaronmichaelacosta wants to merge 27 commits into
APPEX-956/deprecation-policyfrom
APPEX-1609/v2-service-pages
Open

aaronmichaelacosta wants to merge 27 commits into
APPEX-956/deprecation-policyfrom
APPEX-1609/v2-service-pages

Conversation

@aaronmichaelacosta

@aaronmichaelacosta aaronmichaelacosta commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

What this adds

Eleven service landing pages under the v2 API — one per service in the Stable API proposal — each opening as the root of its own nav group, above that service's endpoints.

Each page says what the service covers and lists its endpoints in a generated table. The table and the nav group are both rendered from the spec in the sync workflow, so a service with nothing published yet shows a generated empty state rather than a placeholder that goes stale, and pages fill up as the migration lands.

They live under v2 rather than in a new unversioned section, per the call in [this thread] — customers already know the v1/v2 split, so v1 drains and disappears instead of a third concept showing up.

Scans and Agentic Workflows are one Jobs service, not two: a job is the parent category of both a scan and a workflow job, so merging them now avoids merging them later. That's why it's eleven pages and not twelve.

Also drops the blanket "experimental" framing

The new services contradict it, in two places:

  • the dropdown said v2 (Experimental) — claiming the whole API is experimental, while the page inside it explains per-endpoint maturity badges. Now just v2.
  • the Introduction opened with "The v2 API is under active development". Removed. The rest of that sentence stays, since "we won't deprecate v1 until v2 covers it" is still true.

Merge this before the semgrep-app side reaches prod

Mintlify builds an endpoint's page URL from its first tag, so the regrouping in semgrep-app moves 203 of the 206 v2 endpoint pages. scripts/generate_openapi_redirects.py here is what turns those moves into redirects; without it the first spec sync after that deploy leaves every old URL 404ing. v1 is deliberately untouched by that change, so no v1 URL moves.

Nothing is urgent in this direction: with this merged, the docs render as they do today until the app change deploys.

One thing left alone

The Introduction's maturity list still says stable endpoints get "no breaking changes", while the deprecation policy says they get six months' notice before one. Two different public promises on adjacent pages. Reconciling them is an editorial call rather than a scaffolding change, so I've left it — happy to fold it in if you'd rather.

Test plan

  • uv run pytest scripts/tests — 196 passed, 2 skipped
  • npx mintlify@latest validate passes

Stacked on #2832. Pairs with the semgrep-app directory merge (APPEX-1609).

🤖 Generated with Claude Code

Copy link
Copy Markdown
Contributor Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@aaronmichaelacosta
aaronmichaelacosta force-pushed the APPEX-956/deprecation-policy branch from 9400249 to 691f1ba Compare September 8, 2026 17:17
@aaronmichaelacosta
aaronmichaelacosta force-pushed the APPEX-1609/v2-service-pages branch 2 times, most recently from 84d07fd to 310ed6d Compare September 8, 2026 17:27
@aaronmichaelacosta
aaronmichaelacosta force-pushed the APPEX-956/deprecation-policy branch from 691f1ba to 547a2d0 Compare September 8, 2026 17:27
@aaronmichaelacosta
aaronmichaelacosta changed the base branch from APPEX-956/deprecation-policy to graphite-base/2833 September 9, 2026 17:26
@aaronmichaelacosta
aaronmichaelacosta force-pushed the APPEX-1609/v2-service-pages branch from d9aad15 to 4d634d9 Compare September 9, 2026 17:27
@aaronmichaelacosta
aaronmichaelacosta changed the base branch from graphite-base/2833 to APPEX-956/deprecation-policy September 9, 2026 17:27
gaprl and others added 16 commits September 21, 2026 10:05
Add a per-version API changelog (api-reference/v1/Changelog,
api-reference/v2/Changelog) generated from the OpenAPI specs' git
history with oasdiff. One <Update> block per day the spec changed,
rendered as Stripe-style tables: a colored change-verb chip, the
oasdiff description, and the endpoint as a method Badge linking to its
generated reference page. Repetitive changes (enum values, format
flips) are coalesced into single rows, and rss: true gives subscribers
a feed per API version.

The page fully regenerates on every run, so the backfill is retroactive
(v1 to 2026-05-28, v2 to 2026-07-14) and self-correcting. Endpoint
links are built only from the current spec, so removed endpoints render
unlinked instead of 404ing. The update-openapi-specs cron now installs
a checksum-pinned oasdiff, regenerates both pages after the mirror and
sort steps, and ships them in the same signed-commit PR as the spec
change.
Replace the split method-badge + underlined path link with a single
Badge pill containing method and path, wrapped in a raw anchor (a
markdown link would keep Mintlify's bottom-border underline). <wbr/>
elements after each path segment let long paths wrap inside the pill
instead of overflowing the table, without adding characters that
survive copy-paste; path braces are entity-escaped for JSX.

Tables gain an api-changelog-table wrapper div styled in styles.css:
Mintlify puts min-width: 150px on every td, which held the Change
column open well past its chip.
Rewrite oasdiff's JSON Schema property paths into the field reference a
consumer actually writes, and stop filing routine enum additions under a
breaking-change heading.

- Drop `allOf[#/components/schemas/protos.*]` segments. These specs are
  protobuf-generated and wrap every described $ref in a single-element
  allOf (the OpenAPI 3.0 workaround for $ref siblings being ignored), so
  the segment is composition bookkeeping that the wire payload has no
  level for. Removing it is more accurate as well as shorter, and it was
  the only place an internal proto package name reached the page.
- Fold an `items` segment into its parent as `[]` and join with `.`, so
  `automations/items/filters/allOf[...]/conditions/items/type` renders as
  `automations[].filters.conditions[].type`.
- Demote `response-property-enum-value-added` to INFO. oasdiff rates it
  WARN on the theory that a consumer may switch exhaustively over the
  enum, but these enums come from protobuf, where they are open by
  construction; 46 routine additions were burying the actual removals.

Field-name case is deliberately untouched: the specs emit both
`deployment_id` and `deploymentId` depending on the service, and the
changelog has to name the key the endpoint actually accepts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pages already carried `rss: true`, but Mintlify would have produced a
poor feed from them:

- It emits one entry per Markdown heading inside an `<Update>`, so our
  severity sections turned 3 dated entries into 6 for v1 and 10 into 12
  for v2 -- titled "Changes" (9x), "Breaking changes", "Potentially
  breaking changes", undated and repeated across every day.
- Feed entries "contain pure Markdown only ... exclude components, code,
  and HTML elements", and our rows are a JSX table of Badges and links, so
  subscribers would have received near-empty items.

Emit an explicit `rss={{ title, description }}` per `<Update>`, which is
what Mintlify recommends for updates containing excluded content. That
pins the feed to one dated item per day with a prose summary, and
decouples it from the heading structure -- otherwise a re-level that adds
or drops a section heading counts as "modifying headings inside an
existing Update" and republishes the entry to subscribers.

Also add `--rss-url`, which renders subscribe instructions on the page.
Mintlify only emits a `<link rel="alternate">` autodiscovery tag for
`rss: true` pages and no visible affordance, so the page has to say it.

Fixes a pre-existing summary bug along the way: the noun was attached only
via the "other" bucket, so a breaking-only day read "1 breaking" instead
of "1 breaking change". The count logic is now shared between the on-page
summary and the RSS description so the two cannot drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The caveat duplicates what the v2 Introduction page already states, and on
a page whose whole purpose is to label breaking changes individually it
read as a blanket disclaimer over entries that are mostly additive.

The generator keeps its --note flag; the v2 invocation just stops passing
one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mintlify "publishes entries when you add new Update components and when
you modify headings inside of existing Update components". Our severity
sections were Markdown headings, and which of them a given day has
depends on the level mapping -- so demoting a check (LEVEL_OVERRIDES) or
bumping oasdiff removes or adds a heading inside months-old entries and
re-notifies every subscriber about them. Today's enum demotion would have
done exactly that.

Render the section labels in bold instead. With no headings inside an
<Update>, the only remaining publish trigger is adding a new one. The rss=
prop already supplies the feed title, so nothing depended on these being
headings, and their anchors were colliding anyway -- every day emitted the
same #changes.

Verified on the PR preview deployment beforehand: an edit outside the
Update blocks left all 10 v2 items' guid and pubDate untouched, confirming
both are per-block rather than per-file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regrouping the public API into its new services re-tags roughly 200
endpoints without touching a path, a request, or a response. oasdiff
reports each one twice -- `api-tag-added` and `api-tag-removed` -- so the
reorganisation would land as ~400 rows on a single date. Measured, not
guessed: re-tagging the 6 MiscService operations produced exactly 12
changes.

A day of nothing but re-tagging would therefore create a new <Update>,
and a new <Update> is the one thing that still publishes to RSS
subscribers. The feed's largest entry ever would be an internal filing
change, and any real change shipped the same day would be buried under it.

Drop both checks. A tag decides which nav group an endpoint renders
under; oasdiff itself rates both INFO. Not entirely free -- some SDK
generators namespace by tag, so a consumer generating a client from the
published spec could see a method move class -- but it is not a change to
the HTTP contract, and that caveat is recorded next to the exclusion.

Real changes shipped alongside a re-tag still appear; only the tag rows
are dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ilter

Selecting "Breaking" in the side panel did nothing on either changelog
page: the chip lit up and the URL gained ?tags=Breaking, but all 10 v2
(and all 3 v1) updates stayed on screen.

The cause is Mintlify's <Update> visibility predicate, which reduces to
"hide me only if I have tags and none of them is selected":

    if (!filters.length) return true;
    const active = filters.filter(f => f.active);
    return !(active.length !== 0 && tags?.length)
      || tags.some(t => active.some(f => f.tag === t));

For an <Update> with no tags= at all, `tags` is undefined, `tags?.length`
is falsy, and the first clause short-circuits to true -- it is visible no
matter what is selected. We tagged only the days containing a level-3
change, so the other days had no tags to fail to match and nothing could
ever be hidden. Confirmed with a two-<Update> scratch page: one tagged +
one untagged filters nothing, while tagging both hides the non-match.

So give every <Update> a tag naming the day's highest severity --
Breaking, Potentially breaking, or Non-breaking. The chips are OR'd and
individually toggleable, so "show me anything that might break me" is
Breaking + Potentially breaking, and each severity is now also visible at
a glance under its date.

This rules out the structural suspects, incidentally: the scratch repro
had no table, no <div className="api-changelog-table">, no rss= prop and
no headings, and still failed -- so none of those, and not d4157be's
switch from headings to bold, had anything to do with it.

The regenerated pages change only <Update> opening lines, adding an
attribute. That is neither a new <Update> nor a heading edit inside an
existing one, so it does not trip the feed republish triggers d4157be
documented.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two presentation fixes for rows that span several endpoints:

- The endpoint cell stacks its pills with `<br/>`, which gave them no
  leading and read as one solid block. Adds a little vertical rhythm,
  scoped to the generated table so no badge elsewhere is affected.

- The "and N more" hovercard lists full endpoint paths, and a path is one
  unbreakable token. Mintlify caps the popup at 16rem, so the text ran
  ~190px past the rounded background instead of wrapping inside it. Allows
  a break mid-path and widens the cap so a path takes two lines, not five.
  Verified at 1440px and 390px: no overflow, still bounded by the viewport.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
oasdiff reports per endpoint and per property, so one change to a shared
type fanned out into a row per place it surfaced. Adding a single enum
value to `TriggerSource` produced six near-identical rows -- the same
sentence, once for each automations endpoint, split again across the
request and response copies of the property. The reader had to diff six
rows by eye to work out they were one change.

Two folds, both lossless:

- `merge_across_endpoints` folds rows whose wording is *identical* into
  one carrying an `endpoints` list. Only identical wording merges, so
  nothing is summarised away. Endpoint-scoped checks (`endpoint-*`,
  `api-*`) are exempt: two endpoints added on the same day both read
  "endpoint added", and folding those would hide the endpoints.

- `merge_enum_additions` then folds the request and response copies of one
  property together and lists the values once, normalising the wrapper
  path to its leaf.

The endpoint cell shows the first three and puts the rest behind an "and N
more" hovercard, so a broad change stays a readable height without making
any endpoint unreachable.

RSS is untouched: the feed description is built from the raw, unmerged
changes, so its endpoint count stays exact and nothing is truncated there.

193 rows to 112 across the two changelogs; the Aug 20 v2 entry goes from 10
rows to 4, and `TRIGGER_SOURCE_PATCH_READY` from 6 rows to 1. Both files
regenerate byte-identical from the committed specs with oasdiff 1.28.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The "and N more" hovercard ran its paths together with commas. Paths are
long enough to wrap inside a 28rem popup, so the separator landed mid-line
and there was no way to see where one path ended and the next began -- five
endpoints read as one run-on string.

One bullet per line instead. The tip is now passed as a JSX expression
rather than an attribute string so the newlines survive, with `json.dumps`
doing the escaping. That also drops the entity encoding of `{` and `}`:
they open an expression as bare JSX text, but are ordinary characters
inside a string literal, and encoding them showed the reader `&#123;`
instead of `{`. `white-space: pre-line` renders the breaks while still
collapsing spaces and wrapping long paths, which `pre` would not.

Verified in the browser: bullets on separate lines, braces intact, no
overflow. Both changelogs regenerate byte-identical from the committed
specs with oasdiff 1.28.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems, both found by running the deprecation lifecycle through the
pinned oasdiff (v1.28.0) rather than reasoning about it.

**A deprecation announcement was filed as "Non-breaking."** oasdiff rates
`endpoint-deprecated` INFO, which put it under "Changes" with that tag. The
announcement is the entire advance warning a caller gets, and "Non-breaking"
hides it from the one reader who most needs it: someone filtering the
changelog for what is going to break them. Re-levelled to potentially
breaking -- true on the day it ships, where "Breaking" would cry wolf. This
is not hypothetical; three v1 policies endpoints are already deprecated, and
that day moves from "6 changes / Non-breaking" to "3 potentially breaking ·
3 other changes".

**A removal done right produced no entry at all.** oasdiff reports a removal
only when something went wrong with it -- gone with no deprecation, or gone
before its published sunset date. Retire an endpoint exactly as the policy
prescribes and `oasdiff changelog` says "No changes to report", so the day
the endpoint stopped answering was the one day missing from the changelog.
The changelog recorded only the removals we botched.

There is no oasdiff flag for that case; the check does not exist upstream.
So `sunset_removals` diffs the two snapshots' operation sets directly and
synthesizes the entry, staying quiet whenever oasdiff already had something
to say so a removal never lands twice.

Both need `x-sunset` in the published spec to work, which protobuf-tools#202
now emits -- `x-deprecation` alone is invisible to oasdiff.

Not doing the third thing considered here: `--deprecation-days-stable 180`
would re-check every historical deprecation against *today*, so regenerating
the page would retroactively flag long-served windows as too short. The
notice windows are enforced at the proto level by protobuf-tools#203, which
is the right layer -- it fails the change before it ships rather than
narrating it afterwards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a Deprecation Policy page alongside the changelog in both API
versions, from the customer-facing half of the APPEX-956 policy doc. The
internal appendix (notice channels, the 410-vs-301 decision record, open
gaps) is not published.

The body lives in a snippet imported by both version pages. Terms-of-Use
duplicates its one sentence per version, but 30 lines of policy would
drift, and the repo already shares longer content this way (see
snippets/metrics.mdx).

One content change from the source doc: it was written before we decided
to publish a changelog, so "Finding out what is deprecated today" had no
push channel -- only the reference, response headers, and support. It now
leads with the changelog and its RSS feed, which records every deprecation
tagged Deprecated on the day it ships.

Deliberately not carried over: the in-app banner and admin email named in
the internal appendix. Neither exists yet (appendix gap 4), so publishing
them would commit us to channels we cannot serve. The invented
/api/migrations/<resource> URL (gap 5) appears only in the appendix and is
not published either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stable endpoints had two clocks: 6 months for a breaking change, 3 months
for removing an already-deprecated optional field. The short one does not
survive contact with how the clock actually starts.

Per styleguide 13.2 the clock only runs once the notice is public --
`Deprecation` and `Sunset` headers live, spec marked. A field marked
deprecated with no sunset date has therefore started no clock at all, so
the 3 months would be the entire notice a customer gets, not a follow-on
to time already served. That is half the window for something oasdiff
rates `response-optional-property-removed` at WARN, potentially breaking:
"optional" says the server may omit the field, not that nobody reads it.

One window for stable, whatever the change. Simpler to state, simpler to
honour, and it errs long on a public commitment.

Styleguide 13.1 transcribes this table and still carries the 3-month row;
that needs the same edit in semgrep-app. Its link to
api-deprecation-policy.md is also dangling -- that file does not exist in
that repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Takes all twelve suggestions from the review on #2832: the intro and
summary sentences, the maturity table's header and all three rows, the
undocumented-endpoints paragraph, the security and legal exception (now
two paragraphs), the "Identify deprecated endpoints" heading, and the
four-bullet list in its label-prefixed form.

Two of the twelve needed a judgement call rather than a substitution:

- One was a question -- "Do we add callouts in the API docs? If so, this
  info would benefit from being in a callout box." We do; <Note> appears
  152 times in this repo. The summary paragraph is now a <Note>, using the
  suggested wording.

- The frontmatter description suggestion was left on the v1 page, but v1
  and v2 are deliberate duplicates importing the same snippet, so applying
  it to one only would have made them diverge. Both are updated.

The undocumented-endpoints suggestion carried a double space after its
first period, which is dropped as a typo rather than reproduced.

Style and voice only. Nothing here changes what the policy commits to,
including the six-month window.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Doubles the beta notice period from 30 days to 60. 30 days is a short
window for a customer to notice a deprecation, schedule the work, and ship
it -- particularly for teams on a monthly release train, where it can
amount to a single opportunity to react.

Stable stays at 6 months and experimental still promises nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
aaronmichaelacosta and others added 10 commits September 21, 2026 10:05
Connor's review, plus two accuracy fixes the 410 write-up turned up.

Reviewer-requested:

- Move the maturity-badge sentence above the table, so the question the
  table raises ("how do I know which one an endpoint is?") is answered
  before it is asked rather than after.
- State stable's window as 180 days. Beta was already in days, so the
  column no longer mixes units, and a month-length assumption cannot
  change what the commitment means.
- Rewrite the stable row, which said only "covered by this policy". Beta
  is covered too, so the cell distinguished nothing.
- Soften the exception. It claimed to be "the only exception" and named
  a legal obligation specifically; both are more absolute than we can
  actually promise. Same care, less cornering.
- Restore what counts as a breaking change, from the APPEX-956 draft.
  Split into what will not change without notice and what may change at
  any time, because the second half is what a caller has to build for --
  tolerate new fields, do not match on error strings.

APPEX-956 wanted that definition to be a link to the oasdiff ruleset in
APPEX-959, so it would be mechanical rather than prose. APPEX-959 is
cancelled, so prose is what is left.

Accuracy, found while writing the sunset section against the
implementation in semgrep-app#31680:

- The `Link` header is the machine-readable pointer, and it is omitted
  when a removal has no replacement. "Returns 410 with a machine-readable
  pointer to its replacement" promised it unconditionally.
- Document the sunset response itself: headers, body, and which parts are
  stable enough to parse. `error` wording is not.

semgrep-app#31680 is still a draft, so this must not publish before it
ships -- the page would describe a response nothing returns yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two claims on this page were ahead of the generator, and both are now true
rather than aspirational.

"Tagged as Deprecated" described a chip in the Change column as though it
were a filter. The filter tag was "Non-breaking", so a reader who followed
this page's advice -- subscribe, watch for what will break you -- was told
to filter for exactly the thing that hid the notice. The generator now files
deprecations as potentially breaking, so say both: what the row is marked,
and which filter it survives.

Also say that the removal itself lands in the changelog. It did not
previously; a removal that served its full notice window produced no entry
at all, which made "the changelog records every deprecation" true and
"the changelog tells you when the endpoint went away" false.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twelve landing pages under the v2 dropdown, one per service in the Stable
API proposal, in a new "Services" nav group above the generated Endpoints
group. Each describes what the service covers and where its endpoints will
appear. They stay in v2 rather than a new unversioned section: customers
already know the v1/v2 split, so v1 drains and disappears instead of a
third concept arriving.

Every page is empty of endpoints today, by design. Endpoints arrive one at
a time as each is defined in its new service, migrated, and promoted, so
the pages fill up as the migration lands. The placeholder block is what a
generated endpoint list will replace once there is real data to generate
from -- building it now would duplicate the slug rules in
generate_api_changelog.py before we know the shape of what it renders.

Also drops the blanket-experimental framing, which the new services
contradict:

- the dropdown was labelled "v2 (Experimental)", which claims the whole
  API is experimental while the page inside it explains per-endpoint
  maturity badges and the deprecation policy promises per-endpoint notice
- the Introduction opened with "The v2 API is under active development",
  same problem; the rest of that sentence is kept, since not deprecating
  v1 until v2 covers it is still true

Not addressed here: the Introduction's maturity list still says stable
endpoints get "no breaking changes", while the deprecation policy says
they get six months' notice before one. Two different public promises on
adjacent pages, and reconciling them is an editorial call, not a
scaffolding change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The v2 Introduction told customers "No breaking changes will be made to
this API" for stable endpoints. The deprecation policy on the next page
over says stable endpoints get six months' notice *before a breaking
change* -- which concedes they happen. Two different public promises,
and the weaker one is the honest one.

Point the bullet at the policy rather than restating it, so there is one
customer-facing definition of what each maturity level owes you (which is
what the policy was written to be).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mintlify builds an endpoint's page URL from its first tag:

  /api-reference/v2/<tag-slug>/<summary-slug>

So regrouping the public API into its new services moves every affected
page, even though the path, request and response are untouched. Today's
/api-reference/v2/miscservice/check-container-health becomes
/api-reference/v2/deployments/... and the old URL 404s for anyone who
bookmarked it, linked it, or reached it from search.

Derive the redirects instead of hand-maintaining ~200 of them: diff the
committed spec against the working tree, compute both URLs with the same
slug rules the changelog already uses, and emit one redirect per
difference. Runs in the spec-sync workflow, so a tag change and its
redirect land in the same PR. docs.json joins add-paths, or the redirects
would be generated and then left behind.

Chains collapse rather than accumulate: an endpoint that moves A -> B and
later B -> C leaves A -> C, not A -> B pointing at a page that no longer
exists.

docs.json is patched textually. A json round-trip is not byte-identical
against the checked-in formatting, so it would bury a two-line change in a
whole-file diff.

Slug rules are imported from generate_api_changelog rather than copied --
they were reverse-engineered against the rendered site, and two
independent copies would drift into broken links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Services and endpoints were two separate things in the sidebar: a
"Services" group of twelve descriptions, and an autogenerated "Endpoints"
group of per-tag subgroups. Clicking a service in the second one opened
whichever endpoint happened to sort first.

Mintlify supports a `root` page on a nav group -- "clicking the group title
in the sidebar navigation opens the root page" -- but only on an explicitly
declared group, not an autogenerated one. So the endpoint list has to be
enumerated, which is why this generates it rather than asking anyone to
maintain 226 entries by hand.

One group per tag now, each with its landing page as `root` and its
endpoints as pages. The separate "Services" group is gone; its pages are
the roots. A tag with no landing page still gets a working group, so this
degrades gracefully for the services that do not have one yet.

Landing pages bind by slugged display name, not by tag name, so they
attach both before and after the endpoints are regrouped in semgrep-app.
Today's spec still carries the old service tags and yields 48 groups, 7 of
which already find their page; once the re-tagging deploys that collapses
to twelve, all with pages, without touching this code.

Runs in the spec-sync workflow after the sort step, whose ordering it
inherits, so a newly published endpoint lands in the nav in the same PR
that publishes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first version declared each service group at the dropdown's top level.
Mintlify renders a top-level group as a flat section heading, so the
services stopped being collapsible -- every endpoint of every service was
expanded at once. The collapsibility came from the autogenerated
`Endpoints` group nesting them one level down, and removing that group
removed the nesting with it.

Nest them inside a `Services` group instead. 27 other groups in this
docs.json already nest this way, so this follows the file's own pattern
rather than inventing one.

Adds a regression test asserting the groups land nested rather than
top-level, since the symptom is only visible in the rendered sidebar and
`mintlify validate` passes either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The landing pages carried a hardcoded "This service has no published
endpoints yet", which was wrong the moment the first endpoint was filed.
Each page now imports a generated table of its endpoints: method badge,
summary linked to the endpoint's reference page, and the lead sentence of
its description.

Generated per landing page rather than per spec tag, so a tag with no page
produces nothing and a page whose service is still empty gets an accurate
empty state -- generated, so it cannot go stale, which is what the
hardcoded sentence could not manage. Prose on the page stays hand-written.

Endpoint URLs come from generate_api_changelog's slug rules rather than a
second copy: those were reverse-engineered against the rendered site, and
two copies would drift into broken links.

Also fixes a URL regression this work exposed. The previous commit replaced
the autogenerated endpoint group, which carried `directory:
api-reference/v2`, with explicit groups that did not -- so Mintlify served
every endpoint from /api-reference/<tag>/ instead of
/api-reference/<version>/<tag>/. That silently moved every existing
endpoint URL and would have collided v1 with v2. `mintlify validate`
passes either way; the only thing that caught it was fetching the URLs,
where all 202 generated links 404'd. The nav generator now emits the
object form of `openapi` with `directory`.

Verified against the re-tagged spec from semgrep-app on a local dev
server: all 202 links 200, every landing page renders, and the tables show
linked summaries with descriptions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
38 of 43 entries under a service carry a maturity pill. In the narrow nav
rail the pill wraps onto its own line, roughly doubling each entry's height
and making a service look far larger than it is. Maturity is already stated
on the endpoint page and defined in the deprecation policy, so hide it in
the rail -- but keep `deprecated`, which is rare and worth interrupting for.

Hides the pill's wrapper rather than the pill: the wrapper carries `h-[1lh]`
inside a flex-wrap row, so hiding only the pill left a line-tall empty box
that wrapped below any two-line title, showing up as a stray gap under
exactly those entries.

Also turns off Mintlify's `hyphens: auto` on nav titles, which was breaking
words mid-syllable -- "Preview a detection poli-cy apply".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The step ran generate_api_nav.py over v1 as well as v2, which would have
regrouped the v1 dropdown from its flat "Endpoints" list into per-service
groups on the next sync. Nothing asked for that: v1 has no service landing
pages, so the groups would open an arbitrary endpoint, and regrouping moves
every v1 endpoint's page URL -- Mintlify builds it from the first tag.

It also meant the committed docs.json did not match what the workflow
produced, so the next run would have opened an unreviewed nav change.
With the v1 call gone, running the step reproduces the committed file
exactly.

The redirect step still covers v1. It is a no-op while v1 keeps its flat
group, and it is what would catch the URL moves if v1 is ever regrouped
deliberately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aaronmichaelacosta
aaronmichaelacosta force-pushed the APPEX-1609/v2-service-pages branch from 4d634d9 to 041f108 Compare September 21, 2026 17:10
A job is the parent category of both a scan and an Agentic Workflows job, so
the two land as one service rather than two that would have to be merged later.
Eleven service pages instead of twelve.

The snippet and the nav entry are regenerated output: jobs-endpoints.mdx shows
the empty state until the spec carries a Jobs tag, and the Scans nav group
loses its root now that no page backs it.

Pairs with the semgrep-app directory merge on APPEX-1609/new-api-v2-directory-structure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aaronmichaelacosta
aaronmichaelacosta force-pushed the APPEX-956/deprecation-policy branch from 37ee39b to adb9a7d Compare September 22, 2026 21:52

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants