fix(oas:sync): match the platform's OAS-upload output (tag index pages, root ordering, slug casing) - #35
fix(oas:sync): match the platform's OAS-upload output (tag index pages, root ordering, slug casing)#35rossrdme wants to merge 9 commits into
Conversation
oas:sync generated a different tree than uploading the same spec to ReadMe, so a repo initialized by upload drifted after a local sync: - Tag category pages (<tag>/index.md) were never generated, silently dropping the tag's description from the spec's top-level tags array. - The root reference/_order.yaml was never written, losing top-level ordering. - Slugs kept the operationId casing (getUserById.md) where the platform lowercases (getuserbyid.md), diverging URLs. - Generated pages omitted hidden: false. Generate the tag index.md (title from tag name, excerpt from tag description, never overwriting an existing one), maintain the root _order.yaml, lowercase slugs, and include hidden: false. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
WalkthroughOpenAPI synchronization now generates visible operation pages and per-tag Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/commands/oas-sync.js (1)
253-253: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider generating tag index pages even when operation pages already exist.
Because this
continueskips the rest of the loop for already-synced operations, missing tag category pages (index.md) and their corresponding_order.yamlupdates won't be backfilled for tags whose operation pages were generated in previous runs.If the goal is to fully align with the platform and ensure all tags have an
index.mdregardless of their operations' page status, consider moving the tag index generation logic (lines 268-277) and the relevant_order.yamlupdates above this check.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/oas-sync.js` at line 253, Update the operation-processing loop so tag index generation and its corresponding _order.yaml updates execute before the pagesByOpId.has(opId) early continue. Preserve the continue for already-existing operation pages, while still backfilling missing tag index.md pages and ordering entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/commands/oas-sync.js`:
- Line 257: Update the slug derivation in the operation-page generation flow
around safeSegment to reserve the "index" slug: when the normalized operation
slug equals "index", append a distinct suffix before constructing pagePath,
while preserving existing slugs for all other operations.
---
Nitpick comments:
In `@src/commands/oas-sync.js`:
- Line 253: Update the operation-processing loop so tag index generation and its
corresponding _order.yaml updates execute before the pagesByOpId.has(opId) early
continue. Preserve the continue for already-existing operation pages, while
still backfilling missing tag index.md pages and ordering entries.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: dc454943-2f4a-4b94-9bcc-52610b01a54c
📒 Files selected for processing (2)
src/commands/oas-sync.jstest/oas-sync.test.js
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
readmeio/ai(manual)readmeio/gitto(manual)readmeio/markdown(manual)readmeio/readme(manual)
| file: oasFilename, | ||
| operationId, | ||
| }, | ||
| hidden: false, |
There was a problem hiding this comment.
Why do we need to add this in here? Can we rely on the defaults in our backend?
There was a problem hiding this comment.
Good call — dropped it. Removed the explicit hidden: false from both the generated operation pages and the tag index.md, relying on the backend default. (86bd1b4)
There was a problem hiding this comment.
Correction on my earlier reply here — I'd said we'd drop this, but after testing the actual UI upload I put hidden: false back (363d3de). The upload always stamps hidden and forces false on a newly-added endpoint even when its tag and all siblings are hidden: true, so the backend isn't inferring false from a missing field that we could safely omit — omitting it would make a synced endpoint diverge from an uploaded one. Kept on both operation pages and the tag index.md. Added an @todo to honor the x-internal extension (gitto#2095 / RM-4616 / CX-3303) once it's out, at which point we can derive visibility from the spec instead of hardcoding false.
Per review on #35: - Reserve <tag>/index.md exclusively for the tag category page. An operation whose slug normalizes to "index" now gets the first free numeric slug (index-1, index-2, ...) instead of clobbering the category page. Uses numeric suffixes rather than a fixed "index-operation" suffix, which could itself collide with a real operation. - Drop the explicit `hidden: false` from generated op and tag index pages; rely on the backend default instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
not ready yet, pushed an update too quickly, standby |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/commands/oas-sync.js (1)
263-297: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winBackfill tag index pages and root order entries for existing operations.
Currently, the check
if (pagesByOpId.has(opId)) continue;at the beginning of the loop skips the generation of the tag'sindex.mdand updates to the root_order.yamlif the operation page already exists. This means users upgrading to this version won't have tag index pages generated for their existing APIs unless they also add a new operation to that tag.Consider moving the tag index and root order generation logic above the
continuestatement. This ensures the directory structure and metadata are fully synchronized for all tags in the specification on the next sync.♻️ Proposed refactor
for (const [opId, op] of specOps) { - if (pagesByOpId.has(opId)) continue; - const rawTag = op.tag || 'Other'; const tag = safeSegment(rawTag, 'Other'); const pageDir = path.join(refDir, infoTitle, tag); + + // Ensure the directory exists so index.md and _order.yaml can be safely written + fs.mkdirSync(pageDir, { recursive: true }); + + // Generate the tag's category landing page (index.md) if it doesn't exist + const indexPath = path.join(pageDir, 'index.md'); + if (!fs.existsSync(indexPath)) { + fs.writeFileSync(indexPath, buildTagIndexContent(rawTag, tagDescriptions.get(rawTag))); + changes.added.push(path.relative(refDir, indexPath)); + } + + // Maintain root and infoTitle order entries for the tag + addToOrder(path.join(refDir, infoTitle, '_order.yaml'), tag); + addToOrder(path.join(refDir, '_order.yaml'), infoTitle); + + if (pagesByOpId.has(opId)) continue; + const slug = reserveOperationSlug(pageDir, safeSegment(opId, 'operation').toLowerCase()); const pagePath = path.join(pageDir, `${slug}.md`); // Never overwrite an existing file: it belongs to a manual page, another // spec, or a different operation whose sanitized name collides with this // one. Skipping (rather than clobbering) keeps repeated syncs stable. if (!isWithin(refDir, pagePath) || fs.existsSync(pagePath)) { changes.skipped.push({ path: path.relative(refDir, pagePath), operationId: opId }); continue; } - fs.mkdirSync(pageDir, { recursive: true }); - // The tag's category landing page (index.md), like the platform generates - // on upload. Never overwrite one that already exists. - const indexPath = path.join(pageDir, 'index.md'); - if (!fs.existsSync(indexPath)) { - fs.writeFileSync(indexPath, buildTagIndexContent(rawTag, tagDescriptions.get(rawTag))); - changes.added.push(path.relative(refDir, indexPath)); - } - const content = buildPageContent({ oasFilename, operationId: opId }); fs.writeFileSync(pagePath, content); addToOrder(path.join(pageDir, '_order.yaml'), slug); - addToOrder(path.join(refDir, infoTitle, '_order.yaml'), tag); - addToOrder(path.join(refDir, '_order.yaml'), infoTitle); changes.added.push(path.relative(refDir, pagePath)); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/oas-sync.js` around lines 263 - 297, Update the specOps loop so tag directory/index generation and root/tag _order.yaml updates run for every operation, including entries already present in pagesByOpId. Move the existing metadata synchronization around pageDir, indexPath, and addToOrder above the pagesByOpId continue, while keeping new page creation and changes.added/skipped handling limited to missing operation pages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/commands/oas-sync.js`:
- Around line 263-297: Update the specOps loop so tag directory/index generation
and root/tag _order.yaml updates run for every operation, including entries
already present in pagesByOpId. Move the existing metadata synchronization
around pageDir, indexPath, and addToOrder above the pagesByOpId continue, while
keeping new page creation and changes.added/skipped handling limited to missing
operation pages.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: cdf1de0c-d18f-4089-822c-1d255e906488
📒 Files selected for processing (2)
src/commands/oas-sync.jstest/oas-sync.test.js
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
readmeio/ai(manual)readmeio/gitto(manual)readmeio/markdown(manual)readmeio/readme(manual)
…ex, keep hidden Addresses the #35 review: - hidden: keep `hidden: false` on generated pages (operation + tag index). Testing the UI upload shows it always stamps hidden and forces false on a new endpoint even when the tag/siblings are hidden, so the backend default can't be relied on. Reverts the drop in 86bd1b4. Added an @todo to honor the x-internal extension (gitto#2095) once available. - index-slug collisions: replace the tag-local check with reference-wide, folder-aware slug reservation. Reference page slugs are one flat namespace (docs/ is separate), so uniquify against every slug in reference/ — files and category folders (folder/index.md) alike — appending -1, -2, ... The reserved `index` slug always yields a numbered operation page. - backfill: generate a tag's category index.md in its own pass over every tag in the spec, so references first synced by an older CLI (op pages, no index) get their category pages on the next sync. Never overwrites an existing index; idempotent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Pushed 363d3de addressing the review. Summary of changes since the last review:
Out of scope, tracked as follow-ups: full |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/commands/oas-sync.js`:
- Around line 227-251: Replace the Set-based slug tracking in
collectReferenceSlugs with lowercase-slug reference counts so duplicate
pages/categories retain their ownership. Update the deletion logic around the
slug release at line 311 to decrement the count and remove the slug only when it
reaches zero. Add a regression covering two existing pages with the same slug
where deleting one leaves that slug reserved by the retained page.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 0688a58f-cab2-4e97-9d5e-5cdfaf317478
📒 Files selected for processing (2)
src/commands/oas-sync.jstest/oas-sync.test.js
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
readmeio/ai(manual)readmeio/gitto(manual)readmeio/markdown(manual)readmeio/readme(manual)
|
ok @erunion I believe I have addressed your concerns and coderabbit's concern. Please let me know if anything else persists. Also, I added a new PR, #37 since I noticed some issues when working on that. Lastly, we're about to ship support for the custom extension that hides endpoints, but I didn't want to address that here since it means some medium-sized changes. Thanks for all you've done thus far, I'm excited, and please let me know if this PR or #37 still needs work. |
…" folder
Verified against a repo synced by the real ReadMe platform (OAS upload): an
untagged operation's category folder is derived from its path (e.g.
`/pets/{petId}` -> `petspetid/`, with the category page's title set to the raw
path), one folder per unique path — never a single shared "Other" bucket.
oas:sync instead lumped every untagged operation into one `Other/` folder,
which the earlier commits in this PR's backfill pass made worse: it started
generating an `Other/index.md` category page even for specs whose untagged
operations already live in per-path folders on disk, producing an orphaned
index page with no member operations.
Add `path` to the operations extractOperations returns, and derive an
operation's category grouping (folder + index.md title) from its tag when
present, or its sanitized path otherwise, via the new operationGroup helper.
Tagged operations are unaffected.
… tags array
Two more fidelity gaps found while diffing CLI output against a repo synced
by the real platform (byte-for-byte, after wiping and regenerating from the
raw specs):
- matter.stringify('', frontmatter) always appends a blank body after the
closing fence, even when there's no body. The platform's generated pages
end immediately after the fence with no trailing newline. Trim it.
- Category order previously followed the order operations happen to appear
in `paths`. The platform instead keeps a declared tag in the position it
holds in the spec's own top-level `tags` array (confirmed via a spec whose
`tags` order doesn't match its `paths` order). A group with no declared
position — an untagged path-derived group, or a tag used by an operation
but never listed in `tags` — keeps its natural encounter order, appended
after every declared tag.
Verified against the real synced repo: regenerating all four specs from
scratch and diffing against the original tree, remaining differences are
now only the two open, out-of-scope items (untagged-group ordering has no
declared position to anchor to, YAML quoting of `{`-containing titles) plus
two pre-existing, unrelated artifacts (a hand-authored page relocation, and
OAS `webhooks` operations, which extractOperations doesn't read).
|
@erunion this never got merged. I went back over it last night and made some improvements on the oas-sync-upload-parity branch. I'm not sure how to proceed to get proper review. |
|
Addresses review from CodeRabbit (2026-07-17) and independently from Greptile: a Set collapses two existing owners of the same case-insensitive slug (a hand-authored page/category folder alongside a generated one, or any content that predates this uniqueness logic) into one entry. Deleting either owner's generated page then removed that single Set entry entirely, freeing the slug for reuse by a later operation in the same sync run even though the other owner still held it on disk — producing a real duplicate-slug collision. collectReferenceSlugs now returns a lowercase-slug -> owner-count Map; takeSlug/releaseSlug increment and decrement it, only fully freeing a slug once its count reaches zero. Verified the added regression test fails against the prior Set-based code (reuses the slug) and passes against this fix (suffixes instead).
…latform
Verified against a real platform upload with mixed-case tags (e.g.
"MixedInternal"): the resulting on-disk category folder was lowercased
("mixedinternal"), but the category page's title frontmatter kept the tag's
original casing. operationGroup's untagged/path branch already lowercased
its folder; the tagged branch didn't, so a mixed-case tag name produced a
folder that didn't match the platform's own output — and, more visibly,
caused the tag-order fix's declaredOrder computation (also unlowercased) to
never match groupsByFolder's keys for such a tag, silently appending a
second, differently-cased _order.yaml entry for the same folder every sync
run. No existing test used a mixed-case tag, which is why this went
unnoticed until real upload data surfaced it.
…r pages extractOperations only read spec.paths, so an OAS 3.1 spec's top-level webhooks (calls the API itself makes to a client-registered URL — a separate, same-shaped sibling of paths, not a path the client calls) were invisible to it. Two symptoms, found while validating this against a repo synced by the real platform: - oas:sync's delete pass treats any existing page whose operationId isn't in its operation set as orphaned. A webhook-backed page's operationId is never in that set, so every sync run deleted it — reproduced against a real webhooks page and confirmed the deletion happens on unmodified main. - The oas-reference lint validator reported a false "Operation not found" for the same pages. extractOperations now also walks spec.webhooks, using the same synthetic `<method>_<name>` operationId scheme already used for paths (verified it reproduces the platform's own post_paymentcompleted / post_paymentfailed convention exactly), and marks generated pages with `api.webhook: true` to match what the platform stamps on them. Grouping (tag, or the webhook's own name when untagged) and page generation fall out of the existing operationGroup/buildPageContent machinery from #35 with no special-casing. Stacked on fix/oas-sync-upload-parity (#35): untagged webhook grouping reuses that branch's path-derived-group logic, so this targets that branch rather than main. Verified end-to-end against a repo synced by the real platform: wiping and regenerating all specs from scratch now reproduces the platform's webhook pages exactly (folder, filename, api.webhook, category title from the raw webhook name) with zero remaining diff beyond already-known, out-of-scope gaps (category ordering with no declared position, YAML quoting of brace-containing titles).
Three tests hardcoded the folder "Other" (capital) for a tag literally named "Other" — accurate before the tag-lowercasing fix, stale after it (the real folder is now "other"). Passed locally on macOS regardless (APFS resolves paths case-insensitively) but correctly failed in CI on Linux, which is case-sensitive. Verified the actual on-disk folder name via readdirSync (bypassing existsSync's case-insensitive path resolution) to confirm "other" is correct before updating the assertions.
Addresses review from Greptile (with a suggested diff matching what's applied here). A legacy operation stored literally as index.md — predating the "index is reserved for the category page" convention — claims its folder's name as its slug, same as any index.md (see collectReferenceSlugs). The delete path instead released the literal string "index", which usually isn't even a reserved key in takenSlugs, leaving the real folder-name slug falsely reserved forever. A later, unrelated operation in the same sync wanting that same slug then got an unnecessary numeric suffix. Verified the added regression test fails against the prior code (the unrelated operation gets suffixed to `sometag-1.md` instead of `sometag.md`) and passes against this fix.
…r pages extractOperations only read spec.paths, so an OAS 3.1 spec's top-level webhooks (calls the API itself makes to a client-registered URL — a separate, same-shaped sibling of paths, not a path the client calls) were invisible to it. Two symptoms, found while validating this against a repo synced by the real platform: - oas:sync's delete pass treats any existing page whose operationId isn't in its operation set as orphaned. A webhook-backed page's operationId is never in that set, so every sync run deleted it — reproduced against a real webhooks page and confirmed the deletion happens on unmodified main. - The oas-reference lint validator reported a false "Operation not found" for the same pages. extractOperations now also walks spec.webhooks, using the same synthetic `<method>_<name>` operationId scheme already used for paths (verified it reproduces the platform's own post_paymentcompleted / post_paymentfailed convention exactly), and marks generated pages with `api.webhook: true` to match what the platform stamps on them. Grouping (tag, or the webhook's own name when untagged) and page generation fall out of the existing operationGroup/buildPageContent machinery from #35 with no special-casing. Stacked on fix/oas-sync-upload-parity (#35): untagged webhook grouping reuses that branch's path-derived-group logic, so this targets that branch rather than main. Verified end-to-end against a repo synced by the real platform: wiping and regenerating all specs from scratch now reproduces the platform's webhook pages exactly (folder, filename, api.webhook, category title from the raw webhook name) with zero remaining diff beyond already-known, out-of-scope gaps (category ordering with no declared position, YAML quoting of brace-containing titles).
| const slug = | ||
| path.basename(page.filePath) === 'index.md' | ||
| ? path.basename(pageDir) | ||
| : path.basename(page.filePath, '.md'); | ||
| removeFromOrder(path.join(pageDir, '_order.yaml'), slug); | ||
| releaseSlug(takenSlugs, slug); |
There was a problem hiding this comment.
Legacy index order stays stale
When a deleted legacy operation is stored as <folder>/index.md, this branch passes the folder name to removeFromOrder, even though the folder's _order.yaml records the operation as index. The page is deleted, but - index remains as a dangling order entry or incorrectly orders a subsequently recreated category page.
| const slug = | |
| path.basename(page.filePath) === 'index.md' | |
| ? path.basename(pageDir) | |
| : path.basename(page.filePath, '.md'); | |
| removeFromOrder(path.join(pageDir, '_order.yaml'), slug); | |
| releaseSlug(takenSlugs, slug); | |
| const isIndexPage = path.basename(page.filePath) === 'index.md'; | |
| const pageSlug = path.basename(page.filePath, '.md'); | |
| const referenceSlug = isIndexPage ? path.basename(pageDir) : pageSlug; | |
| removeFromOrder(path.join(pageDir, '_order.yaml'), pageSlug); | |
| releaseSlug(takenSlugs, referenceSlug); |
Problem
oas:syncproduced a different tree than uploading the same OAS spec to ReadMe, so a repo initialized by upload drifted after a local sync:<tag>/index.md) were never generated — the tag'sdescriptionfrom the spec's top-leveltagsarray was silently dropped.reference/_order.yamlwas never written, losing top-level ordering.getUserById.md) where the platform lowercases (getuserbyid.md), diverging page URLs.Changes
Tag category pages. Generate
<tag>/index.md(title = tag name, excerpt = tag description) for every tag in the spec, in its own pass — so it's created even when a tag's operation pages already exist. This backfills category pages for references first synced by an older CLI that didn't generate them, and recreates a deleted one. Never overwrites an existingindex.md(hand-written categories are safe); idempotent.Root ordering + slug casing. Maintain
reference/_order.yaml; lowercase generated slugs to match upload output.Page visibility (
hidden). Generated pages are writtenhidden: false, matching the platform's OAS upload, which always stampshiddenand forcesfalseon a new endpoint even when its tag/siblings are hidden — so the backend default can't be relied on to omit it. A@todotracks honoring thex-internalextension (gitto#2095 / RM-4616 / CX-3303) once released; the resync-side of that (re-applying x-internal to existing pages, parent hide-ratchet) is intentionally out of scope for this create-only command.Reference-wide, folder-aware slug uniqueness. Reference page slugs share one flat namespace (docs/ is a separate namespace and is never consulted). A generated slug is uniquified against every slug already in
reference/— both<slug>.mdfiles and category folders (<slug>/index.md) — appending-1,-2, … until free.indexis reserved for the tag category page, so an operation whose slug normalizes toindexalways gets a numbered page (index-1,index-2, …).Verification
reference/to just the spec, ran the patched sync, diffed against the platform's actual upload output — identical file trees, byte-identical_order.yaml, matching frontmatter.operationId: indexspec confirmed the platform itself producesindex-1/index-2, matching this PR.index.mdgets the category page on next sync; re-run reports "already in sync"._order.yaml; existing index never overwritten; lowercased slugs;indexcollision; reference-wide cross-tag uniqueness; category-folder-taken slug; missing-index backfill).Known follow-ups (out of scope, tracked)
x-internalvisibility resolution (create + resync) —@todoinbuildPageContent.index.md/ stale_order.yamlentry when a tag loses its last operation).🤖 Generated with Claude Code