AlignR: Generate new DITA topics from the ones you have
and check them against the schema before a writer sees them
Disclaimer. This is a portfolio design document. Google has not engaged, sponsored or endorsed this work. The sample content is adapted from the Cloud TPU documentation, which Google publishes under the Creative Commons Attribution 4.0 licence1. The adapted versions are modified from the originals. The sample runs, pull requests and costs in this document are modelled, not measured. Stakeholders are job roles, not real people. The two readers in §3 are fictional.
a writer sees a topic
with one shared skeleton
to start
Each new Cloud TPU generation gets a set of topics with the same structure as the last one and new values. The v5p, v6e and TPU7x pages share one skeleton: an introduction, a system architecture table, configurations and a VM section234. Writers build each new set by copying the last one and editing it by hand. Labels drift and values get typed twice.
AlignR is an agent that drafts the new set from the existing DITA topics. It finds sibling topics of the same type, reads the new values, and asks Gemini to fill a JSON Schema that mirrors the DITA content model. Gemini never writes XML. A plain Python serialiser writes it, with a key reference for every value that has a key. The topic then passes the DITA RELAX NG grammar, house rules in Schematron, a grounding check and Vale before a writer sees it as a pull request.
The original AlignR design aligned a model by fine-tuning on 12,000 to 20,000 preference pairs5. This version keeps its preference pair format and drops the training. Every writer edit to a draft is logged as a pair. A fixed rule (§5.9) decides when the log is large enough, and good enough, to pay for tuning.
§1 Client Brief
The client is Google's data center products and services documentation. The sample content is the Cloud TPU documentation for three hardware generations: TPU v5p, TPU v6e (Trillium) and TPU7x (Ironwood)234. The MANIFEST project converted this content to DITA, with generation values stored once as keys6.
It is a good test for two reasons. First, the generations repeat a structure, so there are real siblings to learn from. Second, the answer exists. AlignR holds out TPU7x, drafts its topics from the v5p and v6e topics and the TPU7x values, and compares the draft with the page Google published. That page was last updated on 13 October 20262.
| Term | Meaning in this document |
|---|---|
| Topic type | A DITA content model: concept, task or reference. Each type fixes which elements a topic can contain and in what order. |
| Sibling topic | A topic of the same type in the same place in the map, for another generation. The v6e VM topic is a sibling of the TPU7x VM topic. |
| Key and key reference | A value defined once in a map (tpu7x-hbm-capacity) and pulled into topics with keyref. Change the key, every page changes. |
| RELAX NG grammar | The normative OASIS grammar files for DITA. They decide whether a topic is valid DITA11. |
| Schematron | An ISO rule language for checks a grammar can't express, such as "spec values must use a key"13. |
| Structured output | A Gemini API setting that makes the model return JSON that matches a JSON Schema you supply9. |
| Draft comment | The DITA <draft-comment> element. Processors don't render it by default, so it carries open questions safely14. |
| Preference pair | A prompt with a rejected and a chosen answer. Here: the agent's draft (rejected) and the text the writer merged (chosen)5. |
§2 Problem Statement
A new hardware generation is the most predictable writing job in data center documentation. The topic set is known in advance. The structure is the same as last time. Only the values and a few new concepts change. Today that predictability is used by hand: a writer copies the previous set and edits it.
2.1 Same skeleton, new values
Figure 2 lines up the three TPU generation pages as Google publishes them. Every page has the same four slots. The labels in those slots have drifted, because each page was written separately.
The skeleton stays. The values move a lot from one generation to the next (Figure 3). In a DITA set every one of these numbers is a key, so a new generation adds a column of values, not a new structure.
2.2 Why the current options fail
There are four common ways to draft the new set. None of them gives a writer a draft that is guaranteed to be valid DITA with sourced values.
| Option | What it does | What it misses | Result |
|---|---|---|---|
| Copy the last generation and edit by hand | The writer copies each v6e topic and changes the values. | No check until the build. Labels drift (Figure 2). Values get typed twice. | Slow |
| Prompt a general chat assistant | Writes prose from a description of the new chip. | Output is not valid DITA. Values have no source and no keys. | Invalid |
| Retrieval plus a prompt, with no gate | Adds the sibling topics to the prompt as examples. | Closer in shape, but the XML and every value are still unchecked. | Unchecked |
| Fine-tune a model (original AlignR) | Trains on preference pairs so the model learns the house structure. | Needs 12,000 to 20,000 pairs over 4 to 8 weeks5. A tuned model is still free to write invalid XML. | No data yet |
| AlignR v0.1: constrained generation | Gemini fills a JSON Schema built from the content model. Python writes the XML. Two schema gates run before review. | Prose quality still needs a writer. New concepts still need a writer. | Valid by construction |
§3 User Profiles
Five roles at Google decide whether AlignR is useful. Two readers at Google use what it produces.
Readers
§4 User Stories
Each story has acceptance criteria that a check in §5 tests.
log_pair adds the edit ratio after merge; the tuning rule in §5.9 reads only that table.keyref or carries a source ID; every open question is a <draft-comment>.validate_grammar and check_rules are required status checks; the agent's account has no merge permission.check_grounding passes with no unsourced value; each question links to its line in the pull request.spec-values-use-keys passes; no literal specification value remains in the topic.check_grounding compares units as well as numbers, and flags GiB against GB as a mismatch.same-columns-as-siblings passes for every reference table.§5 Proposed Design
AlignR v0.1 is an agent built with Agent Development Kit (ADK), Google's open-source agent framework16. It uses Gemini 3.8 Flash (gemini-3.8-flash)17 in one mode only: structured output, where the model must return JSON that matches a JSON Schema9. Plain Python tools, called through function calling18, do the rest. They find siblings, write the XML and run the checks. The checks decide pass or fail, and the agent can't override them.
5.1 How it works
A writer opens an issue for a new generation set. AlignR reads three inputs that already exist: the launch values, the DITA set, and the content model. It drafts each topic, gates it, and opens one draft pull request per topic. Figure 6 is the same context as Figure 1, with AlignR added.
5.2 Two schema gates
The schema is the guardrail, twice. Gate 1 runs inside the Gemini API: the response must match the JSON Schema that AlignR built for this run. Gate 2 runs on the XML: the topic must be valid against the OASIS DITA grammar, which is normative in RELAX NG11. Between and after the gates, three checks catch what a grammar can't.
| Stage | Runs on | Catches | Can't catch |
|---|---|---|---|
| Gate 1: JSON Schema | Gemini API, at generation | Shape, types, required fields, enums of real keys and sources, list lengths9 | Wrong values in the right shape. Google's own guidance is to validate values in the application9. |
| Gate 2: DITA grammar | lxml RelaxNG with the OASIS grammars12 | Any topic that is not valid DITA | Valid DITA that breaks house rules |
| House rules | lxml.isoschematron13 | Rules the IA owns: keys for spec values, sibling table layout, ID patterns | Whether a value is true |
| Grounding | Python, against facts and keys | Values, units, flags and commands with no source | Whether the prose explains well |
| Style | Vale with the Google package15 | Google developer documentation style errors | Meaning. That is the writer's job. |
5.3 From the DITA content model to a JSON Schema
AlignR does not give Gemini the DITA grammar. Gemini accepts a subset of JSON Schema and may reject very large or deeply nested schemas9. Instead, build_schema maps one topic type to a small, flat JSON Schema. Figure 8 shows the mapping for a reference topic.
This is an excerpt of the schema that build_schema produces for the TPU7x system architecture topic. JSON Schema keywords such as enum, required, minItems and additionalProperties are all supported in the Gemini API9. Google extended JSON Schema support to all actively supported Gemini models in November 202510.
// build_schema output (excerpt) · topic type: reference · run 0417 { "type": "object", "additionalProperties": false, "required": ["title", "shortdesc", "specs", "sections", "questions"], "properties": { "specs": { "type": "array", "minItems": 11, "maxItems": 11, "items": { "type": "object", "required": ["label", "key"], "properties": { "label": { "type": "string" }, "key": { "enum": ["tpu7x-chips-per-pod", "tpu7x-bf16-tflops", "tpu7x-hbm-capacity", …] } } } }, "sections": { "type": "array", "items": { … "paras": [{ "text", "sources": { "enum": ["S1","S2","S3"] }, "minItems": 1 }] } }, "questions": { "type": "array", "items": { "properties": { "for": { "enum": ["engineer", "writer"] } } } } } }
The row count in specs comes from the sibling tables, not from the model. The siblings decide the layout.
5.4 Agent tools
The agent has ten tools. One calls the model. The others give the same result every time. The grounding check and the question placeholder come from The Autonomous Author7. Reading structure before calling a model comes from KnowledgeFlow8.
1. Find sibling topics find_siblings
Reads the DITA map. Returns the topics of the same type in the same place for earlier generations. It uses the map, not a search index.
2. Load the new values load_facts
Reads the launch facts file and the existing key definitions. Gives each fact a source ID and records its unit.
3. Build the schema build_schema
Maps the topic type to a JSON Schema (Figure 8). Fills its enums with this run's keys and source IDs. Fixes table rows and columns from the siblings.
4. Draft the topic generate_topic model
Calls Gemini 3.8 Flash with structured output. The prompt holds the siblings, the facts and the house style. On a revision, it holds only the failed fields and the reasons.
5. Write the XML serialise_dita
Turns the JSON into a DITA topic. Writes a keyref for every key. Replaces any literal value that matches a key. Writes questions as <draft-comment> and new keys as a map change.
8. Check every value check_grounding check
Every number, unit, flag and command in the topic must match a fact or a key. Units must match as well as numbers, so GiB against GB fails.
9. Open the pull request open_pr
Opens a draft pull request with the report (§7). Assigns each question to the engineer or writer it names. The agent's account can't merge.
10. Log the edit log_pair
Runs on merge. Stores the agent's draft as rejected and the merged text as chosen, field by field, in the original AlignR pair format5. Adds the edit ratio and cost.
5.5 Agent loop
The model decides how to fill each field. The checks decide whether it is done. Deterministic fixes run before any model revision, so the model is called again only for what code can't fix.
5.6 One topic, end to end
Figure 10 follows one topic from the issue to the edit log. It is the run in the third simulator scenario (§6).
5.7 Sample output
This is the start of the topic that AlignR writes for the TPU7x system architecture. The values are key references, so the table and the prose can't disagree. The one fact that the sources don't settle is a question, not a guess.
<reference id="tpu7x-system-architecture" xml:lang="en-US"> <title>TPU7x system architecture</title> <shortdesc>Each TPU7x chip contains two TensorCores and four SparseCores.</shortdesc> <prolog><metadata> <othermeta name="generated-by" content="alignr 0.1 · run 0417 · schema 3f9c"/> </metadata></prolog> <refbody> <properties id="specs"> <prophead><proptypehd>Specification</proptypehd><propvaluehd>TPU7x</propvaluehd></prophead> <property><proptype>Number of chips per Pod</proptype> <propvalue><ph keyref="tpu7x-chips-per-pod"/></propvalue></property> <property><proptype>HBM capacity per chip</proptype> <propvalue><ph keyref="tpu7x-hbm-capacity"/></propvalue></property> <!-- 9 more rows, same layout as the v6e sibling --> </properties> <section id="memory"><title>Memory hierarchy</title> <p>Each chip has <ph keyref="tpu7x-hbm-capacity"/> of high-bandwidth memory (HBM).</p> <draft-comment author="alignr" disposition="open"> For the engineer: S1 gives HBM as 192 GiB. S2 gives 192 GB. Which unit is correct? </draft-comment> </section> </refbody> </reference>
<ph keyref>. The prose and the table read one value.5.8 The edit log
The original AlignR design spent weeks collecting preference pairs before it could train5. In v0.1 the pairs collect themselves. Every merged pull request is a set of pairs: the agent's draft field and the writer's final field. The log is used first for cheap fixes, and only later, if ever, for training.
Each row keeps the original AlignR preference pair fields and adds what a documentation team needs to trace a change.
// edit log row · same prompt / chosen / rejected core as AlignR v1 { "pair_id": "run0417-sections-1-paras-0", "topic_id": "tpu7x-system-architecture", "topic_type": "reference", "field": "sections[1].paras[0].text", "rejected": "Each chip includes HBM for large batch sizes.", "chosen": "Each chip has {tpu7x-hbm-capacity} of HBM. The large capacity allows large batch sizes.", "edit_ratio": 0.41, "reviewer_role": "technical_writer", "model_id": "gemini-3.8-flash", "schema_hash": "3f9c", "attempts": 1, "tokens_in": 11840, "tokens_out": 1960 }
5.9 When tuning pays
Fine-tuning is not a later phase of this MVP. It is a decision the edit log can make, with a fixed rule. Until the rule says yes, the cheaper fixes in §5.8 come first.
- The log holds at least 12,000 accepted pairs for one topic type. This is the floor the original AlignR design set for one domain5.
- The edit ratio for that topic type has stopped falling for three launches in a row, after new rules and examples.
- A replay of held-out pairs shows that a tuned model saves more writer hours than its training and serving cost.
The numbers show why the rule is strict. Figure 14 models the cost of one draft attempt with today's prices. Tuning does not make a draft cheaper here. The newest Flash model with supervised fine-tuning on the price list is Gemini 3.5 Flash, and a tuned endpoint for Gemini 3 and later costs 1.5 times its base model19. Tuning can pay only through writer time.
| Price used in Figure 14 | Value | Source |
|---|---|---|
| Gemini 3.8 Flash, per 1M tokens in / out | $0.75 / $3.75 to 31 December 2026; $1.50 / $7.50 from 1 January 2027 | Agent Platform pricing19 |
| Gemini 3.5 Flash, per 1M tokens in / out | $1.50 / $9.00 | Agent Platform pricing19 |
| Tuned endpoint, Gemini 3 and later | 1.5 times the base model price | Agent Platform pricing19 |
| Supervised fine-tuning, Gemini 3.5 Flash | $0.01 per 1,000 training tokens | Agent Platform pricing19 |
| Preference tuning, Gemini 2.5 Flash | $0.005 per 1,000 training tokens | Agent Platform pricing19 |
| Training tokens | Dataset tokens × number of epochs | Supervised fine-tuning docs20 |
5.10 Where it runs
| Part | Technology | Free tier or licence |
|---|---|---|
| Agent framework | Agent Development Kit16 | Apache 2.0 |
| Model | Gemini 3.8 Flash (gemini-3.8-flash), structured output | Gemini API free tier for the POC21 |
| Grammar gate | OASIS DITA RELAX NG grammars, validated with lxml1112 | Open standard; open source |
| House rules and style | lxml.isoschematron13, Vale with the Google package15 | Open source; package under CC BY 4.0 |
| Build check | DITA-OT 4.4, as in MANIFEST6 | Apache 2.0 |
| Trigger and review gate | GitHub Actions, protected branches25 | Actions free for public repositories24 |
| Agent runtime | Cloud Run job, one run per issue | 180,000 vCPU-seconds a month22 |
| Edit log | BigQuery | 10 GiB storage and 1 TiB of queries a month23 |
Data handling. The sample content is public, so the Gemini API free tier is acceptable for the POC. Content sent on the free tier is used to improve Google products, so unreleased launch values need the paid tier21.
Known limits. AlignR drafts what has a sibling. A concept that is new in this generation, such as the TPU7x dual-chiplet design, gets a stub with questions, not prose. lxml checks the grammar but doesn't apply the default attribute values that DITA processing needs, so DITA-OT still builds every topic in CI.
§6 Draft Simulator
Three runs from the TPU7x set. The first passes cleanly. In the second, code fixes everything. In the third, Gemini fixes one field and the engineer gets one question.
// choose a sample topic and run it
§7 Success Metrics
The MVP is measured on the TPU7x holdout. Each metric comes from a check result or the edit log, not from a judgement.
| Metric | How it is measured | MVP target |
|---|---|---|
| Drafts that reach a writer and fail the DITA grammar | Draft pull requests opened with a failing validate_grammar result | 0 |
| Values with a source | check_grounding: values with a key or source ID, divided by all values | 100% |
| Specification values written as keys | Schematron rule spec-values-use-keys | 100% |
| Edit ratio on topics with a sibling | Changed tokens between the draft and the published TPU7x text2 | Under 40% |
| Time to first draft | Issue opened to draft pull requests for the whole set | Under 15 minutes |
| Open questions at merge | Draft comments left open when a pull request merges | 0 |
| Model calls per topic | Run log in BigQuery | 1.5 or fewer on average, falling as rules grow |
| Runs with logged cost | Runs with tokens and cost in the edit log | 100% |
Built from
- Siblings:
v6e-training,v5e-training(task, same map position) - S1 TPU7x page: machine type
tpu7x-standard-4t, JAX and PyTorch supported - Schema
c41e: task, 4 to 8 steps, commands checked after generation
Checks
- Grammar: valid against the DITA task grammar
- Rules: 6 of 6 pass. Vale: 0 errors, 2 warnings (
Google.Parens,Google.Will) - Grounding: step 4 had
--version=v2-alpha-tpu7xwith no source. The pattern came from the v6e sibling. Revised once.
Questions
- For the engineer: which TPU software version does TPU7x use? (step 4, line 38)
Cost
2 calls · 15,290 tokens in · 2,600 tokens out · $0.021 at the introductory price
Sources
Checked on 19 October 2026.
- Google Developers, "Site Policies." developers.google.com/site-policies
- Google Cloud, "TPU7x (Ironwood)," last updated 13 October 2026. docs.cloud.google.com/tpu/docs/tpu7x
- Google Cloud, "TPU v6e," last updated 13 October 2026. docs.cloud.google.com/tpu/docs/v6e
- Google Cloud, "TPU v5p," last updated 13 October 2026. docs.cloud.google.com/tpu/docs/v5p
- Siddharth Rao, "AlignR: Behavioural Alignment for Regulated Industry AI," design document v1.0.0. github.com/siddharthraop/AlignR
- Siddharth Rao, "MANIFEST," design document. siddharthraop.github.io/Manifest
- Siddharth Rao, "The Autonomous Author," design document. siddharthraop.github.io/The-Autonomous-Author
- Siddharth Rao, "KnowledgeFlow," design document. siddharthraop.github.io/KnowledgeFlow
- Google AI for Developers, "Structured outputs," last updated 13 October 2026. ai.google.dev/gemini-api/docs/structured-output
- Google, "Improving Structured Outputs in the Gemini API," 5 November 2025. blog.google/technology/developers/gemini-api-structured-outputs
- DITA 1.3 specification, "Recognized XML-document grammar mechanisms." dita-lang.org/1.3/dita/archspec/base/recognizedconstraintmechanisms
- lxml, "Validation with lxml." lxml.de/validation.html
- lxml, "lxml.isoschematron package." lxml.de/apidoc/lxml.isoschematron.html
- DITA language reference, "draft-comment." dita-lang.org/dita/langref/base/draft-comment
- Vale, "Google" style package. vale.sh/hub/google
- 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 Cloud, "Agent Platform pricing: Generative AI." cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing
- Google Cloud, "About supervised fine-tuning for Gemini models." docs.cloud.google.com/.../tuning/supervised-tuning
- Google AI for Developers, "Gemini Developer API pricing." ai.google.dev/gemini-api/docs/pricing
- Google Cloud, "Cloud Run pricing." cloud.google.com/run/pricing
- Google Cloud, "BigQuery pricing." cloud.google.com/bigquery/pricing
- GitHub Docs, "How use of GitHub Actions is measured." docs.github.com/en/billing/concepts/product-billing/github-actions
- GitHub Docs, "About protected branches." docs.github.com/.../about-protected-branches