# 中国历史时空地图 · 数据规范 v1.1

本规范定义「全史壳层 + 分卷地图」的数据契约。目标有三条：

1. **主壳零改动**：新增时期只提供数据包与 `period.json`，总览页自动出现入口。
2. **史实可追溯**：事件必须有年份、地点、来源与置信度；没有来源的内容不得进入正式数据。
3. **离线可运行**：分卷数据以 classic script 形式装载，`file://` 双击即可打开。

---

## 0. 目录与命名

```
periods/<period-id>/
  period.json        时期元数据（必需）
  events.json        事件（ready 卷必需）
  regimes.json       政权与疆域（ready 卷必需）
  places.json        地名索引（可选，P2 规范化用）
  sources.json       史料源清单（ready 卷必需）
  prefectures.json   州府两级界线（可选，按需）
```

- `<period-id>`：小写字母、数字、连字符，例如 `liao-song`、`qin-han`，必须与 `period.json.id` 一致。
- 所有 JSON 使用 UTF-8，不带 BOM。
- 年份统一用整数：公元前用负数，**没有公元 0 年**（前 1 年之后直接是公元 1 年）。
- 坐标统一为 WGS84 经纬度十进制度，保留 2 位小数；本项目分卷视口约束为 **经度 50–140、纬度 15–55**。
- 面积、距离单位分别为 km²、km；不写单位换算。

---

## 1. `period.json` · 时期元数据

```json
{
  "id": "qin-han",
  "order": 2,
  "title": "秦汉",
  "subtitle": "大一统的建立",
  "era": "上古 · 帝国",
  "range": [-221, 220],
  "status": "planned",
  "entry": null,
  "color": "#7a5c46",
  "tagline": "郡县、驰道与丝路，第一次把「天下」连成一张网。",
  "summary": "秦以郡县制取代封建，汉在秦制上修补四百年……",
  "anchor": { "name": "咸阳", "lon": 108.71, "lat": 34.33 },
  "stats": null,
  "highlights": [
    {
      "year": -221,
      "title": "秦灭六国，统一天下",
      "place": "咸阳（今陕西咸阳）",
      "lon": 108.71,
      "lat": 34.33,
      "precision": "approx",
      "source": "《史记·秦始皇本纪》"
    }
  ],
  "regimes": [
    { "name": "秦", "from": -221, "to": -207, "capital": "咸阳", "note": "郡县制、书同文、车同轨" }
  ],
  "capitals": [
    { "name": "咸阳", "polity": "秦", "lon": 108.71, "lat": 34.33 }
  ],
  "bands": [
    { "name": "秦", "from": -221, "to": -207, "row": 0, "note": "前 221–前 207" }
  ],
  "coverage": ["秦郡县与驰道、长城、灵渠"],
  "sources": ["《史记》秦始皇本纪、大宛列传"],
  "notes": ["秦、西汉疆域将按《中国历史地图集》分幅数字化。"]
}
```

| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 与目录名一致，`^[a-z0-9-]+$` |
| `order` | int | ✓ | 全史排序，1–99；总览左栏与时间轴按此排序 |
| `title` | string | ✓ | 卷名，2–6 字最佳 |
| `subtitle` | string | ✓ | 四字副题，如「多政权并立」 |
| `era` | string |  | 时代分组，如「中古 · 并立」 |
| `range` | [int,int] | ✓ | 起止年，`from < to`；负数为公元前 |
| `status` | enum | ✓ | `ready`（已开放）/ `planned`（规划中） |
| `entry` | string\|null | ✓ | 相对平台根目录的分卷入口；`planned` 必须为 `null` |
| `color` | hex | ✓ | 时间轴与锚点主色，`#rrggbb` |
| `tagline` | string | ✓ | 一句话定位，显示在详情卡 |
| `summary` | string | ✓ | 80–160 字简介，不堆砌事件 |
| `anchor` | object |  | 总览地图锚点 `{name,lon,lat}`；八卷各一个，应分散 |
| `stats` | object\|null |  | `ready` 卷统计：`polities/events/emperors/eras/prefectureTiers` |
| `highlights` | array |  | 3 条左右「本期大事」，用于总览地图与右栏 |
| `regimes` | array |  | 主要政权简表 `{name,from,to,capital,note}` |
| `capitals` | array |  | 都城点位 `{name,polity,lon,lat}`；0.6° 内自动合并标注 |
| `bands` | array |  | 全史时间轴分段 `{name,from,to,row,note}`，`row=0` 中原、`1` 并立 |
| `coverage` | string[] |  | 规划范围 / 本卷收录，4–6 条 |
| `sources` | string[] |  | 面向读者列出的史料与数据源 |
| `notes` | string[] |  | 边界说明、口径限制 |

