Skip to content

Commit f230247

Browse files
committed
Two surfaces that machines read, and neither existed
`llms.txt` at both roots and JSON-LD on both surfaces. Neither changes a number and neither changes what a person sees. `llms.txt` is the convention crawlers and retrieval models now look for: the question, the answer, the figures with their period and basis, the definitions that change the answer, the five sources with their licences, and links to the written analysis. Two copies because there are two roots, `docs/` for Pages and `web/public/` for Vercel, and `.vercelignore` excludes `/docs` so one file could not have served both. It goes into `CORPUS_FILES` in the same commit that creates it. `web/` was outside the narrative guard for the app's entire life and any superseded figure typed into a React page would have published silently. A figure-dense summary written for machines is the last place that should be allowed to happen twice, so this surface inherits the guard on the way in rather than two sessions later. The JSON-LD is a Report plus a Dataset plus a WebSite, naming all five upstream sources with their licences, the author and the MIT terms. It carries no figures on purpose: the guard strips tags and reads the JSON body as prose, so a number in there would have to be swept like any other, and metadata is the wrong place to publish one. The load-bearing part is the canonical URL. The mirror on GitHub Pages serves the same argument as the Vercel site, so if it declared itself canonical the two hosts would compete for one signal. `tests/test_delivery.py` now asserts both blocks are valid JSON, that they have not drifted apart, that the canonical is the Vercel origin and that no github.io URL appears, and that the Dataset still names all five sources. Verified in the built export: `web/out/index.html` carries the block, it parses, and `web/out/llms.txt` is served at the root. 213 tests, tsc clean, seven routes.
1 parent eef4189 commit f230247

6 files changed

Lines changed: 573 additions & 1 deletion

File tree

