feat(docs): generate a Fern-ready docs bundle and sync it to contentful-docs [NT-3942] - #442
feat(docs): generate a Fern-ready docs bundle and sync it to contentful-docs [NT-3942]#442Tim Beyer (TimBeyer) wants to merge 9 commits into
Conversation
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio. |
|
| Source | Requirement / Code Area | Status | Notes |
|---|---|---|---|
| NT-3942 | Adjust the SDK documentation pipeline to produce Fern-compatible output with the correctly restructured organization and correct links. | ✅ Met | The PR adds a Fern documentation pipeline in scripts/export-fern-docs.ts, scripts/apply-fern-docs.ts, and the scripts/fern/ modules. It transforms authored SDK documentation into Fern-compatible output, manages page organization and navigation, validates cross-document links and anchors through buildBundle and scripts/validate-fern-export.ts, and records slug redirects in documentation/fern-slugs.lock.json. |
| NT-3942 | Submit the generated Fern documentation output directly through pull requests whenever an SDK release occurs. | 🟡 Partial | The PR adds .github/workflows/sync-fern-docs.yaml, which builds the Fern output and opens or updates a pull request in the documentation repository. However, the workflow is currently triggered only by manual dispatch and does not yet run automatically whenever an SDK release occurs, so release-based submission cannot be confirmed as complete. |
| STYLE_GUIDE.md | Added general documentation style guidance about list introductions and imperative versus descriptive document titles in STYLE_GUIDE.md. These editorial standards are not part of the Fern pipeline or release synchronization requirements. | ⭕ Out of Scope | Added general documentation style guidance about list introductions and imperative versus descriptive document titles in STYLE_GUIDE.md. These editorial standards are not part of the Fern pipeline or release synchronization requirements. |
Impact Analysis by BitoCross-Repository Impact Analysis
Code Paths AnalyzedImpact: Flow: Direct Changes (Diff Files): Repository Impact: Cross-Repository Dependencies: Database/Caching Impact: API Contract Violations: Infrastructure Dependencies: Additional Insights: Testing RecommendationsFrontend Impact: Service Integration: Data Serialization: Privacy Compliance: Backward Compatibility: OAuth Functionality: Cross-Service Communication: Reliability Testing: Additional Insights: Analysis based on known dependency patterns and edges. Actual impact may vary. |
There was a problem hiding this comment.
Code Review Agent Run #94402c
Actionable Suggestions - 7
-
scripts/apply-fern-docs.ts - 1
- Empty-string bundle path silently misroutes · Line 52-52
-
.github/workflows/sync-fern-docs.yaml - 1
- Insufficient permissions declaration · Line 38-39
-
scripts/fern/build.ts - 2
- Slug validation bypassed when acceptSlugChanges is true · Line 78-92
- Extract duplicated sorting and logging pattern · Line 125-125
-
documentation/authoring/recipes/supplemental-recipe.md - 1
- Incorrect relative path · Line 23-24
-
scripts/fern/transform.ts - 2
- Incorrect line numbers in MDX safety check · Line 359-375
- Incorrect line numbers in MDX safety checks · Line 359-375
Additional Suggestions - 3
-
scripts/fern/transform.ts - 1
-
Mdx safety line-number mismatch · Line 364-370In `checkMdxSafety`, `index` inside the `forEach` over `withoutCode.split('\n')` is the position in the fence-stripped, code-stripped joined string — not the original `lines` array index. Error messages will report wrong line numbers to authors. Also: add unit tests for this function covering fenced blocks, inline code spans, and allowed/denied tags.
-
-
scripts/sdk-knowledge/markdown.ts - 1
-
Misleading variable name for array · Line 30-30The variable `inFence` holds a `boolean[]` (a fence mask), but the name implies a single `boolean` flag. This naming is consistent across all 5 call sites in this file and `transform.ts`, making the mismatch easy to miss. A descriptive name like `fenceFlags` or `fenceStatus` signals the array type.
-
-
documentation/guides/building-a-custom-javascript-optimization-adapter.md - 1
-
Duplicate description text · Line 6-8The `description` field in the frontmatter (lines 6-8) is identical to the body text at lines 13-14. Remove the redundant frontmatter field to avoid maintenance divergence.
-
Filtered by Review Rules
Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.
-
scripts/fern/build.ts - 1
- reconcileLock exception escapes reportFatal · Line 94-94
-
documentation/guides/migrating-experience-js-node-ssr-and-esr.md - 1
- Duplicate link in related guides · Line 126-126
-
documentation/concepts/consent-management-in-the-optimization-sdk-suite.md - 1
- Incorrect link text and path mismatch · Line 534-534
Review Details
-
Files reviewed - 55 · Commit Range:
5f34a84..a4f3547- .github/workflows/main-pipeline.yaml
- .github/workflows/sync-fern-docs.yaml
- .gitignore
- STYLE_GUIDE.md
- documentation/AGENTS.md
- documentation/authoring/recipes/decision.md
- documentation/authoring/recipes/integration.md
- documentation/authoring/recipes/migration.md
- documentation/authoring/recipes/supplemental-recipe.md
- documentation/concepts/AGENTS.md
- documentation/concepts/android-sdk-runtime-and-interaction-mechanics.md
- documentation/concepts/consent-management-in-the-optimization-sdk-suite.md
- documentation/concepts/core-state-management.md
- documentation/concepts/entry-personalization-and-variant-resolution.md
- documentation/concepts/interaction-tracking-in-node-and-stateless-environments.md
- documentation/concepts/interaction-tracking-in-web-sdks.md
- documentation/concepts/ios-sdk-runtime-and-interaction-mechanics.md
- documentation/concepts/locale-handling-in-the-optimization-sdk-suite.md
- documentation/concepts/optimization-handoff-and-cache-safe-rendering.md
- documentation/concepts/profile-synchronization-between-client-and-server.md
- documentation/concepts/react-native-sdk-interaction-tracking-mechanics.md
- documentation/fern-slugs.lock.json
- documentation/guides/README.md
- documentation/guides/building-a-custom-javascript-optimization-adapter.md
- documentation/guides/choosing-a-nextjs-migration-path-from-experience-js.md
- documentation/guides/choosing-the-right-sdk.md
- documentation/guides/forwarding-optimization-sdk-context-to-analytics-and-tag-management-tools.md
- documentation/guides/integrating-the-node-sdk-in-a-node-app.md
- documentation/guides/integrating-the-optimization-android-sdk-in-a-compose-app.md
- documentation/guides/integrating-the-optimization-android-sdk-in-a-views-app.md
- documentation/guides/integrating-the-optimization-ios-sdk-in-a-swiftui-app.md
- documentation/guides/integrating-the-optimization-ios-sdk-in-a-uikit-app.md
- documentation/guides/integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md
- documentation/guides/integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md
- documentation/guides/integrating-the-react-native-sdk-in-a-react-native-app.md
- documentation/guides/integrating-the-react-web-sdk-in-a-react-app.md
- documentation/guides/integrating-the-web-sdk-in-a-web-app.md
- documentation/guides/migrating-experience-js-contentful-model-to-optimization.md
- documentation/guides/migrating-experience-js-next-to-nextjs-app-router.md
- documentation/guides/migrating-experience-js-next-to-nextjs-pages-router.md
- documentation/guides/migrating-experience-js-node-ssr-and-esr.md
- documentation/guides/migrating-experience-js-plugins-and-preview.md
- documentation/guides/migrating-experience-js-react-to-react-web.md
- documentation/guides/migrating-experience-js-to-the-web-sdk.md
- documentation/guides/rendering-personalized-nextjs-routes-with-static-isr-and-edge-handoffs.md
- documentation/guides/using-contentful-graphql-data-with-the-optimization-sdks.md
- package.json
- scripts/apply-fern-docs.ts
- scripts/export-fern-docs.ts
- scripts/fern/build.ts
- scripts/fern/bundle.ts
- scripts/fern/docs.ts
- scripts/fern/transform.ts
- scripts/sdk-knowledge/markdown.ts
- scripts/validate-fern-export.ts
-
Files skipped - 0
-
Tools
- GITHUB_ADVISORY (Security Vulnerability) - ✔︎ Successful
- OWASP (Security Vulnerability) - ✔︎ Successful
- Whispers (Secret Scanner) - ✔︎ Successful
- Detect-secrets (Secret Scanner) - ✔︎ Successful
Bito Usage Guide
Commands
Type the following command in the pull request comment and save the comment.
-
/review- Manually triggers a full AI review. -
/pause- Pauses automatic reviews on this pull request. -
/resume- Resumes automatic reviews. -
/resolve- Marks all Bito-posted review comments as resolved. -
/abort- Cancels all in-progress reviews.
Refer to the documentation for additional commands.
Configuration
This repository uses Default Agent You can customize the agent settings here or contact your Bito workspace admin at jared.jolton@contentful.com.
Documentation & Help
✅ Review Settings OverriddenStatus: Guidelines:
Note: Extra guidelines beyond 3 general purpose guidelines and 1 language specific guideline per language are not processed. Guidelines are fetched from the source branch. |
a4f3547 to
f68566c
Compare
Code Review Agent Run #7c72f9Actionable Suggestions - 0Additional Suggestions - 7
Filtered by Review RulesBito filtered these suggestions based on rules created automatically for your feedback. Manage rules.
Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
|
Reviewed the non-threaded Bito output against the current head and repository authoring rules. No code changes are warranted for those suggestions:
The MDX line-number observation is also non-actionable here. Fence lines retain their positions; other intentional transformations already make these transformed-document diagnostics rather than a guaranteed source map. Unsafe markup is still rejected and generated output is unaffected. Exact authored-source locations would require provenance across every transform, not either suggested local rewrite. The generated impact summaries also contain factual mismatches: there is no All seven inline findings have evidence replies on their originating threads and have been resolved directly. |
The Compose and React Web integration guides ended with `</content>` and `</invoke>` tags left behind by an authoring agent. They render as literal text on GitHub and would break MDX compilation once these guides publish to the documentation site, so the tech writers were stripping them by hand on every copy. Also tag two code fences that were unlabelled or used a non-canonical alias, so every fence in the published set carries a language the site recognises. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The authoring pipeline was producing gerund titles ("Integrating the...") because it was
told to. `recipes/integration.md` mandated the H1 form literally, and the decision,
migration, and supplemental recipes stated no title form at all, so authors defaulted to
the same shape. STYLE_GUIDE.md covered imperative mood for instructions and for
procedure steps, but said nothing about the title of a document.
Give the rule one home in STYLE_GUIDE.md: a task document takes an imperative title
naming the task, an explanatory document takes a descriptive noun phrase, and neither
takes a gerund. All four recipes now defer to it instead of restating or omitting it,
and the concepts guide states the noun-phrase counterpart so the two archetypes cannot
drift into each other.
Two related rules the pipeline was also missing: a document title is its published page
title and sidebar label, so cross-document link text naming it must change with it; and
a concept closes with `## Related documentation` under exactly that name, which had
three spellings across eleven files with nothing governing it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tion name Add the `fern:` frontmatter block each concept needs to publish: slug, sidebar section, and meta description. The published page title is the document's `#` heading, so there is no title key here and nothing for the title to drift from. Update the link text that names a guide, which is now imperative, and rename the closing section to `## Related documentation` in the four concepts that spelled it `Related docs` or `Related docs and reference implementations`. The TOC entries and the five in-page anchors pointing at the old headings move with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Apply the title rule to the 23 guides that carried gerund headings: 23 headings and the link text that names them across the guide set. The four guides with an authored frontmatter `title` have it realigned to their heading, which `documentation/AGENTS.md` already required. Add the `fern:` block each guide needs to publish: slug, sidebar section, and meta description, plus `navTitle` on the two guides whose sidebar label is deliberately shorter than their page title. Frontmatter and heading land together because in the nineteen guides that had no frontmatter they are adjacent lines in the same diff hunk. Slugs are recorded rather than derived. Rewording a heading changes the published title, which is correct; it must never silently move a live URL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replace the hand-copy step into contentful-docs with a deterministic transform. It owns structure, links, and metadata, and never prose: the wording in `documentation/` stays the single source of truth, and a transform that rewrote sentences would make the two copies impossible to diff. Three layers, so the first two need no credentials and can be iterated on locally: - `pnpm docs:fern` builds the pages, the navigation block, and the redirects into a gitignored `fern-bundle/`. - `pnpm docs:fern:apply -- --docs-repo <path>` splices that bundle into a local contentful-docs checkout, so the output can be checked with that repo's own gates before any cross-repo automation is involved. Navigation and redirects are spliced, never regenerated, because that file also holds hand-maintained Personalization content this repo does not own. - `pnpm fern:check` runs the same pipeline without writing, and is what makes the link rot the manual process accumulated impossible to reintroduce: every cross-document link must resolve to a published page, every `#fragment` must match a real heading, no page may link into `authoring/` or `internal/`, the MDX must be safe, and a slug may not move without a recorded redirect. Every line-based pass is fence-aware, reusing `headingsOf` from the existing knowledge validators, because a Swift `#if DEBUG` and a JSX `<h1>` both appear inside fenced blocks in these guides and neither is prose. Slugs live in a lock file. A changed slug needs `--update-lock`, which appends a permanent redirect; old URLs have to keep resolving, so that list accumulates and is never pruned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Run `pnpm fern:check` in the knowledge-check job, and add
`documentation/concepts/**` to the filter that triggers it. Concepts were absent from
that filter, so a concepts-only change ran no documentation validation at all.
Add `sync-fern-docs.yaml` to open the pull request against contentful-docs. Manual
dispatch only for now, but built so a release trigger is additive rather than a rewrite:
ref-parameterized, idempotent against one fixed branch and one upserted pull request,
and concurrency-guarded. That last part matters because merging the grouped release pull
request creates one GitHub release per component, so `on: release` fires several times
for a single release moment and those firings have to converge on one pull request.
Follows the constraints contentful-docs sets for its own workflows: no `${{ }}`
interpolation inside a `run:` block, and no third-party actions beyond the SHA-pinned
ones this repo already uses. Needs a `FERN_DOCS_SYNC_TOKEN` secret scoped to that repo;
a GitHub App installation token is preferable to a long-lived PAT.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
f68566c to
871b417
Compare
The repository gained an architecture decision record practice in 4d2e37f, and generating the public documentation site content from this repository is architecture-significant: it introduces a cross-repository integration, turns pages in contentful-docs into generated output, and changes who owns the wording on the published site. Records the decision, and the alternatives that were considered and rejected along the way, including the ones that were tried first and then reversed: an explicit `fern.title`, deriving slugs from headings, holding site metadata in a single manifest, putting the transform in contentful-docs, and publishing on every documentation merge rather than on release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review Agent Run #492cb5
Actionable Suggestions - 3
-
scripts/export-fern-docs.ts - 1
- Unsafe output directory deletion · Line 75-77
-
scripts/apply-fern-docs.ts - 1
- Nav splice may remove non-list content · Line 90-90
-
scripts/fern/bundle.ts - 1
- Redirect cycle causes infinite loop · Line 48-59
Additional Suggestions - 10
-
scripts/export-fern-docs.ts - 1
-
CWE-22: Destructive Path Deletion · Line 75-76`--out` is resolved via `path.resolve(rootDir, options.outDir)` and then `rmSync(outDir, { recursive: true, force: true })` deletes it. Passing `--out .` or `--out ..` resolves to `rootDir` or a parent, recursively deleting the whole repo before the bundle is written. Guard that `outDir` is a strict subdirectory of `rootDir` before the destructive call. ([CWE-22](https://cwe.mitre.org/data/definitions/22.html))
-
-
.github/workflows/sync-fern-docs.yaml - 1
-
Draft input ignored on update · Line 133-135The `draft` input is only honored on the initial `gh pr create`. On re-runs — the designed convergence path — `gh pr edit` (line 134) never passes `--draft`/`--no-draft`, so the input is silently ignored and the existing PR keeps its prior draft state. Consider applying the flag on the update branch too.
-
-
documentation/AGENTS.md - 1
-
Inaccurate publish command · Line 26-27`pnpm docs:fern` (`scripts/export-fern-docs.ts`) only writes a local bundle to `fern-bundle/`; it does not publish. Applying the bundle to a `contentful-docs` checkout is the separate `pnpm docs:fern:apply` step (`scripts/apply-fern-docs.ts`, "Layer 2 of the sync"). A contributor following this would believe they published when they only built locally.
-
-
documentation/concepts/android-sdk-runtime-and-interaction-mechanics.md - 1
-
Doc link text inconsistency · Line 21-21The link text "XML Views app" doesn't match the target guide's H1, which is "Integrate the Optimization Android SDK in an Android Views app". The repo's dominant convention for this guide (`core-state-management.md`, `locale-handling-in-the-optimization-sdk-suite.md`, `choosing-the-right-sdk.md`) uses "Android Views app". Consider aligning both occurrences (line 21 and 343) for consistency.
-
-
documentation/concepts/entry-personalization-and-variant-resolution.md - 2
-
Link label mismatch · Line 710-711These two rewritten labels drop the "Optimization" prefix that the other nine links in this list and the target guide titles include. `integrating-the-react-native-sdk-in-a-react-native-app.md` and `integrating-the-node-sdk-in-a-node-app.md` both title themselves "Integrate the Optimization ... SDK". Align the labels for consistency.
-
Link label mismatch · Line 716-716The rewritten label says "XML Views app", but the target `integrating-the-optimization-android-sdk-in-a-views-app.md` titles itself "Integrate the Optimization Android SDK in an Android Views app". The modifier no longer matches the page it links to.
-
-
scripts/sdk-knowledge/markdown.ts - 1
-
Anchor logic duplicated · Line 46-55`headingAnchor` duplicates the anchor logic already present as `slug` in `scripts/validate-guide-authoring.ts` (line 78), and the two diverge: `## [Quick Start](quickstart.md)` yields `quick-start` here but `-quick-startquickstartmd` in `slug`, and `_` is stripped here but kept there. Both scripts validate heading anchors for links, so this is a divergence risk. Consider sharing one implementation.
-
-
documentation/authoring/recipes/decision.md - 1
-
Line exceeds 100-char limit · Line 21-21Line 21 is 121 characters, exceeding the repo's `MD013` limit of 100 set in `.markdownlint.yaml`. Reflow the prose so no line exceeds 100 chars (e.g. put the two example titles on their own line).
-
-
documentation/concepts/consent-management-in-the-optimization-sdk-suite.md - 1
-
Link text hyphenation mismatch · Line 554-554The link text here reads "tag management tools", but the target page title is "Forward Optimization SDK context to analytics and tag-management tools" (hyphenated), and the sibling file `interaction-tracking-in-web-sdks.md:535` uses the hyphenated form. Align the text for consistency.
-
-
documentation/guides/integrating-the-optimization-android-sdk-in-a-views-app.md - 1
-
Link text/title mismatch · Line 976-976The new link text "tag management tools" doesn't match the target guide's H1, which is "Forward Optimization SDK context to analytics and tag-management tools" (hyphenated). Since this PR's intent is to align link text with titles, update the text to "tag-management tools" for consistency.
-
Review Details
-
Files reviewed - 55 · Commit Range:
10cd407..871b417- .github/workflows/main-pipeline.yaml
- .github/workflows/sync-fern-docs.yaml
- .gitignore
- STYLE_GUIDE.md
- documentation/AGENTS.md
- documentation/authoring/recipes/decision.md
- documentation/authoring/recipes/integration.md
- documentation/authoring/recipes/migration.md
- documentation/authoring/recipes/supplemental-recipe.md
- documentation/concepts/AGENTS.md
- documentation/concepts/android-sdk-runtime-and-interaction-mechanics.md
- documentation/concepts/consent-management-in-the-optimization-sdk-suite.md
- documentation/concepts/core-state-management.md
- documentation/concepts/entry-personalization-and-variant-resolution.md
- documentation/concepts/interaction-tracking-in-node-and-stateless-environments.md
- documentation/concepts/interaction-tracking-in-web-sdks.md
- documentation/concepts/ios-sdk-runtime-and-interaction-mechanics.md
- documentation/concepts/locale-handling-in-the-optimization-sdk-suite.md
- documentation/concepts/optimization-handoff-and-cache-safe-rendering.md
- documentation/concepts/profile-synchronization-between-client-and-server.md
- documentation/concepts/react-native-sdk-interaction-tracking-mechanics.md
- documentation/fern-slugs.lock.json
- documentation/guides/README.md
- documentation/guides/building-a-custom-javascript-optimization-adapter.md
- documentation/guides/choosing-a-nextjs-migration-path-from-experience-js.md
- documentation/guides/choosing-the-right-sdk.md
- documentation/guides/forwarding-optimization-sdk-context-to-analytics-and-tag-management-tools.md
- documentation/guides/integrating-the-node-sdk-in-a-node-app.md
- documentation/guides/integrating-the-optimization-android-sdk-in-a-compose-app.md
- documentation/guides/integrating-the-optimization-android-sdk-in-a-views-app.md
- documentation/guides/integrating-the-optimization-ios-sdk-in-a-swiftui-app.md
- documentation/guides/integrating-the-optimization-ios-sdk-in-a-uikit-app.md
- documentation/guides/integrating-the-optimization-sdk-in-a-nextjs-app-router-app.md
- documentation/guides/integrating-the-optimization-sdk-in-a-nextjs-pages-router-app.md
- documentation/guides/integrating-the-react-native-sdk-in-a-react-native-app.md
- documentation/guides/integrating-the-react-web-sdk-in-a-react-app.md
- documentation/guides/integrating-the-web-sdk-in-a-web-app.md
- documentation/guides/migrating-experience-js-contentful-model-to-optimization.md
- documentation/guides/migrating-experience-js-next-to-nextjs-app-router.md
- documentation/guides/migrating-experience-js-next-to-nextjs-pages-router.md
- documentation/guides/migrating-experience-js-node-ssr-and-esr.md
- documentation/guides/migrating-experience-js-plugins-and-preview.md
- documentation/guides/migrating-experience-js-react-to-react-web.md
- documentation/guides/migrating-experience-js-to-the-web-sdk.md
- documentation/guides/rendering-personalized-nextjs-routes-with-static-isr-and-edge-handoffs.md
- documentation/guides/using-contentful-graphql-data-with-the-optimization-sdks.md
- package.json
- scripts/apply-fern-docs.ts
- scripts/export-fern-docs.ts
- scripts/fern/build.ts
- scripts/fern/bundle.ts
- scripts/fern/docs.ts
- scripts/fern/transform.ts
- scripts/sdk-knowledge/markdown.ts
- scripts/validate-fern-export.ts
-
Files skipped - 0
-
Tools
- OWASP (Security Vulnerability) - ✔︎ Successful
- GITHUB_ADVISORY (Security Vulnerability) - ✔︎ Successful
- Whispers (Secret Scanner) - ✔︎ Successful
- Detect-secrets (Secret Scanner) - ✔︎ Successful
Bito Usage Guide
Commands
Type the following command in the pull request comment and save the comment.
-
/review- Manually triggers an incremental AI Review. -
/review full- Manually triggers a full AI Review. -
/pause- Pauses automatic reviews on this pull request. -
/resume- Resumes automatic reviews. -
/resolve- Marks all Bito-posted review comments as resolved. -
/abort- Cancels all in-progress reviews.
Refer to the documentation for additional commands.
Configuration
This repository uses Default Agent You can customize the agent settings here or contact your Bito workspace admin at jared.jolton@contentful.com.
Documentation & Help
| const outDir = path.resolve(rootDir, options.outDir) | ||
| rmSync(outDir, { force: true, recursive: true }) | ||
| mkdirSync(path.join(outDir, 'pages'), { recursive: true }) |
There was a problem hiding this comment.
The --out option is resolved with path.resolve(rootDir, options.outDir) and then passed directly to rmSync(outDir, { force: true, recursive: true }). If a user passes --out ., --out .., or --out "", the resolved path becomes the repository root or its parent, and rmSync would recursively delete the entire repository. Consider adding a guard that refuses to delete the repository root or any ancestor directory.
Code suggestion
Check the AI-generated fix before applying
| const outDir = path.resolve(rootDir, options.outDir) | |
| rmSync(outDir, { force: true, recursive: true }) | |
| mkdirSync(path.join(outDir, 'pages'), { recursive: true }) | |
| const outDir = path.resolve(rootDir, options.outDir) | |
| if (outDir === rootDir || rootDir.startsWith(`${outDir}${path.sep}`)) { | |
| throw new Error(`refusing to write to ${outDir}: would delete the repository root or a parent directory`) | |
| } | |
| rmSync(outDir, { force: true, recursive: true }) | |
| mkdirSync(path.join(outDir, 'pages'), { recursive: true }) |
Suggested based on your custom review guideline "Review Posture"
Code Review Run #492cb5
Should Bito avoid suggestions like this for future reviews? (Manage Rules)
- Yes, avoid them
| continue | ||
| } | ||
| const lineIndent = line.length - line.trimStart().length | ||
| if (lineIndent <= indent && line.trimStart().startsWith('- ')) { |
There was a problem hiding this comment.
The end-detection loop in spliceNavSection only stops at the next list item (- ). If a comment or a non-list top-level key (e.g., tabs:) appears between the section and the next list item, it is skipped and then removed by the splice. Consider stopping at the first non-blank, non-comment line at the same or lower indentation.
Suggested based on your custom review guideline "Review Posture"
Code Review Run #492cb5
Should Bito avoid suggestions like this for future reviews? (Manage Rules)
- Yes, avoid them
| for (const doc of docs) { | ||
| const { fern, relPath } = doc | ||
| const { slug } = fern | ||
| const { [relPath]: previous } = slugs | ||
| if (previous !== undefined && previous !== slug) { | ||
| const exists = redirects.some((entry) => entry.from === previous && entry.to === slug) | ||
| if (!exists) { | ||
| redirects.push({ from: previous, to: slug }) | ||
| } | ||
| } | ||
| slugs[relPath] = slug | ||
| } |
There was a problem hiding this comment.
The redirect handling can produce an infinite redirect loop when a page's slug changes back to a previously used value. For example, if a page moves A -> B and then B -> A, reconcileLock appends both A -> B and B -> A to the redirect list. renderRedirects then emits both entries because resolveRedirectTarget detects the cycle but returns the last non-cyclic target (B for the A entry, A for the B entry), so neither destination === entry.from check trips. A user visiting /optimization-sdk/A/ gets bounced to /B/, then back to /A/, forever. The exists guard in reconcileLock only blocks exact duplicates, not cycles.
Code suggestion
Check the AI-generated fix before applying
| for (const doc of docs) { | |
| const { fern, relPath } = doc | |
| const { slug } = fern | |
| const { [relPath]: previous } = slugs | |
| if (previous !== undefined && previous !== slug) { | |
| const exists = redirects.some((entry) => entry.from === previous && entry.to === slug) | |
| if (!exists) { | |
| redirects.push({ from: previous, to: slug }) | |
| } | |
| } | |
| slugs[relPath] = slug | |
| } | |
| for (const doc of docs) { | |
| const { fern, relPath } = doc | |
| const { slug } = fern | |
| const { [relPath]: previous } = slugs | |
| if (previous !== undefined && previous !== slug) { | |
| // Drop the reverse redirect if it exists, so a slug revert does not create a cycle. | |
| const cycleIndex = redirects.findIndex((entry) => entry.from === slug && entry.to === previous) | |
| if (cycleIndex !== -1) { | |
| redirects.splice(cycleIndex, 1) | |
| } | |
| const exists = redirects.some((entry) => entry.from === previous && entry.to === slug) | |
| if (!exists) { | |
| redirects.push({ from: previous, to: slug }) | |
| } | |
| } | |
| slugs[relPath] = slug | |
| } |
Suggested based on your custom review guideline "Review Posture"
Code Review Run #492cb5
Should Bito avoid suggestions like this for future reviews? (Manage Rules)
- Yes, avoid them
Code Review Agent Run #8b234fActionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
|
I (Codex) reviewed the two latest automated review runs against the current head ( Three findings are valid, realistic, and worth addressing before merge:
The following suggestions are also valid, but lower priority:
I do not consider the All current CI checks are green, but they do not exercise the three failure histories above. |
Three failure histories from review that the current tests do not reach. **A caller-supplied output path could delete the working tree.** `--out` resolved against the repository root and went straight into a recursive delete, so `--out .` or `--out ..` removed the repository or its parent. Requiring the path to sit inside the repository does not fix this: `--out documentation` is inside it and would delete the authored docs. Nothing consumed a configurable location — not the sync workflow, not the apply step, not once in practice — so the option is gone and the bundle directory is a fixed path this script owns. Cleanup now removes the members a previous build wrote rather than the directory, so no recursive delete takes a caller's input at all. To compare two builds, copy the directory aside between runs. **Navigation splicing could delete content this repo does not own.** The owned span ended at the next sibling `- ` list item, so a comment banner at the section's indentation, or a trailing top-level key such as `tabs:` or `landing-page:`, fell inside the replaced range. With the Optimization SDK section last in the list, that consumed everything to end of file — 127 lines in a reproduction. The span now ends at the first nonblank line indented no deeper than the section, whatever that line is. **A restored slug pointed the live page away from itself.** The recorded history is append-only, so moving `A` to `B` and later back to `A` kept both edges and emitted both, which is a redirect loop and, worse, sends the published `A` somewhere else. Published slugs are now terminal: a slug that is live again is a page, not a redirect source, so its recorded move is skipped. Longer histories collapse the same way — `A -> B -> C -> A` publishes `B -> A` and `C -> A` — rather than special- casing the direct reverse edge. A history that never reaches a published slug now fails `pnpm fern:check` instead of silently emitting nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**One anchor implementation, and the correct one.** `headingAnchor` collapsed runs of whitespace to a single hyphen while the validator's own local helper replaced each whitespace character. They disagreed on 87 headings in the knowledge base and authoring templates. The local helper was right: dropping the punctuation from `## Render / entry resolution` leaves two adjacent spaces, and 415 authored links spell that anchor `#render--entry-resolution`. `headingAnchor` now matches, keeps its handling of inline code, links, and emphasis that the local helper lacked, and is the single implementation both `pnpm fern:check` and `pnpm guides:check` use. The published set was unaffected either way, so the anchor count is unchanged. **Link labels that read as titles now match one.** Twelve labels named a document but not by its title: `XML Views app` for a guide titled `Android Views app`, Node and React Native labels missing `Optimization`, and `tag management tools` missing the title's hyphen. The style guide already asks for the exact document title when a label names one; a shortened contextual phrase stays fine, but a label that looks like a full title should be one. Also: `documentation/AGENTS.md` described `pnpm docs:fern` as publishing, when it only builds the local bundle — applying it and opening the cross-repository pull request are separate steps. And one over-long line in the decision recipe is rewrapped to the checked-in 100-column limit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Code Review Agent Run #2c0f1cActionable Suggestions - 0Additional Suggestions - 3
Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
Closes NT-3942.
Today the Optimization SDK documentation is hand-copied into
contentful/contentful-docs: links rewritten, pages restructured, slugs renamed by hand. This replaces that step with a deterministic transform, and fixes the authoring instructions that caused the divergence in the first place.Why
The manual process has measurably drifted. On the live site right now:
integrate-the-node-sdk-in-a-node-app(×15 — the real slug is…into-a-node-app) andintegrate-the-optimization-android-sdk-in-a-compose-app(×9 — the nav declaresintegrate-the-android-sdk-in-a-compose-app).…/main/implementationsreact-native-sdk/README.md.…/entry-personalization-and-variant-resolution#single-locale-cda-entry-contract/.<Info>, so the one realWARNING(native SDKs are in beta) lost it.description, and one slug is a typo (builde-a-custom-…).It is also lossy, which is the part worth flagging:
isEmptyVariantappears 9 times andslugField7 times in these guides, and zero times on the live site. The Node SDK's slug-based fetching and empty-variant handling are documented here but missing in public, even though both landed days before the last manual sync.What this adds
Three layers, so the first two need no credentials and can be iterated on locally:
pnpm docs:fernfern-bundle/pnpm docs:fern:apply -- --docs-repo <path>contentful-docscheckoutpnpm fern:checkThe transform owns structure, links, and metadata, and never prose — the wording in
documentation/stays the source of truth, and a transform that rewrote sentences would make the two copies impossible to diff.fern:checkis what makes the link rot unrepeatable: every cross-document link must resolve to a published page, every#fragmentmust match a real heading, no page may link intoauthoring/orinternal/, the MDX must be safe, and a slug cannot move without a recorded redirect. Every line-based pass is fence-aware (reusingheadingsOffrom the existing knowledge validators), because a Swift#if DEBUGand a JSX<h1>both appear inside fenced blocks here and neither is prose.The instruction fix
The gerund titles were not an accident —
recipes/integration.mdmandated the H1 form literally, and the decision, migration, and supplemental recipes stated no title form at all.STYLE_GUIDE.mdcovered imperative mood for instructions and procedure steps, but nothing about document titles.So the rule now has one home in
STYLE_GUIDE.md(task documents take imperative titles, explanatory documents descriptive noun phrases, neither takes a gerund), all four recipes defer to it, andconcepts/AGENTS.mdstates the counterpart. Then applied: 23 headings and 152 link texts. The published title is now the heading itself, with no second key to drift from it.Same class of gap, found on the way: concepts used three different names for one closing section with nothing governing it. Normalized to
## Related documentationand the rule written down.Verification
Applied end to end against a real
contentful-docscheckout, then reset:fern check— 0 errors (3 warnings, all pre-existing in unrelatedexperience-orchestration/pages)check-orphan-pages— 490/490 registered ·check-redirects— 409 valid ·markdownlint— 0 errorsIn this repo:
fern:check,guides:check,knowledge:check,eslint,prettier,tscall clean.Reviewing this
Six commits, each one concern, meant to be read in order.
docs(docs): give guides imperative titles…is the large one but its diff is uniform and mechanical.Not in this PR, and needing a human
The golden diff (generated bundle vs the 35 live pages) leaves 29 items across 14 pages that need editorial judgement, not code. The ones that matter are 9 where the site has content this repo does not — they would be destroyed by a sync unless back-ported first:
<Info>callouts the writers added tobuild-a-custom-…,choose-a-nextjs-migration-path, andconsent-managementforwardedSemanticInteractionscode block and a consent bullet list in the forwarding guideTwo judgement calls rather than clear wins: the site dropped a definitional clause from
states.selectedOptimizations, and itscore-state-managementintro reads better than this repo's. Separately, the site renamed## Related guides and concepts→Related documentation, which conflicts with whatsupplemental-recipe.mdmandates — I left the recipe's version standing, since that is an editorial call.Before this can run
FERN_DOCS_SYNC_TOKENsecret scoped tocontentful-docs(contents: write,pull-requests: write). A GitHub App installation token is preferable to a long-lived PAT. A PR from a branch in that repo also gets the Fern preview URL; a fork PR cannot, because forks cannot read secrets.contentful-docs: its.bito/guidelinessay "do not add SDK generation workflows", andCODEOWNERSrequires review from both technical-writing and developer-experience.children:lists, which are reader-routing order (Choose the right SDK → Node → Web → React → Next.js). The live site is alphabetical. Deliberate, and worth a look before merge.Decision record
docs/ADRs/0002-generate-the-public-documentation-site-content-from-this-repository.mdrecords thisdecision in the practice
4d2e37f3introduced, including the alternatives rejected along the way andthe three that were tried first and then reversed: an explicit
fern.title, deriving slugs fromheadings, and holding site metadata in a single manifest.
It also states two consequences that outlive this PR: pages under
fern/docs/pages/personalization/optimization-sdk/become generated output, andcontentful-docsaccepting generated content from another repository is a change to how that repository works, which
is its maintainers' decision to record rather than ours.
Rebase notes
Rebased onto
mainat 32 commits (2026-08-31). One conflict, inreact-native-sdk-interaction-tracking-mechanics.md:ddca28dereplaced the untagged code fence myfirst commit was tagging with a table, because the interaction model became start/end only. Main's
version wins and that part of the commit is now moot. Everything else applied clean.
Because most of the 19 overlapping docs auto-merged, I re-swept the whole published set afterward for
content main could have introduced past the merge — gerund titles or link text,
Relatedheadingvariants, frontmatter drifted from its heading, missing
fern:blocks, non-canonical fences, MDXhazards. All zero.
guides:checkstill agrees blueprint-to-guide across the 9 blueprints mainchanged, and
knowledge:checknow covers 1511 source pointers (was 1424).A correction to the numbers above. They previously read 53 links across 13 slugs. The real
figure is 52 across 12: my measurement script mis-associated slugs for the four nav entries that
order their keys
page:/slug:/path:instead ofpage:/path:/slug:, and over-counted by one.The generated nav emits a fixed key order, which is why the corrected and original parsers agree on
this PR's output but disagree on the hand-maintained file.
A second manual sync landed meanwhile (
3eca89b, contentful-docs #214, 2026-08-19) carrying theAndroid, iOS, and React Native updates across by hand. It changed no slugs, so the lock and redirect
here are unaffected. It also moved none of the drift metrics: still 52 broken links, 35 gerund
titles or link texts, 16 malformed GitHub URLs, 7 anchors broken by a trailing slash — before and
after. The manual process is neither fixing nor worsening this on its own.
🤖 Generated with Claude Code