Skip to content

feat(docs): generate a Fern-ready docs bundle and sync it to contentful-docs [NT-3942] - #442

Open
Tim Beyer (TimBeyer) wants to merge 9 commits into
mainfrom
feat/fern-docs-sync-nt-3942
Open

feat(docs): generate a Fern-ready docs bundle and sync it to contentful-docs [NT-3942]#442
Tim Beyer (TimBeyer) wants to merge 9 commits into
mainfrom
feat/fern-docs-sync-nt-3942

Conversation

@TimBeyer

@TimBeyer Tim Beyer (TimBeyer) commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

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:

  • 52 internal links point at 12 slugs that do not exist. Worst: integrate-the-node-sdk-in-a-node-app (×15 — the real slug is …into-a-node-app) and integrate-the-optimization-android-sdk-in-a-compose-app (×9 — the nav declares integrate-the-android-sdk-in-a-compose-app).
  • 13 malformed GitHub URLs where a find-replace ate a slash: …/main/implementationsreact-native-sdk/README.md.
  • Deep links are broken by appending the trailing slash after the fragment: …/entry-personalization-and-variant-resolution#single-locale-cda-entry-contract/.
  • Severity was flattened — all three GitHub alert types became <Info>, so the one real WARNING (native SDKs are in beta) lost it.
  • Two pages ship an empty description, and one slug is a typo (builde-a-custom-…).

It is also lossy, which is the part worth flagging: isEmptyVariant appears 9 times and slugField 7 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:

Command Does
pnpm docs:fern Builds pages, the nav block, and redirects into a gitignored fern-bundle/
pnpm docs:fern:apply -- --docs-repo <path> Splices a bundle into a local contentful-docs checkout
pnpm fern:check Runs the same pipeline without writing; gates CI

The 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:check is what makes the link rot unrepeatable: 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 cannot 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 here and neither is prose.

The instruction fix

The gerund titles were not an accident — recipes/integration.md mandated the H1 form literally, and the decision, migration, and supplemental recipes stated no title form at all. STYLE_GUIDE.md covered 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, and concepts/AGENTS.md states 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 documentation and the rule written down.

Verification

Applied end to end against a real contentful-docs checkout, then reset:

  • fern check0 errors (3 warnings, all pre-existing in unrelated experience-orchestration/ pages)
  • check-orphan-pages — 490/490 registered · check-redirects — 409 valid · markdownlint — 0 errors
  • Broken internal links: 52 → 0

In this repo: fern:check, guides:check, knowledge:check, eslint, prettier, tsc all 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 to build-a-custom-…, choose-a-nextjs-migration-path, and consent-management
  • a forwardedSemanticInteractions code block and a consent bullet list in the forwarding guide

Two judgement calls rather than clear wins: the site dropped a definitional clause from states.selectedOptimizations, and its core-state-management intro reads better than this repo's. Separately, the site renamed ## Related guides and conceptsRelated documentation, which conflicts with what supplemental-recipe.md mandates — I left the recipe's version standing, since that is an editorial call.

Before this can run

  • A FERN_DOCS_SYNC_TOKEN secret scoped to contentful-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.
  • Likely an ADR in contentful-docs: its .bito/guidelines say "do not add SDK generation workflows", and CODEOWNERS requires review from both technical-writing and developer-experience.
  • Sidebar order changes. Order now comes from the README 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.md records this
decision in the practice 4d2e37f3 introduced, including the alternatives rejected along the way and
the three that were tried first and then reversed: an explicit fern.title, deriving slugs from
headings, 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, and contentful-docs
accepting 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 main at 32 commits (2026-08-31). One conflict, in
react-native-sdk-interaction-tracking-mechanics.md: ddca28de replaced the untagged code fence my
first 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, Related heading
variants, frontmatter drifted from its heading, missing fern: blocks, non-canonical fences, MDX
hazards. All zero. guides:check still agrees blueprint-to-guide across the 9 blueprints main
changed, and knowledge:check now 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 of page:/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 the
Android, 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

@wiz-inc-38d59fb8d7

wiz-inc-38d59fb8d7 Bot commented Aug 18, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 1 Medium
Software Management Finding Software Management Findings -
Total 1 Medium

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

@bito-code-review