‎docs/index.html‎

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,168 @@
2323
<meta name="twitter:title" content="India's Wide-Body Window"/>
2424
<meta name="twitter:description" content="Compete with the Gulf hubs, do not fly more aircraft to them. A commercial aviation case built on DGCA, Eurostat and World Bank data, with every figure computed in-repo and ten documented changes of mind."/>
2525
<meta name="twitter:image" content="https://india-widebody-window.vercel.app/assets/social-card.png"/>
26+
<!-- Structured data. Report plus Dataset, naming the five sources, the licence and
27+
the author, with the CANONICAL url on both hosts so the mirror does not compete
28+
with the site it mirrors. Deliberately carries no figures: the narrative guard
29+
strips tags and reads this body as prose, so a number here would have to be swept
30+
like any other, and metadata is the wrong place to publish one. -->
31+
<script type="application/ld+json">
32+
{
33+
"@context": "https://schema.org",
34+
"@graph": [
35+
{
36+
"@id": "https://india-widebody-window.vercel.app/#report",
37+
"@type": "Report",
38+
"about": [
39+
{
40+
"@type": "Thing",
41+
"name": "Commercial aviation market entry"
42+
},
43+
{
44+
"@type": "Thing",
45+
"name": "Wide-body aircraft deployment"
46+
},
47+
{
48+
"@type": "Thing",
49+
"name": "India Gulf air corridor"
50+
},
51+
{
52+
"@type": "Organization",
53+
"name": "IndiGo"
54+
},
55+
{
56+
"@type": "Organization",
57+
"name": "Air India"
58+
},
59+
{
60+
"@type": "Place",
61+
"name": "India"
62+
}
63+
],
64+
"abstract": "Compete with the Gulf hubs, do not fly more aircraft to them. Europe first, North America second, Gulf capacity roughly flat. A commercial aviation market entry case in which every figure is computed from committed open data, conflicts between agencies are reported rather than resolved away, and every change of mind is published.",
65+
"author": {
66+
"@type": "Person",
67+
"name": "Anklesh Rawat",
68+
"url": "https://github.com/DogInfantry"
69+
},
70+
"creator": {
71+
"@type": "Person",
72+
"name": "Anklesh Rawat",
73+
"url": "https://github.com/DogInfantry"
74+
},
75+
"datePublished": "2026-08-15",
76+
"headline": "Where should Indian carriers deploy their next 100 long-haul aircraft, and can the India-Gulf corridor absorb them?",
77+
"inLanguage": "en",
78+
"isAccessibleForFree": true,
79+
"isBasedOn": {
80+
"@id": "https://india-widebody-window.vercel.app/#dataset"
81+
},
82+
"license": "https://opensource.org/licenses/MIT",
83+
"name": "India's Wide-Body Window",
84+
"url": "https://india-widebody-window.vercel.app"
85+
},
86+
{
87+
"@id": "https://india-widebody-window.vercel.app/#dataset",
88+
"@type": "Dataset",
89+
"codeRepository": "https://github.com/DogInfantry/india-widebody-window",
90+
"creator": {
91+
"@type": "Person",
92+
"name": "Anklesh Rawat",
93+
"url": "https://github.com/DogInfantry"
94+
},
95+
"description": "Indian international and domestic air traffic, carrier operating statistics, corridor economics, wide-body order book and market sizing, computed from DGCA, Eurostat, IATA, World Bank and OurAirports and committed as parquet. Modelled values are labelled as modelled and hand-entered values carry a verification status.",
96+
"distribution": [
97+
{
98+
"@type": "DataDownload",
99+
"contentUrl": "https://github.com/DogInfantry/india-widebody-window/tree/main/data/processed",
100+
"encodingFormat": "application/vnd.apache.parquet",
101+
"name": "Processed parquet corpus"
102+
}
103+
],
104+
"isAccessibleForFree": true,
105+
"isBasedOn": [
106+
{
107+
"@type": "Dataset",
108+
"description": "Indian domestic and international traffic, the spine of the analysis.",
109+
"license": "https://opendatacommons.org/licenses/odbl/",
110+
"name": "DGCA traffic statistics",
111+
"url": "https://github.com/Vonter/india-aviation-traffic"
112+
},
113+
{
114+
"@type": "Dataset",
115+
"description": "The European end of India to Europe routes, used to check DGCA from the opposite direction.",
116+
"license": "https://ec.europa.eu/info/legal-notice_en",
117+
"name": "Eurostat avia_par",
118+
"url": "https://ec.europa.eu/eurostat/web/transport/data/database"
119+
},
120+
{
121+
"@type": "Dataset",
122+
"description": "India's departing international origin to destination split, by region and by country.",
123+
"license": "https://www.iata.org/en/about/terms-conditions/",
124+
"name": "IATA, Aviation in India",
125+
"url": "https://www.iata.org/"
126+
},
127+
{
128+
"@type": "Dataset",
129+
"description": "Income, population and air travel propensity across twelve peer countries.",
130+
"license": "https://creativecommons.org/licenses/by/4.0/",
131+
"name": "World Bank Open Data",
132+
"url": "https://data.worldbank.org/"
133+
},
134+
{
135+
"@type": "Dataset",
136+
"description": "Airport reference data and coordinates.",
137+
"license": "https://creativecommons.org/publicdomain/zero/1.0/",
138+
"name": "OurAirports",
139+
"url": "https://ourairports.com/data/"
140+
}
141+
],
142+
"keywords": [
143+
"commercial aviation",
144+
"India",
145+
"DGCA",
146+
"airline economics",
147+
"market sizing",
148+
"wide-body aircraft",
149+
"India Gulf corridor",
150+
"yield headroom",
151+
"available seat kilometres",
152+
"open data"
153+
],
154+
"license": "https://opensource.org/licenses/MIT",
155+
"name": "India's Wide-Body Window computed corpus",
156+
"spatialCoverage": {
157+
"@type": "Place",
158+
"name": "India and its international corridors"
159+
},
160+
"temporalCoverage": "2015/2026",
161+
"url": "https://github.com/DogInfantry/india-widebody-window",
162+
"variableMeasured": [
163+
"International sector passengers",
164+
"Available seat kilometres",
165+
"Average stage length",
166+
"Load factor",
167+
"Yield headroom by corridor",
168+
"Seat entitlement utilisation",
169+
"Unit cost per available seat kilometre"
170+
]
171+
},
172+
{
173+
"@id": "https://india-widebody-window.vercel.app/#website",
174+
"@type": "WebSite",
175+
"inLanguage": "en",
176+
"license": "https://opensource.org/licenses/MIT",
177+
"name": "India's Wide-Body Window",
178+
"publisher": {
179+
"@type": "Person",
180+
"name": "Anklesh Rawat",
181+
"url": "https://github.com/DogInfantry"
182+
},
183+
"url": "https://india-widebody-window.vercel.app"
184+
}
185+
]
186+
}
187+
</script>
26188
<link rel="stylesheet" href="assets/style.css"/>
27189
<script src="https://cdn.plot.ly/plotly-2.35.0.min.js" charset="utf-8"></script>
28190
<script src="https://unpkg.com/scrollama"></script>

