MANIFEST: Migrate DITA to Markdown
without losing content
Disclaimer. This is a portfolio design document. Google has not engaged, sponsored or endorsed this work. The sample content is adapted from the Google Cloud TPU documentation, which Google publishes under the Creative Commons Attribution 4.0 licence5. The adapted version is modified from the original. Stakeholders are job roles, not real people. The two readers in §3 are fictional.
DITA-OT already supports
must pass before merge
allowed per topic
Google's data center products and services documentation is written in DITA XML. DITA lets writers reuse content. A writer writes a warning or a spec value once and references it from many topics. The team now wants to move to docs as code: Markdown files in Git, reviewed in pull requests and built by CI (continuous integration).
Converting DITA to Markdown is easy. DITA Open Toolkit (DITA-OT), the free publishing engine the team already uses, does it with one command1. The hard part is what the conversion removes without a warning. It copies shared content into every topic, writes key values as plain text and drops element IDs. No team can read thousands of converted pages by hand, so these losses reach readers.
MANIFEST is a migration agent. It converts one DITA map at a time and proves that the converted topics are complete. Gemini runs the migration and fixes problems. Fixed Python checks decide whether each topic passes. Topics that pass go into a pull request for a writer to approve. Topics that fail go to a writer with the reason.
§1 Client Brief
Google publishes documentation for its data center products and services, including Cloud TPU (Tensor Processing Unit) accelerators. This MVP uses a small part of the Cloud TPU documentation as its sample: the pages for four TPU generations (v5e, v5p, v6e and TPU7x), rewritten as DITA topics.
These pages are a good test because they repeat a lot of content. Each generation has the same spec table with different values. The same values appear in tables and in text. Different readers need different parts of the page. In DITA, writers handle this with three features:
| DITA feature | What it does | Example in the TPU pages |
|---|---|---|
| Content reference (conref) | Reuses an element, such as a note or a table row, from another topic. | One note about slice shapes, used in every generation's topic. |
| Key | Stores a value once in the map. Topics refer to the key, not the value. | tpu7x-chips-per-pod = 9,2164 |
| Conditional content | Marks content for an audience. A DITAVAL file includes or excludes it at build time. | Capacity-planning notes shown only to operators. |
§2 Problem Statement
DITA-OT can publish DITA as Markdown in three formats: standard Markdown, GitHub-Flavored Markdown and GitBook1. The current release is 4.42. The team does not need a new converter. It needs to know what the converter changed.
Before DITA-OT writes any output, it processes the source. It replaces each key reference with its value, copies each conref target into place and removes content that the DITAVAL file excludes3. The output is correct for one audience, but the reuse is gone. Nothing in the Markdown shows which text was shared or which values came from keys.
The GitHub format loses more. DITA-OT turns definition lists into bulleted lists and leaves out attribute blocks that hold IDs and class names1.
| DITA feature | In the Markdown output | Effect after migration | Status |
|---|---|---|---|
| Conref | The shared text is copied into each topic. | One change becomes many edits. | Lost |
| Key reference | The value is written as plain text. | Copies of a value drift apart. | Lost |
| Conditional content | Only one audience's version is kept. | Other readers lose content. | Lost |
| Element ID | Dropped in the GitHub format. | Cross-references and support links break. | Lost |
| Definition list | Becomes a bulleted list in the GitHub format. | The structure changes. | Changed |
| Paragraphs, steps, code, simple tables | Converted as expected. | None. | Kept |
| Option | What it does | What it misses | Result |
|---|---|---|---|
| Convert by hand | Writers copy each topic into Markdown. | Slow. Reuse is lost. No record of changes. | Too slow |
| DITA-OT Markdown output | Converts a whole map with one command. | Loses reuse, IDs and conditions without a warning. | Silent loss |
| DITA-OT and spot checks | Reviewers read a sample of converted pages. | Pages outside the sample are never checked. | Partial |
| MANIFEST | Converts, restores reuse, checks every topic, then fixes or escalates. | Cannot map some advanced DITA features. It sends these to a writer (§5.4). | Checked |
§3 User Profiles
Five roles at Google run or depend on the migration. Two readers depend on the result.
§4 User Stories
Each story comes from the Goal and Success fields in §3. The acceptance criteria are what the checks in §5 test.
§5 Proposed Design
MANIFEST does one thing. It migrates a DITA map to MyST Markdown and proves that every topic is complete. It is an agent built with Agent Development Kit (ADK), Google's open-source agent framework. ADK is licensed under Apache 2.0 and its agents can run on Cloud Run14. Gemini 3.8 Flash (gemini-3.8-flash) runs the agent15 and calls its tools through Gemini function calling16.
The agent has five tools. Each tool is plain Python and gives the same result every time. The agent decides which tool to run next and how to fix a failed topic. The checks decide whether a topic passes. The agent cannot change a check result.
1 · Count what is in the source inventory_map
Reads the map and its topics with Python and lxml. Counts elements, conrefs, key references, conditional attributes, element IDs and links in each topic. This count is the "before" picture that every check uses.
2 · Convert with DITA-OT convert_map
Runs DITA-OT 4.4 with the standard markdown format, not the GitHub format, which drops more1. Before the run, the tool puts a placeholder in place of each conref, key reference and conditional block in a working copy. DITA-OT cannot resolve a placeholder, so the reuse survives the build. The DITA source is never changed.
3 · Put the reuse back restore_reuse
Replaces each placeholder with its MyST Markdown equivalent. Shared content becomes an include file. A key becomes a substitution. Conditional content becomes an only block. Each element ID becomes a target. §5.4 lists every mapping.
4 · Check the topic check_topic
Runs three checks. Counts: counts the output again and compares it with the inventory. Text: builds the source with DITA-OT HTML5 and the output with Sphinx for the same DITAVAL file, then compares the text. IDs: confirms that every source ID is an anchor. Returns pass, or a list of differences.
5 · Check the links check_links
Runs lychee, an open-source link checker, over the Markdown and the built HTML. It checks internal links offline and writes the result as JSON11. It also checks that every old URL has a redirect.
The agent runs tools 1 to 5 for each topic. When check_topic reports a difference, Gemini reads the difference and the source, and proposes a fix. For example, it maps an element that has no rule yet. The agent applies the fix and runs the check again. After two failed fixes, it stops and opens a review issue for a writer. The issue contains the source, the output and the check result.
When a writer approves a fix, MANIFEST saves it as a new mapping rule. The next map uses the rule without calling Gemini.
MyST Markdown supports substitutions, set in the project configuration or in a page's front matter8, and an include directive18. Sphinx's only directive includes content based on tags set at build time9. Together they cover the three DITA reuse features.
| DITA | MyST Markdown | How the check proves it | MVP |
|---|---|---|---|
| Conref | Include file in _includes/, written once | Includes in the output equal conrefs in the source | Mapped |
| Key reference | Substitution, for example {{ tpu7x_chips_per_pod }} | Substitutions equal key references; no typed copies | Mapped |
| Conditional content and DITAVAL | only block with build tags; one build for each DITAVAL file | Text matches DITA-OT output for the same DITAVAL file | Mapped |
| Element ID | Target, for example (slice-shapes)= | Every source ID is an anchor | Mapped |
| Specialised elements, conref push, key scopes | No rule | Sent to a writer as a review issue | Writer |
One limit to know: Sphinx still processes content inside an only block that a build excludes, which can cause surprises when headings or links sit inside one10. The text check in tool 4 is there to catch this.
| Option | Reuse support | Result |
|---|---|---|
| Sphinx with MyST Markdown | Includes, substitutions and build tags are built in. Python, like the rest of MANIFEST. | Chosen |
| Material for MkDocs | Reaches end of life on 5 November 20266. | Not chosen |
| Docusaurus | Reuse works by importing MDX files. The page's table of contents does not include headings from imported Markdown7. | Not chosen |
| Part | Service | Note |
|---|---|---|
| Agent | Cloud Run job, one map per run | Free tier: 180,000 vCPU-seconds a month13 |
| Pull request check | GitHub Actions | Standard runners are free for public repositories12 |
| Reports | Cloud Storage, US region | Free tier: 5 GB-months13 |
| Model | Gemini API, gemini-3.8-flash | The free tier is acceptable here because the sample content is public. Free tier content is used to improve Google products, so private content needs the paid tier17. |
§6 Migration Simulator
The simulator runs three sample topics through MANIFEST. Each run shows the tools in order and the log that the agent writes. The runs are scripted to show the design. They do not call Gemini or DITA-OT.
// choose a sample topic and run the migration
§7 Success Metrics
The MVP runs on the TPU sample map. It succeeds if the numbers below are met on that map. The first metric matters most: a migration that reports every loss is safe to review, even when it has failures.
| Metric | How it is measured | MVP target |
|---|---|---|
| Unreported losses | Losses a writer finds after merge that the report did not list | 0 |
| Topics checked | Topics with a report ÷ topics in the map | 100% |
| Reuse kept | Includes and substitutions in the output ÷ conrefs and key references in the source | 100% of mapped features |
| IDs kept | Source element IDs that are anchors in the output | 100% |
| Broken links | lychee result after migration, including old URLs | 0 |
| Topics passed without a writer | Topics that pass on the first run or after a Gemini fix | Set from the first run; tracked for each map |
| Fixes approved | Gemini fixes that a writer approves ÷ fixes proposed | Tracked; each approved fix becomes a rule |
| Topic | Counts | Text | IDs | Links | Note |
|---|---|---|---|---|---|
| create-tpu-vm | pass | pass | pass | pass | None |
| tpu7x-specs | pass | pass | pass | pass | 3 includes, 9 substitutions restored |
| v6e-specs | fixed | pass | pass | pass | Definition list mapped by Gemini; approved |
| slice-planning | fail | pass | pass | pass | Specialised element has no rule; issue #41 |
Sources
Checked on 19 October 2026.
- DITA Open Toolkit 4.3, "Generating Markdown output." dita-ot.org/4.3/topics/dita2markdown
- DITA Open Toolkit, "Migrating to release 4.4." dita-ot.org/4.4/topics/migrating-to-4.4
- DITA Open Toolkit, pre-processing modules: debug and filter, branch filtering, keyref, conref and profile steps. dita-ot.org/dev/reference/preprocess-debugfilter
- Google Cloud, "TPU7x (Ironwood)," last updated 13 October 2026. docs.cloud.google.com/tpu/docs/tpu7x
- Google Developers, "Site Policies." developers.google.com/site-policies
- mkdocs-material 9.7.7 release notes (17 July 2026), quoted in a dependency update. github.com/cms-cat/mkdocs-material/pull/229
- Docusaurus, "MDX and React: Importing Markdown." docusaurus.io/docs/markdown-features/react
- MyST-Parser, "Syntax extensions: Substitutions." myst-parser.readthedocs.io/en/stable/syntax/optional.html
- Sphinx
onlydirective reference. devopstutodoc.readthedocs.io - MongoDB Meta Documentation, "cond Directive." mongodb.com/docs/meta/reference/cond
- lychee link checker. snapcraft.io/lychee · github.com/lycheeverse/lychee-action
- GitHub Docs, "How use of GitHub Actions is measured." docs.github.com/en/billing/concepts/product-billing/github-actions
- Google Cloud, "Free Tier." cloud.google.com/free
- Google, Agent Development Kit documentation README. github.com/google/adk-docs
- Google AI for Developers, "Gemini models." ai.google.dev/gemini-api/docs/models
- Google AI for Developers, "Function calling with the Gemini API." ai.google.dev/gemini-api/docs/function-calling
- Google AI for Developers, "Gemini Developer API pricing." ai.google.dev/gemini-api/docs/pricing
- MyST-Parser changelog (include directive in MyST documents). github.com/executablebooks/MyST-Parser