bito-code-review Bot commented Aug 18, 2026

Copy link
Copy Markdown

Functional Validation by Bito

SourceRequirement / Code AreaStatusNotes
NT-3942Adjust the SDK documentation pipeline to produce Fern-compatible output with the correctly restructured organization and correct links.✅ MetThe 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-3942Submit the generated Fern documentation output directly through pull requests whenever an SDK release occurs.🟡 PartialThe 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.mdAdded 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 ScopeAdded 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.

@bito-code-review

bito-code-review Bot commented Aug 18, 2026

Copy link
Copy Markdown

Impact Analysis by Bito

Cross-Repository Impact Analysis
What Changed Impact of Change Suggested Review Actions
Added deterministic Fern export and a workflow that applies generated documentation to contentful/contentful-docs. - ⚠️ unverified — contentful-docs/fern/docs/pages/personalization/optimization-sdk: generated output CONSUMES optimization documentation bundle: The workflow checks out contentful-docs, applies the generated bundle, force-pushes a sync branch, and creates or updates a pull request. - Verify the FERN_DOCS_SYNC_TOKEN is scoped only to the required contentful-docs repository permissions.
- Run the workflow against a test ref and confirm branch reuse, pull-request idempotency, and no unintended files are modified.
Introduced stable Fern frontmatter and slug-lock behavior for published documents. - ⚠️ unverified — contentful-docs/public documentation URLs CONSUMES optimization documentation fern.slug values: Changing a slug now requires an explicit lock update and generated redirect; changing headings alone should not move live URLs. - Test an unchanged slug, an intentionally changed slug with --update-lock, and an unrecorded slug change.
- Verify generated redirects preserve all existing public URLs.
Code Paths Analyzed

Impact:
The change establishes authored documentation as the source of truth, adds deterministic Fern transformation and validation tooling, and automates synchronization into contentful-docs.

Flow:
Authored Markdown with fern frontmatter → document discovery and parsing → title, link, TOC, callout, and metadata transformation → slug and redirect validation → bundle generation → optional application to contentful-docs → branch push and pull-request creation.