‎docs/llms.txt‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# India's Wide-Body Window
2+
3+
> Where should Indian carriers deploy their next 100 long-haul aircraft, and can the India-Gulf corridor absorb them? A commercial aviation market entry case built on DGCA, Eurostat, IATA and World Bank data. The answer: compete with the Gulf hubs, do not fly more aircraft to them. Europe first, North America second, Gulf capacity roughly flat.
4+
5+
Author: Anklesh Rawat. Licence: MIT. Published 2026.
6+
Canonical: https://india-widebody-window.vercel.app
7+
Mirror: https://doginfantry.github.io/india-widebody-window/
8+
Source and data: https://github.com/DogInfantry/india-widebody-window
9+
10+
Every figure below is computed inside the repository from committed data, or carries a
11+
source URL and pull date in the data dictionary. Modelled figures are labelled as modelled
12+
on the chart face and a test fails the build if one is published without that label. Where
13+
two agencies disagree the conflict is reported rather than resolved away. The analysis
14+
changed direction ten times and each change is written up with the commit it happened in.
15+
16+
## The answer
17+
18+
Compete with the Gulf hubs. Do not fly more aircraft to them.
19+
20+
The India-Gulf corridor carries 50.9% of India's international sector traffic, 39.7M
21+
passengers in 2025, and is 4.1 times the size of India's entire direct Europe market. It is
22+
still the wrong place to put a wide-body, for three separate reasons. About 8.5M of those
23+
passengers a year are not going to the Gulf at all, they are connecting through Dubai, Doha
24+
or Abu Dhabi to somewhere else. The treaty room left at the two Gulf points with a published
25+
entitlement would absorb about 4% of the aircraft on firm order. And Gulf sectors are short
26+
enough that unit cost stays high, so the Gulf is the only corridor that cannot cover its own
27+
cost at IndiGo's achieved yield. Indian carriers win that traffic by flying past the Gulf,
28+
not to it: Europe first, North America second, Gulf capacity roughly flat.
29+
30+
## Key figures
31+
32+
- India international sector passengers, both directions, all carriers, 2025: 78.0M. Computed from DGCA.
33+
- Share of that traffic touching a Gulf point, 2025: 50.9%, which is 39.7M passengers. Computed from DGCA.
34+
- Share flown by Indian carriers, 2025: 45.9%, against 26.2% for Gulf carriers. Computed from DGCA.
35+
- Indian carrier share in 2015: 37.0%, against 32.7% for Gulf carriers. The premise reversed: Indian carriers are gaining, not losing.
36+
- IndiGo international passengers, 2025: 16.7M, against Air India's 10.7M.
37+
- IndiGo average international stage length, 2025: 2,643 km, against Air India's 5,316 km.
38+
- Passengers a year connecting through a Gulf hub to somewhere else: 8.5M. Modelled, and bounded below at 7.84M by IATA measurement, so the modelled figure is slightly conservative.
39+
- Firm wide-body order book: 140 aircraft, 80 Air India and 60 IndiGo, which is +78% on Indian carriers' current international capacity measured in ASK, and 1.94 times the growth needed to hold share.
40+
- Share of that order book the remaining Gulf treaty room could absorb: about 4%.
41+
- Yield headroom by corridor, 2025: Gulf -4.3%, South Asia -1.9%, East Asia +8.9%, Southeast Asia +11.2%, Africa +16.9%, Europe +21.3%, Oceania +29.5%, North America +31.5%.
42+
- India international passengers in 2030: a band of 96M to 109M across three independent methods. Reported as a band and never as an average, because the spread is the useful output.
43+
- India-Dubai seat entitlement already used: 88.8%. Abu Dhabi: 70.1%. The Gulf is not uniformly capped.
44+
- Gulf share of corridor revenue: 31%, against 52% of passengers.
45+
- IndiGo FY2026 EBITDAR margin: 17.8% as reported, and 27.3% excluding foreign exchange. Both figures must be read together or neither means anything.
46+
- Coverage of the target job posting: 82%, not 100%, and the missing items are named rather than engineered away.
47+
48+
## Definitions that change the answer
49+
50+
- Sector traffic counts a passenger on each leg. Origin-destination counts where the passenger is actually going. The Gulf is 50.9% of sectors and roughly 40% of origin-destination, and the eleven point gap is the connecting passenger. That gap is the case, not a discrepancy.
51+
- Capacity is measured in ASK, available seat kilometres, never in seats or aircraft. A seat is not capacity until you say how far and how often it flies, and this case turns on how far.
52+
- DGCA sums both directions and reports on a fiscal year. IATA counts departures only and reports on a calendar year. Comparing the wrong pair doubles the connect gap.
53+
54+
## Sources
55+
56+
- DGCA traffic statistics, via the community mirror at github.com/Vonter/india-aviation-traffic, ODbL. Five datasets, fresh to May 2026, pulled 2026-08-15. The spine of the analysis.
57+
- Eurostat avia_par, EU reuse policy. The European end of India-Europe routes, used to check DGCA from the opposite direction. The two agree to 2.6% across seven countries.
58+
- IATA, Aviation in India, free publication. India's departing international origin-destination split by region and by country. Route level data is a separate paid product.
59+
- World Bank Open Data, CC BY 4.0. Income, population and air travel propensity across twelve peer countries.
60+
- OurAirports, CC0. Airport reference data and coordinates.
61+
62+
## Written analysis
63+
64+
- [Storyline](https://github.com/DogInfantry/india-widebody-window/blob/main/docs/storyline.md): the client brief, the recommendation and the SCQA under it.
65+
- [Recommendation](https://github.com/DogInfantry/india-widebody-window/blob/main/docs/recommendation.md): five costed options, a roadmap, a nine row risk register and leading indicators.
66+
- [Pivot log](https://github.com/DogInfantry/india-widebody-window/blob/main/docs/pivot_log.md): ten documented changes of mind, each citing the commit it happened in.
67+
- [Hypothesis tree](https://github.com/DogInfantry/india-widebody-window/blob/main/docs/hypothesis_tree.md): the decomposition, including the branches still open.
68+
- [Methodology](https://github.com/DogInfantry/india-widebody-window/blob/main/docs/methodology.md): frameworks, limits, what the data cannot tell you, and a published claim that was retracted.
69+
- [Survey design](https://github.com/DogInfantry/india-widebody-window/blob/main/docs/survey_design.md): a conjoint instrument for the softest number in the case. Designed, not fielded.
70+
- [Data dictionary](https://github.com/DogInfantry/india-widebody-window/blob/main/data/data_dictionary.md): every field with its source, pull date and reliability grade.
71+
- [Coverage](https://github.com/DogInfantry/india-widebody-window/blob/main/docs/coverage.md): the job posting mapped to artifacts, gaps included.
72+
- [Alternative B](https://github.com/DogInfantry/india-widebody-window/blob/main/docs/alternative_b_datacenters.md): the case that lost, and why.
73+
- [Roadmap](https://github.com/DogInfantry/india-widebody-window/blob/main/ROADMAP.md): what is deliberately unfinished, separating paywalled from merely unbuilt.
74+
75+
## Reading surfaces
76+
77+
- [The site](https://india-widebody-window.vercel.app): the delivery layer, seven routes, 26 exhibits.
78+
- [The scrollytelling analysis](https://doginfantry.github.io/india-widebody-window/): eighteen charts, one scrolly step per claim.
79+
- [The deck](https://doginfantry.github.io/india-widebody-window/deck.html): one claim per screen.
80+
- [The print edition](https://doginfantry.github.io/india-widebody-window/report.html): the same analysis in reading order.
81+
82+
## What would make this wrong
83+
84+
The softest input is the Gulf six origin-destination share. No agency publishes it, so it is
85+
gated as unverifiable, corroborated at region level and bounded by measurement, and it is
86+
still the likeliest reason the recommendation fails. Wide-body lease rates are the largest
87+
unquantified cost and no free primary exists for them. Route level cross-checking against a
88+
second agency covers only 5.6% of India's international traffic, all of it Europe, because
89+
no Gulf authority publishes route statistics.
90+
91+
## How to cite
92+
93+
Rawat, A. (2026). India's Wide-Body Window: where should Indian carriers deploy their next
94+
100 long-haul aircraft, and can the India-Gulf corridor absorb them?
95+
https://india-widebody-window.vercel.app

‎tests/test_delivery.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,53 @@ def test_interactive_elements_keep_a_pointer_cursor():
401401
"`clickable-marks`. Clicking a bar filters every exhibit on the page, which "
402402
"is worth nothing if the reader cannot tell it is clickable."
403403
)
404+
405+
406+
def test_both_surfaces_publish_the_same_valid_structured_data():
407+
"""Invalid JSON-LD is worse than none, and it fails silently in both directions.
408+
409+
A malformed block is skipped by every crawler that reads it and by every
410+
validator nobody runs, so the page keeps rendering and the metadata simply
411+
does not exist. Two copies of it, one in `docs/index.html` and one in
412+
`web/app/layout.tsx`, can also drift apart without anything noticing, which
413+
is the same shape as the exhibit count this file was written for.
414+
415+
The canonical URL is the assertion that matters. The mirror on GitHub Pages
416+
serves the same argument as the Vercel site, and if it declared itself
417+
canonical the two hosts would compete for the same signal. The repo sidebar
418+
made exactly that mistake by pointing `homepageUrl` at the mirror.
419+
"""
420+
import json
421+
422+
index = (ROOT / "docs" / "index.html").read_text(encoding="utf-8")
423+
layout = (ROOT / "web" / "app" / "layout.tsx").read_text(encoding="utf-8")
424+
425+
found = re.search(
426+
r'<script type="application/ld\+json">\s*(.*?)\s*</script>', index, flags=re.S
427+
)
428+
assert found, "docs/index.html publishes no JSON-LD"
429+
static = json.loads(found.group(1))
430+
431+
start = layout.index("const JSON_LD = ") + len("const JSON_LD = ")
432+
app = json.loads(layout[start : layout.index("\n};", start) + 2])
433+
434+
assert static == app, (
435+
"the JSON-LD in docs/index.html and web/app/layout.tsx have drifted apart"
436+
)
437+
438+
canonical = "https://india-widebody-window.vercel.app"
439+
urls = {node.get("url") for node in static["@graph"] if node.get("url")}
440+
assert canonical in urls, "the structured data does not name the canonical site"
441+
assert not any(u and "github.io" in u for u in urls), (
442+
"the structured data points at the GitHub Pages mirror. The mirror must "
443+
"declare the Vercel origin as canonical or the two hosts compete."
444+
)
445+
446+
types = {node["@type"] for node in static["@graph"]}
447+
assert {"Report", "Dataset", "WebSite"} <= types, f"structured data types are {types}"
448+
449+
dataset = next(n for n in static["@graph"] if n["@type"] == "Dataset")
450+
assert len(dataset["isBasedOn"]) == 5, (
451+
"the Dataset should name all five upstream sources, so a reader or a crawler "
452+
"can see the provenance without opening the data dictionary"
453+
)

‎tests/test_narrative.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@
6565
# it documents provenance rather than making the argument, and it carries
6666
# historical values on purpose.
6767
CORPUS_FILES = (
68-
["README.md", "CLAUDE.md", "ROADMAP.md"]
68+
# `llms.txt` is a figure-dense summary written for retrieval, served from
69+
# both roots, and it inherits this guard on the way in rather than two
70+
# sessions later the way `web/` did.
71+
["README.md", "CLAUDE.md", "ROADMAP.md", "docs/llms.txt", "web/public/llms.txt"]
6972
+ [f"docs/{p.name}" for p in sorted((ROOT / "docs").glob("*.md"))]
7073
+ [f"docs/{p.name}" for p in sorted((ROOT / "docs").glob("*.html"))]
7174
# The delivery layer carries prose too, and for the whole life of the app it

0 commit comments

Comments
 (0)