ontology_release_build.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. r"""本体发布层 r1/r2 的**观澜自算**生成端(源代码化清单第 ① 件)。
  4. 网关的 `/release/` 只读暴露两层(`scripts/guanlan_gateway.py` 的 `dirs = {'r1': ont/release_r1, 'r2': ont/release_r2}`):
  5. `/release/` 回两层的 manifest,`/release/<r>/manifest.json` 回该层清单,`/release/<r>/objects.json` 回该层对象。
  6. 这两层此前是**随包件**(全库 0 处写入方)⇒ 清过产物、只从 data/raw 重算的机器上 `/release/` 两件必然 503。
  7. 本器把两层由**观澜自算**出来(同一份本体对象库 → 契约层 / 分层覆盖层),并如实写清口径与来历:
  8. r1(契约层): objects.json(对象库快照)+ manifest.json(计数、类型分布、源件 sha16、口径)
  9. r2(分层覆盖层): layering.json(按类型/字段的覆盖声明)+ objects.json(同 r1 快照, 供只读对比)
  10. ★口径纪律: 这里**不新造判断**, 只是把 `ontology/objects.json` 按发布层的形态投影出来;
  11. manifest 里写明 `source.sha16`, 谁都能核出这两层是同一份对象库的投影。
  12. 用法: python scripts/ontology_release_build.py [--status]
  13. """
  14. from __future__ import annotations
  15. import argparse
  16. import hashlib
  17. import json
  18. import pathlib
  19. import sys
  20. import time
  21. from collections import Counter
  22. ROOT = pathlib.Path(__file__).resolve().parents[1]
  23. sys.path.insert(0, str(ROOT))
  24. sys.path.insert(0, str(ROOT / 'scripts'))
  25. from src import paths as P # noqa: E402
  26. LAYERS = ('release_r1', 'release_r2')
  27. def status(farm: str | None = None) -> int:
  28. ont = P.ont(farm)
  29. out = {}
  30. for L in LAYERS:
  31. m = ont / L / 'manifest.json'
  32. out[L] = dict(exists=m.is_file(),
  33. built=(json.loads(m.read_text(encoding='utf-8')).get('built') if m.is_file() else None))
  34. print(json.dumps(dict(root=P.rel(ont), layers=out,
  35. note='不在位 ⇒ 网关 /release/ 两件回 503(如实); 跑本器可由本体对象库生成'),
  36. ensure_ascii=False))
  37. return 0
  38. def build(farm: str | None = None) -> int:
  39. ont = P.ont(farm)
  40. src = ont / 'objects.json'
  41. if not src.is_file():
  42. print(f'[X] 缺 {P.rel(src)} —— 发布层是它的投影, 先跑本体链(⑦ kb_ingest → populate)')
  43. return 2
  44. raw = src.read_bytes()
  45. objs = json.loads(raw.decode('utf-8'))
  46. by_type = Counter((v or {}).get('type') or '?' for v in objs.values())
  47. sha16 = hashlib.sha256(raw).hexdigest()[:16]
  48. built = time.strftime('%Y-%m-%d %H:%M:%S')
  49. common = dict(source=dict(path=P.rel(src), sha16=sha16, n_objects=len(objs)),
  50. counts=dict(by_type=dict(by_type.most_common()), total=len(objs)),
  51. built=built, generated_by='scripts/ontology_release_build.py (观澜自算)',
  52. note='只读发布层: 由本体对象库**投影**得到, 不含新判断; source.sha16 可核同一份对象库')
  53. for L in LAYERS:
  54. d = ont / L
  55. d.mkdir(parents=True, exist_ok=True)
  56. (d / 'objects.json').write_bytes(raw) # 投影: 同源同 sha
  57. man = dict(common, release=L.replace('release_', ''))
  58. if L == 'release_r2':
  59. # r2 = 分层覆盖层: 声明"哪些类型允许被上层覆盖", 供只读对比(不合并主库 —— E9/E10 裁定)
  60. lay = {t: dict(overlay_fields=['props'], merge='层内覆盖', basis='类型分层')
  61. for t, _n in by_type.most_common()}
  62. (d / 'layering.json').write_text(json.dumps(dict(
  63. schema='guanlan-ontology-layering/v1', built=built, source_sha16=sha16,
  64. note='分层覆盖声明: 仅描述允许覆盖的类型与字段; 运行期不合并主库(E9/E10 裁定)',
  65. layers=lay), ensure_ascii=False, indent=1), encoding='utf-8')
  66. man['files'] = ['manifest.json', 'objects.json', 'layering.json']
  67. else:
  68. man['files'] = ['manifest.json', 'objects.json']
  69. (d / 'manifest.json').write_text(json.dumps(man, ensure_ascii=False, indent=1), encoding='utf-8')
  70. print(f'已写 {P.rel(d)}: 对象 {len(objs)} 件 · 类型 {len(by_type)} 种 · 源 sha16 {sha16}')
  71. try:
  72. from src import derived_manifest as DM
  73. DM.record(P.out_root(farm), {
  74. f'ontology/release_r1/manifest.json': 'scripts/ontology_release_build.py (对象库投影)',
  75. f'ontology/release_r1/objects.json': 'scripts/ontology_release_build.py (对象库投影, 同 sha)',
  76. f'ontology/release_r2/manifest.json': 'scripts/ontology_release_build.py (对象库投影)',
  77. f'ontology/release_r2/objects.json': 'scripts/ontology_release_build.py (对象库投影, 同 sha)',
  78. f'ontology/release_r2/layering.json': 'scripts/ontology_release_build.py (分层覆盖声明)',
  79. }, by='ontology_release_build')
  80. print(' 已自登记 → _derived_manifest.json')
  81. except Exception as e:
  82. print(f' [i] 自登记跳过: {type(e).__name__}: {e}')
  83. return 0
  84. def main() -> int:
  85. ap = argparse.ArgumentParser(description='本体发布层 r1/r2 的观澜自算生成端')
  86. ap.add_argument('--farm', default=None)
  87. ap.add_argument('--status', action='store_true')
  88. a = ap.parse_args()
  89. return status(a.farm) if a.status else build(a.farm)
  90. if __name__ == '__main__':
  91. for _s in (sys.stdout, sys.stderr):
  92. try:
  93. _s.reconfigure(errors='replace')
  94. except Exception:
  95. pass
  96. sys.exit(main())