**校验规则**

- `planned` 卷只允许存在 `period.json`；`ready` 卷必须有 `events/regimes/sources`。
- `prefectures.json` 可选；没有州府数据时该图层整层隐藏，不要拿别期州界冒充。
- `entry` 指向的文件必须存在。
- `highlights[].year` 必须落在 `range` 内；坐标必须在视口内且标注 `precision`。
- `bands` 不得有 `from >= to`；同名分段可跨卷重复，但同一 `period` 内不应重复。

---

## 2. `events.json` · 事件

```json
{
  "id": "e-001",
  "year": 1005,
  "title": "澶渊之盟",
  "kind": "politics",
  "dyn": "song",
  "desc": "宋辽在澶州议和，约为兄弟之国，宋岁输银绢，边境进入百余年相对和平。",
  "source": "《宋史·真宗本纪》",
  "place": {
    "name": "澶州（今河南濮阳）",
    "lon": 115.0,
    "lat": 35.7,
    "precision": "approx",
    "modern": "河南省濮阳市"
  },
  "confidence": "high",
  "note": null
}
```

| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 全卷唯一，建议 `e-###` |
| `year` | int | ✓ | 事件发生年；跨年事件取起始年 |
| `title` | string | ✓ | 同题可出现在不同政权下（如宋辽各自记载），但 `id` 必须唯一 |
| `kind` | enum | ✓ | `politics` / `war` / `reform` / `economy` / `culture` |
| `dyn` | string | ✓ | 所属叙事朝代 id，与 `regimes.json` 的 `id` 或卷内朝代键对应 |
| `desc` | string | ✓ | 40–120 字；写清因果，不写评价 |
| `source` | string | ✓ | 书名 + 篇目；禁止「通行史实」「据载」等泛化来源 |
| `place` | object\|null | ✓ | 事件地点；`name` 为古今对照，`lon/lat` 为近似点位 |
| `place.precision` | enum | ✓ | `exact`（遗址/城址明确）、`approx`（约今某地）、`region`（仅到区域） |
| `confidence` | enum | ✓ | `high`（正史明确）、`medium`（史实有争议但主流）、`low`（存疑/传说） |
| `note` | string\|null |  | 争议说明、纪年差异、地名变迁 |

**迁移债务**：916–1279 卷的 120 条事件来自既有页面，坐标是原页近似定位，`place.name` 暂为 `null`。校验器会汇总为一条待补提醒，列为 P2 地名规范化任务；新增事件必须填写 `place.name`。

---

## 3. `regimes.json` · 政权与疆域

```json
{
  "id": "song",
  "name": "北宋",
  "full": "大宋",
  "from": 960,
  "to": 1127,
  "capital": "东京开封府",
  "color": "#5f9e7d",
  "line": "#2f5f48",
  "desc": "……",
  "relation": "……",
  "label": [112.53, 34.94],
  "visibleTo": 1127,
  "geoSource": "atlas:beisong",
  "frames": [
    {
      "year": 960,
      "parts": [[[103.35, 33.74], [103.28, 33.75], [103.15, 33.82]]]
    }
  ],
  "parts": [[[103.35, 33.74], [103.28, 33.75], [103.15, 33.82]]]
}
```

| 字段 | 类型 | 必需 | 说明 |
|---|---|---|---|
| `id` | string | ✓ | 卷内唯一 |
| `name` / `full` | string | ✓ | 简称 / 国号全称 |
| `from` / `to` | int | ✓ | 起止年；可超出 `period.range`（前朝残余、跨卷政权） |
| `capital` | string | ✓ | 都城；多都写「A → B」 |
| `color` / `line` | hex | ✓ | 填色 / 界线色 |
| `label` | [lon,lat] | ✓ | 政权名注记点，应落在本政权面内 |
| `frames` | array |  | 史实快照 `{year, parts}`；按年份升序，不得超出起止年 |
| `parts` | array |  | 无快照时的静态边界；与 `frames` 至少有一个 |
| `visibleTo` | int |  | 实际显示截止年（如辽在 1125 年已被金取代，可设 1124） |
| `geoSource` | string |  | 边界数据来源标识，如 `atlas:beisong` |
| `desc` / `relation` | string |  | 来历 / 与同期政权关系 |