Direct Changes (Diff Files):
• .github/workflows/main-pipeline.yaml [3-160] — Expands documentation-related validation paths and adds Fern export validation coverage.
• .github/workflows/sync-fern-docs.yaml [1-174] — Adds ref-parameterized, concurrency-guarded cross-repository documentation synchronization.
• .gitignore [178-186] — Ignores generated fern-bundle output.
• STYLE_GUIDE.md [64-109] — Adds title conventions for task and concept documents.
• docs/ADRs/0002-generate-the-public-documentation-site-content-from-this-repository.md [223-331] — Documents the source-of-truth, transformation, slug, validation, and publication decisions.
• documentation/AGENTS.md [334-365] — Documents publishing workflow, frontmatter, links, MDX safety, and generated-output ownership.
• documentation/authoring/recipes/.md [371-452] — Aligns guide templates with imperative title conventions.
• documentation/concepts/AGENTS.md [457-470] — Adds concept-document title and closing-heading requirements.
• documentation/concepts/
.md [475-1355] — Adds Fern metadata, updates document titles, and normalizes related-documentation links.
• documentation/guides/*.md [1357-2200] — Adds publication metadata, changes guide titles to imperative form, and updates cross-document link text.
• documentation/fern-slugs.lock.json [1-100] — Adds persistent published slug and redirect state.
• package.json [1-100] — Adds docs:fern, docs:fern:apply, and fern:check commands.
• scripts/apply-fern-docs.ts [1-250] — Applies the generated bundle to a target contentful-docs checkout.
• scripts/export-fern-docs.ts [1-250] — Provides the CLI entry point for generating Fern documentation output.
• scripts/fern/build.ts [1-250] — Coordinates discovery, transformation, validation, and bundle construction.
• scripts/fern/bundle.ts [1-335] — Models and serializes the generated documentation bundle.
• scripts/fern/docs.ts [1-335] — Discovers published documents and determines navigation ordering.
• scripts/fern/transform.ts [1-250] — Transforms source Markdown into Fern-compatible Markdown/MDX.
• scripts/sdk-knowledge/markdown.ts [1-100] — Updates shared Markdown processing behavior used by validation or documentation tooling.
• scripts/validate-fern-export.ts [1-250] — Adds validation for generated export correctness.
• scripts/validate-guide-authoring.ts [1-100] — Integrates authoring validation with the updated title and documentation rules.

Repository Impact:
Documentation authoring: Authors must maintain fern.slug, section, description, and applicable navTitle metadata and follow updated heading conventions.
Documentation build tooling: New TypeScript modules parse Markdown, transform links and headings, generate navigation, and enforce slug stability.
CI/CD: The main pipeline validates the export while a separate workflow publishes generated content to another repository.
Public documentation navigation and URLs: Sidebar order derives from authored children lists and stable slugs are now explicitly locked.

Cross-Repository Dependencies:
contentful/contentful-docs: The sync workflow writes generated pages, navigation, and redirects to this repository through a pull request.

Database/Caching Impact:
• None

API Contract Violations:
• No runtime SDK API or network endpoint is changed.
• The public documentation URL and navigation contract changes operationally through generated slugs, redirects, titles, and sidebar order.
• The workflow assumes contentful-docs accepts generated content and that the target repository's Fern structure matches the bundle layout.

Infrastructure Dependencies:
• GitHub Actions permissions must allow repository contents writes and pull-request writes for contentful-docs.
• FERN_DOCS_SYNC_TOKEN must be available to the workflow and must not be exposed in logs.
• The target contentful-docs checkout requires a compatible branch, Fern directory layout, and GitHub CLI authentication.
• Concurrency and force-push behavior must be tested to prevent competing sync runs from overwriting one another unexpectedly.

Additional Insights:
Security: A cross-repository write token is introduced; a GitHub App installation token is preferable to a long-lived personal access token.
Idempotency: The workflow force-pushes a fixed branch and updates one pull request so repeated release-triggered runs converge.
MDX safety: Generated content must escape or preserve angle-bracket and brace-heavy prose safely because Fern interprets the output as MDX.

Testing Recommendations

Frontend Impact:
• Build the Fern bundle and inspect representative Android, Web, React, Node, iOS, and migration pages for correct titles, descriptions, navigation labels, and generated links.
• Confirm generated sidebar ordering follows authored children lists rather than alphabetical ordering.

Service Integration:
• Run pnpm docs:fern:apply against a disposable contentful-docs checkout and verify only the owned Fern pages, navigation section, and redirects are changed.
• Exercise the sync workflow with draft=true and draft=false, repeated runs, and simultaneous runs to verify concurrency and pull-request idempotency.

Data Serialization:
• Test parsing of fern frontmatter with folded descriptions, optional navTitle, missing fields, unexpected fields, and malformed YAML-like content.
• Test bundle serialization and application when a document contains headings, fenced code, links with fragments, GitHub URLs, callouts, and mtoc markers.

Privacy Compliance:
• Verify workflow logs never print FERN_DOCS_SYNC_TOKEN or other secret values.
• Confirm generated documentation does not publish documentation/authoring or documentation/internal paths.

Backward Compatibility:
• Run fern:check against the existing documentation corpus and confirm every existing published slug is either unchanged or has an explicit redirect in documentation/fern-slugs.lock.json.
• Test that changing a document H1 changes the published title but does not change its URL.
• Test that an unrecorded slug change fails validation.

OAuth Functionality:
• None

Cross-Service Communication:
• Use a test token and test branch in contentful-docs to verify checkout, bundle application, commit, force-push, and gh pr create/gh pr edit behavior.
• Validate the generated navigation and redirect configuration against the target repository's Fern schema before enabling release-triggered publication.

Reliability Testing:
• None

Additional Insights:
• Run the full pnpm validation and test suite, including existing guide and SDK-knowledge validators.
• Add fixtures for links to group indexes, internal-only documents, missing fragments, trailing-slash fragments, and malformed GitHub links.
• Use a clean checkout to ensure generated fern-bundle output is reproducible and does not depend on ignored local files.

Analysis based on known dependency patterns and edges. Actual impact may vary.

@bito-code-review bito-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
  • scripts/fern/transform.ts - 2
Additional Suggestions - 3
  • scripts/fern/transform.ts - 1
    • Mdx safety line-number mismatch · Line 364-370
      In `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-30
      The 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-8
      The `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
  • documentation/concepts/consent-management-in-the-optimization-sdk-suite.md - 1
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

AI Code Review powered by Bito Logo

Comment thread scripts/apply-fern-docs.ts
Comment thread .github/workflows/sync-fern-docs.yaml
Comment thread scripts/fern/build.ts
Comment thread scripts/fern/build.ts
Comment thread documentation/authoring/recipes/supplemental-recipe.md
Comment thread scripts/fern/transform.ts
Comment thread scripts/fern/transform.ts
@bito-code-review

Copy link
Copy Markdown

✅ Review Settings Overridden

Status: Overridden Successfully

Guidelines:

  • Accepted:

    • General : Review Posture, Repo Truth And Boundaries, Domain Invariants

    • Language : typescript= TypeScript Review Evidence

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.

@TimBeyer
Tim Beyer (TimBeyer) force-pushed the feat/fern-docs-sync-nt-3942 branch from a4f3547 to f68566c Compare August 18, 2026 15:23
@bito-code-review

bito-code-review Bot commented Aug 18, 2026

Copy link
Copy Markdown

Code Review Agent Run #7c72f9

Actionable Suggestions - 0
Additional Suggestions - 7
  • documentation/guides/integrating-the-optimization-android-sdk-in-a-compose-app.md - 1
    • CWE-N: Frontmatter field inconsistency across guides · Line 5-5
      The `navTitle` field is inconsistent with all other integration guides in this repository (Views, iOS SwiftUI, React Native, React Web), which omit this field entirely. Fern infers the nav title from the H1 heading by default, so explicit `navTitle` is redundant. Removing it maintains API consistency and reduces maintenance surface.
  • scripts/export-fern-docs.ts - 1
    • Magic number without invariant · Line 23-23
      Line 106 divides by this constant. While the value is correct today (each redirect in `renderRedirects` produces a `source` line and a `destination` line), an accidental future edit to `renderRedirects` that adds or removes a line per entry would silently corrupt the count without any test catching it. A self-documenting constant or an inline assertion would make the invariant explicit.
  • documentation/guides/using-contentful-graphql-data-with-the-optimization-sdks.md - 1
    • Naming mismatch vs peer guides · Line 11-11
      The H1 starts with 'Use', but every other integration guide in this guides section (e.g., 'Integrate the Optimization React Web SDK in a React app') uses 'Integrate' as the heading verb. Update to 'Integrate' for naming consistency across related guides.
  • documentation/guides/migrating-experience-js-to-the-web-sdk.md - 1
    • Duplicate description text · Line 10-13
      The description on lines 12–13 is an exact duplicate of the frontmatter `description` field. Remove the redundant paragraph to eliminate divergence risk when one copy is updated without the other.
  • documentation/concepts/react-native-sdk-interaction-tracking-mechanics.md - 1
    • Duplicate link in related docs · Line 592-593
      The 'Related documentation' section now contains a duplicate entry. The guide `../guides/integrating-the-react-native-sdk-in-a-react-native-app.md` is already referenced at line 18 under 'For step-by-step setup, see'. Remove line 592 to eliminate redundancy and maintenance risk.
  • scripts/fern/bundle.ts - 1
    • Unreachable code for Migration guides · Line 134-149
      The loop body for `"Migration guides"` is unreachable: `docsInSection(docs, 'Migration guides')` always returns `[]` because `loadPublishedDocs` only reads from `guides/` and `concepts/` (no migration directory exists), and `inSection.length === 0` on line 136 causes an immediate `continue` on every iteration for this section.
  • scripts/validate-fern-export.ts - 1
    • Misleading anchor count metric · Line 34-34
      The variable `anchors` counts the total number of anchor strings available on each page (`doc.anchors.size` summed), but the message describes it as 'anchors resolvable' — implying fragment references from other docs were resolved against these anchors. The name and message are misleading: `anchors` is a static set populated during doc parsing (what headings exist), not a count of cross-doc link resolutions. Rename to `totalAnchors` and clarify the message.
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • documentation/guides/integrating-the-optimization-ios-sdk-in-a-swiftui-app.md - 1
  • documentation/guides/building-a-custom-javascript-optimization-adapter.md - 1
Review Details
  • Files reviewed - 55 · Commit Range: 19e974c..f68566c
    • .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

AI Code Review powered by Bito Logo

bito-code-review[bot]
bito-code-review Bot previously approved these changes Aug 26, 2026
@phobetron

Copy link
Copy Markdown
Collaborator

Reviewed the non-threaded Bito output against the current head and repository authoring rules. No code changes are warranted for those suggestions:

  • The Android Compose navTitle intentionally shortens “Jetpack Compose” to “Compose”, which is the documented reason to supply navTitle.
  • REDIRECT_LINES is already a named constant with a comment stating the two-line invariant.
  • The GraphQL document is a supplemental recipe, not an SDK integration guide; “Use” is the correct imperative for its reader task.
  • Fern description metadata is required, while the opening paragraph separately orients the reader. Removing either copy would violate the publishing or guide-authoring contract.
  • Links that appear once during initial routing and again in the closing related-documentation section serve different reader moments; they are not duplicate list entries.
  • Migration guides live under documentation/guides/ and select section: Migration guides in frontmatter, so the migration navigation branch is reachable.
  • The anchor count describes headings available for fragment resolution. Cross-document fragment references are validated separately in the same build, so the status wording does not misrepresent a missing check.
  • inFence is consistently used as the per-line fence predicate array, with its boolean[] type and indexed access making that meaning explicit.
  • Unexpected non-DocError exceptions are deliberately rethrown with their stack rather than reformatted as authored-document diagnostics.
  • The filtered iOS, custom-adapter, Node-migration, and consent-document link observations either match their targets or intentionally repeat routing at the opening and closing navigation points.

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 scripts/fern/nav.ts, no package source was changed, the workflow explicitly integrates with the separate contentful-docs repository, and alerts render as Info or Warning components rather than a Callout component. The workflow is currently manual-dispatch only, so its release trigger is described in source as a future additive step rather than functionality already present.

All seven inline findings have evidence replies on their originating threads and have been resolved directly.

Tim Beyer (TimBeyer) and others added 6 commits August 31, 2026 16:07
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>
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>

@bito-code-review bito-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #492cb5

Actionable Suggestions - 3
  • scripts/export-fern-docs.ts - 1
  • 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-135
      The `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-21
      The 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-711
      These 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-716
      The 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-21
      Line 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-554
      The 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-976
      The 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

AI Code Review powered by Bito Logo

Comment thread scripts/export-fern-docs.ts Outdated
Comment on lines +75 to +77
const outDir = path.resolve(rootDir, options.outDir)
rmSync(outDir, { force: true, recursive: true })
mkdirSync(path.join(outDir, 'pages'), { recursive: true })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsafe output directory deletion

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
Suggested change
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

Comment thread scripts/apply-fern-docs.ts Outdated
continue
}
const lineIndent = line.length - line.trimStart().length
if (lineIndent <= indent && line.trimStart().startsWith('- ')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nav splice may remove non-list content

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

Comment thread scripts/fern/bundle.ts
Comment on lines +48 to +59
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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redirect cycle causes infinite loop

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
Suggested change
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

@bito-code-review

bito-code-review Bot commented Aug 31, 2026

Copy link
Copy Markdown

Code Review Agent Run #8b234f

Actionable Suggestions - 0
Review Details
  • Files reviewed - 1 · Commit Range: 871b417..6f05b26
    • docs/ADRs/0002-generate-the-public-documentation-site-content-from-this-repository.md
  • Files skipped - 0
  • Tools
    • 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

AI Code Review powered by Bito Logo

@phobetron

Charles Hudson (phobetron) commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

I (Codex) reviewed the two latest automated review runs against the current head (6f05b26). The later run examined only the new ADR and reported no findings. The full run reported 13 items, representing 12 unique claims because the unsafe-output issue was listed twice.

Three findings are valid, realistic, and worth addressing before merge:

  1. Unsafe output-directory deletion: pnpm docs:fern -- --out . or --out .. can pass the repository or its parent to recursive rmSync. The destination should be required to be a strict subdirectory of the repository before any mutation. The proposed guard is incomplete because it still permits arbitrary outside directories.
  2. Navigation splicing can remove non-owned YAML: the live contentful-docs layout is safe today because another navigation list item follows the generated section, but an ordinary same-indent separator comment or a valid trailing tabs:/landing-page: property would be consumed by the splice. The first nonblank line at the section's indentation or less should terminate the owned span, including comments.
  3. Slug restoration can create redirect cycles: a documented A -> B change followed by a later B -> A restoration persists both redirects. The downstream contentful-docs redirect check should normally block the sync PR, so a blocked publication is the expected first consequence rather than an immediate live loop. Removing only the direct reverse edge is insufficient for longer histories such as A -> B -> C -> A; the redirect graph should be validated and current slugs treated as terminal nodes.

The following suggestions are also valid, but lower priority:

  • documentation/AGENTS.md describes pnpm docs:fern as publishing, although it only builds the local bundle; applying it and opening the cross-repository PR are separate layers.
  • The seven flagged title-style link labels should match their destination titles: three XML Views app labels, the React Native and Node labels missing Optimization, and two tag management tools labels missing the title's hyphen.
  • headingAnchor and the local slug helper can disagree for supported headings containing inline links or repeated whitespace, potentially making fern:check and guides:check disagree. No current heading triggers this. The review's underscore example is incorrect, but the underlying divergence is real.
  • documentation/authoring/recipes/decision.md:21 is 121 characters against the checked-in MD013 limit of 100. Markdownlint is not currently run by repository CI, so this is a local/manual lint issue rather than a failing gate.

I do not consider the draft-on-update suggestion valid. The input says "Open the pull request as a draft," making it creation-scoped. Preserving the existing PR state on reruns avoids silently re-drafting a PR after a maintainer marks it ready; gh pr edit also cannot toggle draft state.

All current CI checks are green, but they do not exercise the three failure histories above.

Tim Beyer (TimBeyer) and others added 2 commits September 1, 2026 10:40
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>
@bito-code-review

bito-code-review Bot commented Sep 1, 2026

Copy link
Copy Markdown

Code Review Agent Run #2c0f1c

Actionable Suggestions - 0
Additional Suggestions - 3
  • scripts/fern/build.ts - 1
    • Redundant O(n²) redirect resolution · Line 105-105
      `resolveRedirects(reconciled)` is now computed twice per build: here at line 105 and again inside `renderRedirects(reconciled)` at line 113 (bundle.ts:142). Since `resolveRedirects` is O(n²) (nested `lock.redirects.find`), this doubles the work on every `docs:fern`/`fern:check` run. Resolve once and pass the entries to the render step.
  • scripts/export-fern-docs.ts - 1
    • Misleading comment · Line 22-23
      The comment claims "Nothing consumes a configurable location: `docs:fern:apply` reads from here", but `apply-fern-docs.ts` still exposes a `--bundle` option (lines 49-52) that overrides the bundle directory. This inaccuracy could mislead a maintainer into removing `--bundle` as dead code. Consider acknowledging the override.
  • scripts/fern/bundle.ts - 1
    • O(n²) redirect walk · Line 104-104
      `lock.redirects.find(...)` is an O(n) linear scan executed inside the nested chain-walk loop, once per hop per distinct `from`. Since `redirects` is append-only and never pruned (per the file header), this is O(n²) overall and grows with the lock. Build a `Map` from `from`→`to` once before the loop and use `nextByFrom.get(current)` for O(1) hops.
Review Details
  • Files reviewed - 17 · Commit Range: 6f05b26..2bddaab
    • scripts/apply-fern-docs.ts
    • scripts/export-fern-docs.ts
    • scripts/fern/build.ts
    • scripts/fern/bundle.ts
    • documentation/AGENTS.md
    • documentation/authoring/recipes/decision.md
    • documentation/concepts/android-sdk-runtime-and-interaction-mechanics.md
    • documentation/concepts/consent-management-in-the-optimization-sdk-suite.md
    • documentation/concepts/entry-personalization-and-variant-resolution.md
    • documentation/concepts/locale-handling-in-the-optimization-sdk-suite.md
    • documentation/concepts/profile-synchronization-between-client-and-server.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/using-contentful-graphql-data-with-the-optimization-sdks.md
    • scripts/sdk-knowledge/markdown.ts
    • scripts/validate-guide-authoring.ts
  • Files skipped - 0
  • Tools
    • 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

AI Code Review powered by Bito Logo

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