**几何约定**

- `parts` 是「多环多边形」：`[[[lon,lat],…], …]`，每个环 ≥3 点，不要求首尾重复。
- 坐标为经纬度，落在 50–140E / 15–55N；跨出视口的西境应做裁剪，不得钳到边缘拉出假边界。
- 快照是**史实年份的离散切换**，不做逐年插值；相邻快照几何相同可省略。
- 面积过小的碎环（<0.08 平方度）在总览简化时会被丢弃，分卷可保留。

---

## 4. `places.json` · 地名索引

```json
{
  "id": "p-001",
  "name": "澶州",
  "modern": "河南省濮阳市",
  "lon": 115.0,
  "lat": 35.7,
  "precision": "approx",
  "legacyCoord": false
}
```

P1 阶段事件内嵌 `place`，`places.json` 作为后续跨卷地名去重与检索的索引；`legacyCoord=true` 表示坐标来自旧页近似定位、地名尚未规范化。

---

## 5. `sources.json` · 史料源清单

```json
{
  "id": "s-001",
  "title": "《宋史·真宗本纪》",
  "type": "primary",
  "note": "事件出处，沿用原整理页所标篇目",
  "url": null
}
```

| `type` | 含义 |
|---|---|
| `primary` | 正史、实录、编年、文集等原始文献 |
| `secondary` | 近现代研究著作 |
| `atlas` | 历史地图集 / 政区数字化数据 |
| `modern` | 现代地理数据（DEM、河网、OSM 等），仅作底图 |

事件 `source` 必须能在 `sources.json` 中找到同名 `title`；校验器对未登记来源给出警告。

---

## 6. `prefectures.json` · 州府界线

```json
{
  "regions": {
    "京畿路": ["京畿路", 114.57, 33.95, [[115.31, 36.14], [115.37, 36.14]]]
  },
  "sets": [
    { "p": [["开封府", "府", 114.31, 34.8, [[...]]]], "g": ["京畿路"] }
  ],
  "years": [[960, 0], [979, 1]]
}
```

- `regions`：路级多边形与注记点，键为「路名」或「路名@年份」。
- `sets[].p`：州/府/军/监 `[名, 类别, 注记经, 注记纬, 环…]`。
- `sets[].g`：该档包含的路键。
- `years`：`[起始年, 档位下标]`，单调递增；运行时取「不晚于当前年的最后一档」。

---

## 7. 校验与构建

```bash
node tools/validate_period.mjs            # 全部卷
node tools/validate_period.mjs liao-song  # 单卷
node tools/build.mjs                      # 生成 data/catalog.js 与示例快照
node tools/build_volume.mjs liao-song 916-1279   # 由源 JSON 生成运行包
```

`validate_period.mjs` 检查：

- `period.json` 必填字段、range、status、entry 存在性；
- 事件 id 唯一、年份有序且在卷范围内、来源非空、坐标与精度合法、置信度枚举；
- 政权 id 唯一、起止合法、边界环坐标在视口内、快照年份合法；
- 史料源 id 唯一；州府若提供则 14 档、年份表非空；
- `stats` 与实际条数一致。

**失败即阻断**：有任何 `✗` 时脚本以非 0 退出，不得进入发布包。

---

## 8. 版本与兼容

- 本规范版本 `1.0`，与平台 `ATLAS v1.0.0`、916–1279 卷数据版本 `1.0` 对应。
- 新增字段必须可选，或在 `period.json` 增加 `schemaVersion` 后由校验器分支处理。
- 不得删除既有字段语义；字段重命名需提供迁移脚本并保留旧字段读取兼容。
- 事件的 `id` 一旦发布不再变更；标题可修订，但需在 `note` 记录旧题。

---

## 9. 合规红线

1. 历史疆域图必须标注「历史空间示意，不代表现代国界」。
2. 公开发布前按中国地图管理要求使用合规底图并送审；当前总览底图为不含国界线的地形晕渲。
3. 不绘制、不暗示任何违反国家主权与领土完整的边界表述。
4. 现代河道（长江、黄河）只作地理参照，不冒充历史故道；宋代 1048 年北流、1128 年夺淮等改道须在图注中说明。
5. 不确定的坐标标「约」，有争议的结论标「争议」，不得用推测补空白。


---

## 7. v1.1 增补（八卷实践）

### 7.1 `figures.json` · 人物

```json
{
  "id": "wei-zheng",
  "name": "魏征",
  "courtesy": "玄成",
  "from": 580,
  "to": 643,
  "role": "名臣",
  "bio": "以直谏著称，辅佐太宗开创贞观之治，主持修撰《隋书》。",
  "place": "changan",
  "activeYear": 627,
  "related": ["玄武门之变", "贞观之治始"],
  "lon": 108.94,
  "lat": 34.34,
  "placeName": "长安（今陕西西安）"
}
```

- `role`：君主 / 名臣 / 名将 / 文人 / 宗教 / 改革者 / 割据者 / 工程。
- `activeYear` 必须落在本卷 `range` 内；`related` 为事件标题，构建时解析为事件 id。
- `lon/lat/placeName` 由构建脚本从 `_src/places.mjs` 展开，源文件可不写。

### 7.2 `period.json` 新增字段

```json
{
  "overview": { "year": 741, "label": "开元盛世 · 唐、吐蕃、后突厥并立" },
  "noEraNote": "上古无汉式年号，纪年以王位与在位年计。",
  "geoFrom": -403,
  "geoNote": "前 403 年以前图集无疆域分幅，仅显示事件与都城示意",
  "viewer": {
    "tabs": [{ "dyn": "tang", "label": "唐", "from": 618, "to": 907 }],
    "figure": true,
    "defaultDyn": "tang",
    "regionLabel": "道",
    "prefDefault": true,
    "regimeDyn": { "tang": "tang", "zhou": "tang" },
    "cities": [["长安", 108.94, 34.34, -34, 4]],
    "palette": { "tang": ["#5b8c8c", "#6d8fa3"] }
  },
  "stats": { "polities": 51, "events": 164, "emperors": 40, "eras": 100, "prefectureTiers": 3, "figures": 36 }
}
```

- `overview`：主页概略图的代表年份与标题；必须落在 `range` 内，且该年至少有一个有效政权。
- `viewer.tabs`：朝代 tab；`dyn` 与 `emperors[].dyn`、`events[].dyn` 对应。
- `viewer.regimeDyn`：政权 id → tab 的 `dyn`，点击政权时自动切 tab。
- `viewer.cities`：`[名称, lon, lat, dx, dy]`，按卷配置，避免先秦显示宋代的东京/西京。
- `viewer.prefDefault`：`false` 时该卷默认关闭州府层（先秦、移动端）。
- `geoFrom`：该年以前无疆域帧，地图显示事件与都城并提示；`geoNote` 为提示文案。

### 7.3 `regimes.json` 新增字段

- `hidden: [[from, to]]`：政权在区间内隐藏（如武周代唐、王莽新朝）。
- `admin: true`：都护府/羁縻类政权，填色更淡。
- `frames[].label`：该帧注记点；缺省用最大环中心。
- `frameYears`：帧年份索引，供调试与审计。

### 7.4 `emperors.json`

- `primary: true`：多政权并立时，作为该年的主政权帝王优先展示（先秦秦君、三国曹魏等）。
- `eras: []` 合法：上古、秦、蒙古大汗等无汉式年号；面板显示 `noEraNote`。

### 7.5 `events.json`

- `place.name` / `place.modern` 为必填，格式「古名」「今地」。
- `related` 存事件 id 数组，构建时由 `_src/chains.mjs` 生成，不再手写标题。
- 年份可为负数（公元前），UI 统一显示「前 N 年」。

### 7.6 构建顺序

```bash
node tools/build_geo.mjs <period-id>
node tools/build_content.mjs <period-id>
node tools/build_volume.mjs <period-id> <volume-dir>
python tools/patch_viewer.py <period-id> <volume-dir> <title> <default-year>
node tools/build_overview_geo.mjs
node tools/build.mjs
```
