@amaster.ai/pi-lark 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/README.md +5 -1
  2. package/dist/config.d.ts +1 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +2 -2
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/package.json +4 -4
  10. package/skills/lark-approval/references/lark-approval-initiate.md +2 -5
  11. package/skills/lark-approval/references/lark-approval-instances-initiated.md +6 -0
  12. package/skills/lark-approval/references/lark-approval-tasks-query.md +9 -0
  13. package/skills/lark-approval/references/lark-approval-tasks-rollback.md +8 -2
  14. package/skills/lark-apps/SKILL.md +46 -16
  15. package/skills/lark-apps/creative-design/agents/assets/vision-probe.png +0 -0
  16. package/skills/lark-apps/creative-design/agents/fork-verifier-agent.md +71 -0
  17. package/skills/lark-apps/creative-design/agents/vision-probe-agent.md +41 -0
  18. package/skills/lark-apps/creative-design/assets/index.html +27 -0
  19. package/skills/lark-apps/creative-design/creative-design.md +239 -0
  20. package/skills/lark-apps/creative-design/references/aily.md +39 -0
  21. package/skills/lark-apps/creative-design/references/animated-video.md +34 -0
  22. package/skills/lark-apps/creative-design/references/charts.md +165 -0
  23. package/skills/lark-apps/creative-design/references/claude.md +36 -0
  24. package/skills/lark-apps/creative-design/references/codex.md +32 -0
  25. package/skills/lark-apps/creative-design/references/data-report.md +108 -0
  26. package/skills/lark-apps/creative-design/references/frontend-design.md +71 -0
  27. package/skills/lark-apps/creative-design/references/hi-fi-design.md +32 -0
  28. package/skills/lark-apps/creative-design/references/interactive-prototype.md +24 -0
  29. package/skills/lark-apps/creative-design/references/make-a-deck.md +133 -0
  30. package/skills/lark-apps/creative-design/references/visual-exposure.md +82 -0
  31. package/skills/lark-apps/creative-design/references/wireframe.md +14 -0
  32. package/skills/lark-apps/creative-design/starter-components/android-frame.jsx +188 -0
  33. package/skills/lark-apps/creative-design/starter-components/animations.jsx +773 -0
  34. package/skills/lark-apps/creative-design/starter-components/browser-window.jsx +122 -0
  35. package/skills/lark-apps/creative-design/starter-components/deck-stage.js +2483 -0
  36. package/skills/lark-apps/creative-design/starter-components/design-canvas.jsx +1432 -0
  37. package/skills/lark-apps/creative-design/starter-components/ios-frame.jsx +270 -0
  38. package/skills/lark-apps/creative-design/starter-components/macos-window.jsx +197 -0
  39. package/skills/lark-apps/creative-design/starter-components/tweaks-panel.jsx +752 -0
  40. package/skills/lark-apps/references/lark-apps-access-scope-set.md +1 -1
  41. package/skills/lark-apps/references/lark-apps-automation.md +242 -0
  42. package/skills/lark-apps/references/lark-apps-cache.md +61 -0
  43. package/skills/lark-apps/references/lark-apps-cloud-dev.md +0 -1
  44. package/skills/lark-apps/references/lark-apps-create.md +1 -2
  45. package/skills/lark-apps/references/lark-apps-db-execute.md +186 -2
  46. package/skills/lark-apps/references/lark-apps-db.md +4 -4
  47. package/skills/lark-apps/references/lark-apps-env-pull.md +1 -1
  48. package/skills/lark-apps/references/lark-apps-file.md +2 -2
  49. package/skills/lark-apps/references/lark-apps-get.md +43 -0
  50. package/skills/lark-apps/references/lark-apps-git-credential.md +1 -1
  51. package/skills/lark-apps/references/lark-apps-html-publish.md +5 -4
  52. package/skills/lark-apps/references/lark-apps-init.md +2 -3
  53. package/skills/lark-apps/references/lark-apps-list.md +1 -1
  54. package/skills/lark-apps/references/lark-apps-local-dev.md +54 -11
  55. package/skills/lark-apps/references/lark-apps-openapi-key.md +1 -1
  56. package/skills/lark-apps/references/lark-apps-release-create.md +5 -3
  57. package/skills/lark-apps/references/lark-apps-release-get.md +3 -3
  58. package/skills/lark-apps/references/lark-apps-role.md +133 -0
  59. package/skills/lark-base/SKILL.md +26 -15
  60. package/skills/lark-base/references/dashboard-block-data-config.md +28 -2
  61. package/skills/lark-base/references/lark-base-cell-value.md +12 -7
  62. package/skills/lark-base/references/lark-base-dashboard-block-get-data.md +7 -7
  63. package/skills/lark-base/references/lark-base-dashboard.md +11 -2
  64. package/skills/lark-base/references/lark-base-data-query.md +20 -11
  65. package/skills/lark-base/references/lark-base-field-create.md +8 -2
  66. package/skills/lark-base/references/lark-base-field-json.md +56 -19
  67. package/skills/lark-base/references/lark-base-field-update.md +21 -3
  68. package/skills/lark-base/references/lark-base-filter-condition.md +179 -0
  69. package/skills/lark-base/references/lark-base-form-questions-create.md +40 -7
  70. package/skills/lark-base/references/lark-base-form-questions-update.md +73 -20
  71. package/skills/lark-base/references/lark-base-form-submit.md +16 -7
  72. package/skills/lark-base/references/lark-base-record-batch-create.md +12 -10
  73. package/skills/lark-base/references/lark-base-record-batch-update.md +11 -9
  74. package/skills/lark-base/references/lark-base-record-upsert.md +1 -1
  75. package/skills/lark-base/references/lark-base-role-guide.md +11 -0
  76. package/skills/lark-base/references/lark-base-view-set-filter.md +14 -138
  77. package/skills/lark-base/references/role-config.md +31 -5
  78. package/skills/lark-calendar/SKILL.md +101 -37
  79. package/skills/lark-calendar/references/lark-calendar-create.md +13 -43
  80. package/skills/lark-calendar/references/lark-calendar-recurring.md +1 -0
  81. package/skills/lark-calendar/references/lark-calendar-room-find.md +7 -10
  82. package/skills/lark-calendar/references/lark-calendar-rsvp.md +1 -5
  83. package/skills/lark-calendar/references/lark-calendar-schedule-clear-time.md +60 -0
  84. package/skills/lark-calendar/references/lark-calendar-schedule-fuzzy-time.md +88 -0
  85. package/skills/lark-calendar/references/lark-calendar-schedule-meeting.md +67 -210
  86. package/skills/lark-calendar/references/lark-calendar-suggestion.md +2 -6
  87. package/skills/lark-calendar/references/lark-calendar-update.md +12 -11
  88. package/skills/lark-contact/SKILL.md +19 -3
  89. package/skills/lark-contact/references/lark-contact-search-bot.md +60 -0
  90. package/skills/lark-doc/SKILL.md +1 -1
  91. package/skills/lark-doc/references/lark-doc-fetch.md +14 -4
  92. package/skills/lark-doc/references/lark-doc-mindnote.md +17 -2
  93. package/skills/lark-doc/references/lark-doc-whiteboard.md +13 -8
  94. package/skills/lark-doc/references/lark-doc-xml-extended-blocks.md +76 -0
  95. package/skills/lark-doc/references/lark-doc-xml.md +6 -4
  96. package/skills/lark-drive/SKILL.md +35 -43
  97. package/skills/lark-drive/references/lark-drive-add-comment.md +2 -4
  98. package/skills/lark-drive/references/lark-drive-add-reply.md +47 -0
  99. package/skills/lark-drive/references/lark-drive-apply-permission.md +2 -2
  100. package/skills/lark-drive/references/lark-drive-batch-query-comments.md +46 -0
  101. package/skills/lark-drive/references/lark-drive-comment-content.md +50 -0
  102. package/skills/lark-drive/references/lark-drive-comment-location.md +18 -12
  103. package/skills/lark-drive/references/lark-drive-delete-reply.md +48 -0
  104. package/skills/lark-drive/references/lark-drive-delete.md +35 -11
  105. package/skills/lark-drive/references/lark-drive-download.md +5 -1
  106. package/skills/lark-drive/references/lark-drive-export.md +39 -10
  107. package/skills/lark-drive/references/lark-drive-files-list.md +27 -2
  108. package/skills/lark-drive/references/lark-drive-inspect.md +2 -0
  109. package/skills/lark-drive/references/lark-drive-list-comments.md +82 -0
  110. package/skills/lark-drive/references/lark-drive-list-replies.md +54 -0
  111. package/skills/lark-drive/references/lark-drive-member-add.md +3 -3
  112. package/skills/lark-drive/references/lark-drive-member-list.md +65 -0
  113. package/skills/lark-drive/references/lark-drive-move.md +5 -3
  114. package/skills/lark-drive/references/lark-drive-permission-get-setting.md +48 -0
  115. package/skills/lark-drive/references/lark-drive-permission-guide.md +12 -0
  116. package/skills/lark-drive/references/lark-drive-preview.md +11 -1
  117. package/skills/lark-drive/references/lark-drive-pull.md +3 -3
  118. package/skills/lark-drive/references/lark-drive-push.md +33 -6
  119. package/skills/lark-drive/references/lark-drive-react-reply.md +51 -0
  120. package/skills/lark-drive/references/lark-drive-reactions.md +27 -25
  121. package/skills/lark-drive/references/lark-drive-resolve-comment.md +45 -0
  122. package/skills/lark-drive/references/lark-drive-restore-comment.md +46 -0
  123. package/skills/lark-drive/references/lark-drive-search.md +7 -1
  124. package/skills/lark-drive/references/lark-drive-secure-label.md +1 -1
  125. package/skills/lark-drive/references/lark-drive-status.md +12 -14
  126. package/skills/lark-drive/references/lark-drive-task-result.md +58 -5
  127. package/skills/lark-drive/references/lark-drive-update-reply.md +46 -0
  128. package/skills/lark-drive/references/lark-drive-upload.md +1 -0
  129. package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize.md +26 -20
  130. package/skills/lark-drive/references/lark-drive-workflow-permission-governance-commands.md +38 -8
  131. package/skills/lark-drive/references/lark-drive-workflow-permission-governance-outputs.md +10 -10
  132. package/skills/lark-drive/references/lark-drive-workflow-permission-governance.md +22 -20
  133. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-execute.md +273 -0
  134. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-recall.md +202 -0
  135. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-resolve-verify.md +231 -0
  136. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-review-plan.md +248 -0
  137. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-setup.md +174 -0
  138. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector.md +202 -0
  139. package/skills/lark-drive/references/lark-drive-workflow.md +5 -3
  140. package/skills/lark-event/SKILL.md +3 -1
  141. package/skills/lark-event/references/lark-event-application.md +38 -0
  142. package/skills/lark-event/references/lark-event-approval.md +170 -0
  143. package/skills/lark-im/SKILL.md +6 -5
  144. package/skills/lark-im/references/card/card-2.0-schema.md +1 -1
  145. package/skills/lark-im/references/card/lark-im-card-style.md +4 -4
  146. package/skills/lark-im/references/card/resource/icons.md +14 -0
  147. package/skills/lark-im/references/lark-im-flag-list.md +8 -7
  148. package/skills/lark-im/references/lark-im-messages-reply.md +1 -1
  149. package/skills/lark-im/references/lark-im-messages-send.md +1 -1
  150. package/skills/lark-mail/SKILL.md +12 -9
  151. package/skills/lark-mail/references/lark-mail-forward.md +1 -1
  152. package/skills/lark-mail/references/lark-mail-message-modify.md +48 -0
  153. package/skills/lark-mail/references/lark-mail-message-trash.md +41 -0
  154. package/skills/lark-mail/references/lark-mail-reply-all.md +1 -1
  155. package/skills/lark-mail/references/lark-mail-reply.md +1 -1
  156. package/skills/lark-mail/references/lark-mail-watch.md +1 -1
  157. package/skills/lark-markdown/SKILL.md +3 -2
  158. package/skills/lark-markdown/references/lark-markdown-create.md +22 -2
  159. package/skills/lark-minutes/SKILL.md +19 -4
  160. package/skills/lark-minutes/references/lark-minutes-download.md +0 -2
  161. package/skills/lark-minutes/references/lark-minutes-search.md +0 -2
  162. package/skills/lark-minutes/references/lark-minutes-speaker-replace.md +0 -2
  163. package/skills/lark-minutes/references/lark-minutes-summary.md +0 -2
  164. package/skills/lark-minutes/references/lark-minutes-todo.md +2 -4
  165. package/skills/lark-minutes/references/lark-minutes-update.md +0 -2
  166. package/skills/lark-minutes/references/lark-minutes-upload.md +10 -10
  167. package/skills/lark-okr/SKILL.md +71 -26
  168. package/skills/lark-okr/references/lark-okr-batch-create.md +19 -18
  169. package/skills/lark-okr/references/lark-okr-create.md +173 -0
  170. package/skills/lark-okr/references/lark-okr-cycle-list.md +17 -7
  171. package/skills/lark-okr/references/lark-okr-entities.md +1 -0
  172. package/skills/lark-okr/references/lark-okr-indicator-update.md +3 -1
  173. package/skills/lark-okr/references/lark-okr-indicators.md +61 -12
  174. package/skills/lark-okr/references/lark-okr-progress-list.md +21 -9
  175. package/skills/lark-shared/SKILL.md +26 -8
  176. package/skills/lark-sheets/SKILL.md +98 -29
  177. package/skills/lark-sheets/references/lark-sheets-batch-update.md +18 -9
  178. package/skills/lark-sheets/references/lark-sheets-changeset.md +105 -0
  179. package/skills/lark-sheets/references/lark-sheets-chart.md +4 -2
  180. package/skills/lark-sheets/references/lark-sheets-conditional-format.md +2 -0
  181. package/skills/lark-sheets/references/lark-sheets-filter-view.md +1 -1
  182. package/skills/lark-sheets/references/lark-sheets-float-image.md +6 -6
  183. package/skills/lark-sheets/references/lark-sheets-formula-translation.md +12 -3
  184. package/skills/lark-sheets/references/lark-sheets-formula-verify.md +77 -0
  185. package/skills/lark-sheets/references/lark-sheets-history.md +93 -0
  186. package/skills/lark-sheets/references/lark-sheets-pivot-table.md +7 -2
  187. package/skills/lark-sheets/references/lark-sheets-range-operations.md +44 -14
  188. package/skills/lark-sheets/references/lark-sheets-read-data.md +3 -3
  189. package/skills/lark-sheets/references/lark-sheets-sheet-structure.md +4 -4
  190. package/skills/lark-sheets/references/lark-sheets-visual-standards.md +4 -4
  191. package/skills/lark-sheets/references/lark-sheets-workbook.md +29 -4
  192. package/skills/lark-sheets/references/lark-sheets-write-cells.md +21 -11
  193. package/skills/lark-slides/SKILL.md +121 -63
  194. package/skills/lark-slides/references/asset-planning.md +18 -5
  195. package/skills/lark-slides/references/iconpark.md +3 -3
  196. package/skills/lark-slides/references/lark-slides-create.md +30 -3
  197. package/skills/lark-slides/references/lark-slides-history.md +132 -0
  198. package/skills/lark-slides/references/lark-slides-media-upload.md +1 -3
  199. package/skills/lark-slides/references/lark-slides-pptx-template-workflows.md +85 -0
  200. package/skills/lark-slides/references/lark-slides-replace-pages.md +1 -1
  201. package/skills/lark-slides/references/lark-slides-replace-slide.md +1 -4
  202. package/skills/lark-slides/references/lark-slides-screenshot.md +11 -8
  203. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-create.md +5 -6
  204. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-delete.md +5 -2
  205. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-get.md +5 -5
  206. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-replace.md +14 -13
  207. package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +67 -31
  208. package/skills/lark-slides/references/planning-layer.md +41 -10
  209. package/skills/lark-slides/references/slides_chart_demo.xml +1416 -0
  210. package/skills/lark-slides/references/slides_xml_schema_definition.xml +499 -78
  211. package/skills/lark-slides/references/troubleshooting.md +5 -5
  212. package/skills/lark-slides/references/validation-checklist.md +65 -19
  213. package/skills/lark-slides/references/visual-planning.md +26 -22
  214. package/skills/lark-slides/references/xml-schema-quick-ref.md +285 -45
  215. package/skills/lark-slides/scripts/sxsd_validator.py +908 -0
  216. package/skills/lark-slides/scripts/xml_text_overlap_lint.py +2429 -91
  217. package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +3567 -70
  218. package/skills/lark-task/SKILL.md +8 -0
  219. package/skills/lark-task/references/lark-task-complete.md +6 -2
  220. package/skills/lark-task/references/lark-task-create.md +23 -1
  221. package/skills/lark-task/references/lark-task-update.md +6 -2
  222. package/skills/lark-vc/SKILL.md +6 -3
  223. package/skills/lark-vc/references/lark-vc-recording.md +0 -2
  224. package/skills/lark-vc/references/vc-domain-boundaries.md +9 -1
  225. package/skills/lark-vc-agent/SKILL.md +25 -15
  226. package/skills/lark-vc-agent/references/lark-vc-agent-meeting-events.md +65 -37
  227. package/skills/lark-vc-agent/references/lark-vc-agent-meeting-leave.md +1 -1
  228. package/skills/lark-vc-agent/references/lark-vc-agent-meeting-list-active.md +8 -8
  229. package/skills/lark-whiteboard/SKILL.md +13 -12
  230. package/skills/lark-whiteboard/elements/layout.md +1 -1
  231. package/skills/lark-whiteboard/elements/schema.md +2 -2
  232. package/skills/lark-whiteboard/references/{lark-whiteboard-query.md → lark-whiteboard-export.md} +15 -15
  233. package/skills/lark-whiteboard/references/lark-whiteboard-update.md +3 -3
  234. package/skills/lark-whiteboard/references/lark-whiteboard-workflow.md +12 -19
  235. package/skills/lark-whiteboard/routes/dsl.md +3 -3
  236. package/skills/lark-whiteboard/routes/mermaid.md +2 -2
  237. package/skills/lark-whiteboard/routes/svg-edit.md +4 -4
  238. package/skills/lark-whiteboard/routes/svg.md +11 -6
  239. package/skills/lark-whiteboard/scenes/bar-chart.md +1 -1
  240. package/skills/lark-whiteboard/scenes/fishbone.md +1 -1
  241. package/skills/lark-whiteboard/scenes/flywheel.md +1 -1
  242. package/skills/lark-whiteboard/scenes/line-chart.md +1 -1
  243. package/skills/lark-whiteboard/scenes/treemap.md +1 -1
  244. package/skills/lark-wiki/SKILL.md +8 -3
  245. package/skills/lark-wiki/references/lark-wiki-move-to-drive.md +122 -0
  246. package/skills/lark-wiki/references/lark-wiki-move.md +5 -3
  247. package/skills/lark-wiki/references/lark-wiki-node-get.md +1 -1
  248. package/skills/lark-wiki/references/lark-wiki-node-list.md +9 -2
  249. package/skills/lark-calendar/references/lark-calendar-agenda.md +0 -78
  250. package/skills/lark-calendar/references/lark-calendar-freebusy.md +0 -124
  251. package/skills/lark-calendar/references/lark-calendar-search-event.md +0 -29
  252. package/skills/lark-drive/references/lark-drive-comments-guide.md +0 -72
  253. package/skills/lark-sheets/references/lark-sheets-core-operations.md +0 -103
  254. package/skills/lark-slides/references/examples.md +0 -261
  255. package/skills/lark-slides/references/lark-slides-whiteboard.md +0 -330
  256. package/skills/lark-slides/references/slide-templates.md +0 -201
  257. package/skills/lark-slides/references/slides_demo.xml +0 -226
  258. package/skills/lark-slides/references/xml-format-guide.md +0 -369
@@ -0,0 +1,908 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026 Lark Technologies Pte. Ltd.
3
+ # SPDX-License-Identifier: MIT
4
+ """Internal XSD model and constraint validation for the Slides lint entrypoint."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ import re
10
+ import xml.etree.ElementTree as ET
11
+ from dataclasses import dataclass
12
+ from decimal import Decimal, InvalidOperation
13
+ from functools import lru_cache
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+
18
+ XS_NS = "{http://www.w3.org/2001/XMLSchema}"
19
+ SML_NAMESPACE = "http://www.larkoffice.com/sml/2.0"
20
+ SML_READBACK_NAMESPACE = "/sml/2.0"
21
+ SML_HTTPS_READBACK_NAMESPACE = "https://www.larkoffice.com/sml/2.0"
22
+ ACCEPTED_SML_NAMESPACES = frozenset(
23
+ (SML_NAMESPACE, SML_READBACK_NAMESPACE, SML_HTTPS_READBACK_NAMESPACE)
24
+ )
25
+
26
+
27
+ def local_name(value: str) -> str:
28
+ if value.startswith("{"):
29
+ return value.rsplit("}", 1)[-1]
30
+ return value.rsplit(":", 1)[-1]
31
+
32
+
33
+ def direct_children(element: ET.Element, name: str) -> list[ET.Element]:
34
+ return [child for child in element if child.tag == f"{XS_NS}{name}"]
35
+
36
+
37
+ def first_direct_child(element: ET.Element, *names: str) -> ET.Element | None:
38
+ wanted = {f"{XS_NS}{name}" for name in names}
39
+ return next((child for child in element if child.tag in wanted), None)
40
+
41
+
42
+ def occurs_value(raw: str | None, default: int) -> int | None:
43
+ if raw == "unbounded":
44
+ return None
45
+ return int(raw) if raw is not None else default
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class SimpleTypeRule:
50
+ name: str
51
+ base: str | None = None
52
+ enums: tuple[str, ...] = ()
53
+ patterns: tuple[str, ...] = ()
54
+ bounds: tuple[tuple[str, Decimal], ...] = ()
55
+ length_bounds: tuple[tuple[str, int], ...] = ()
56
+ union_members: tuple[str, ...] = ()
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class AttributeRule:
61
+ name: str
62
+ type_name: str
63
+ required: bool
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class ElementRule:
68
+ name: str
69
+ type_name: str | None
70
+ inline_complex_type: ET.Element | None
71
+ ref_name: str | None
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class ChildRule:
76
+ element: ElementRule
77
+ min_occurs: int
78
+ max_occurs: int | None
79
+ order: int | None
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class ChoiceRequirement:
84
+ names: tuple[str, ...]
85
+ min_occurs: int
86
+ max_occurs: int | None
87
+
88
+
89
+ @dataclass(frozen=True)
90
+ class SchemaModel:
91
+ simple_types: dict[str, SimpleTypeRule]
92
+ complex_types: dict[str, ET.Element]
93
+ element_candidates: dict[str, tuple[ElementRule, ...]]
94
+ global_elements: dict[str, ElementRule]
95
+
96
+
97
+ def parse_simple_type(
98
+ element: ET.Element,
99
+ fallback_name: str,
100
+ simple_types: dict[str, SimpleTypeRule] | None = None,
101
+ ) -> SimpleTypeRule:
102
+ name = element.attrib.get("name", fallback_name)
103
+ restriction = first_direct_child(element, "restriction")
104
+ union = first_direct_child(element, "union")
105
+ if union is not None:
106
+ union_members = [
107
+ local_name(member) for member in union.attrib.get("memberTypes", "").split()
108
+ ]
109
+ for index, inline_simple in enumerate(direct_children(union, "simpleType"), start=1):
110
+ inline_name = f"__inline_union_member_{name}_{index}"
111
+ union_members.append(inline_name)
112
+ if simple_types is not None:
113
+ simple_types[inline_name] = parse_simple_type(
114
+ inline_simple,
115
+ inline_name,
116
+ simple_types,
117
+ )
118
+ return SimpleTypeRule(
119
+ name=name,
120
+ union_members=tuple(union_members),
121
+ )
122
+ if restriction is None:
123
+ return SimpleTypeRule(name=name)
124
+
125
+ facet_names = {
126
+ "minInclusive",
127
+ "minExclusive",
128
+ "maxInclusive",
129
+ "maxExclusive",
130
+ }
131
+ bounds: list[tuple[str, Decimal]] = []
132
+ length_bounds: list[tuple[str, int]] = []
133
+ for child in restriction:
134
+ facet = local_name(child.tag)
135
+ if "value" not in child.attrib:
136
+ continue
137
+ if facet in facet_names:
138
+ bounds.append((facet, Decimal(child.attrib["value"])))
139
+ elif facet in {"minLength", "maxLength"}:
140
+ length_bounds.append((facet, int(child.attrib["value"])))
141
+ return SimpleTypeRule(
142
+ name=name,
143
+ base=local_name(restriction.attrib.get("base", "string")),
144
+ enums=tuple(child.attrib["value"] for child in direct_children(restriction, "enumeration")),
145
+ patterns=tuple(child.attrib["value"] for child in direct_children(restriction, "pattern")),
146
+ bounds=tuple(bounds),
147
+ length_bounds=tuple(length_bounds),
148
+ )
149
+
150
+
151
+ def parse_element_rule(element: ET.Element) -> ElementRule | None:
152
+ raw_ref = element.attrib.get("ref")
153
+ name = element.attrib.get("name")
154
+ if name is None and raw_ref:
155
+ name = local_name(raw_ref)
156
+ if not name:
157
+ return None
158
+ return ElementRule(
159
+ name=name,
160
+ type_name=local_name(element.attrib["type"]) if element.attrib.get("type") else None,
161
+ inline_complex_type=first_direct_child(element, "complexType"),
162
+ ref_name=local_name(raw_ref) if raw_ref else None,
163
+ )
164
+
165
+
166
+ @lru_cache(maxsize=4)
167
+ def load_schema_model(schema_path: str) -> SchemaModel:
168
+ root = ET.parse(schema_path).getroot()
169
+ simple_types: dict[str, SimpleTypeRule] = {}
170
+ for element in direct_children(root, "simpleType"):
171
+ name = element.attrib.get("name")
172
+ if not name:
173
+ continue
174
+ simple_types[name] = parse_simple_type(element, name, simple_types)
175
+ for attribute in root.iter(f"{XS_NS}attribute"):
176
+ inline_simple = first_direct_child(attribute, "simpleType")
177
+ if inline_simple is None:
178
+ continue
179
+ inline_name = f"__inline_attribute_{attribute.attrib.get('name', 'anonymous')}_{id(attribute)}"
180
+ simple_types[inline_name] = parse_simple_type(inline_simple, inline_name, simple_types)
181
+ complex_types = {
182
+ element.attrib["name"]: element
183
+ for element in direct_children(root, "complexType")
184
+ if element.attrib.get("name")
185
+ }
186
+ candidates: dict[str, list[ElementRule]] = {}
187
+ for element in root.iter(f"{XS_NS}element"):
188
+ rule = parse_element_rule(element)
189
+ if rule is not None:
190
+ candidates.setdefault(rule.name, []).append(rule)
191
+ global_elements = {
192
+ rule.name: rule
193
+ for element in direct_children(root, "element")
194
+ if (rule := parse_element_rule(element)) is not None
195
+ }
196
+ return SchemaModel(
197
+ simple_types=simple_types,
198
+ complex_types=complex_types,
199
+ element_candidates={name: tuple(rules) for name, rules in candidates.items()},
200
+ global_elements=global_elements,
201
+ )
202
+
203
+
204
+ def attributes_for_complex_type(
205
+ complex_type: ET.Element,
206
+ model: SchemaModel,
207
+ resolving: set[str] | None = None,
208
+ ) -> dict[str, AttributeRule]:
209
+ resolving = resolving or set()
210
+ attributes: dict[str, AttributeRule] = {}
211
+
212
+ for content_name in ("simpleContent", "complexContent"):
213
+ content = first_direct_child(complex_type, content_name)
214
+ if content is None:
215
+ continue
216
+ extension = first_direct_child(content, "extension")
217
+ if extension is None:
218
+ continue
219
+ base_name = local_name(extension.attrib.get("base", ""))
220
+ if base_name in model.complex_types and base_name not in resolving:
221
+ resolving.add(base_name)
222
+ attributes.update(attributes_for_complex_type(model.complex_types[base_name], model, resolving))
223
+ resolving.remove(base_name)
224
+ attributes.update(direct_attribute_rules(extension))
225
+
226
+ attributes.update(direct_attribute_rules(complex_type))
227
+ return attributes
228
+
229
+
230
+ def direct_attribute_rules(element: ET.Element) -> dict[str, AttributeRule]:
231
+ rules: dict[str, AttributeRule] = {}
232
+ for attribute in direct_children(element, "attribute"):
233
+ name = attribute.attrib.get("name")
234
+ if not name:
235
+ continue
236
+ type_name = local_name(attribute.attrib.get("type", "string"))
237
+ inline_simple = first_direct_child(attribute, "simpleType")
238
+ if inline_simple is not None:
239
+ type_name = f"__inline_attribute_{name}_{id(attribute)}"
240
+ rules[name] = AttributeRule(
241
+ name=name,
242
+ type_name=type_name,
243
+ required=attribute.attrib.get("use") == "required",
244
+ )
245
+ return rules
246
+
247
+
248
+ def attributes_for_element(rule: ElementRule, model: SchemaModel) -> dict[str, AttributeRule]:
249
+ complex_type = rule.inline_complex_type
250
+ if complex_type is None and rule.type_name in model.complex_types:
251
+ complex_type = model.complex_types[rule.type_name]
252
+ if complex_type is None:
253
+ return {}
254
+ return attributes_for_complex_type(complex_type, model)
255
+
256
+
257
+ def best_element_rule(element_name: str, model: SchemaModel) -> ElementRule | None:
258
+ candidates = model.element_candidates.get(element_name, ())
259
+ if not candidates:
260
+ return None
261
+ return max(
262
+ candidates,
263
+ key=lambda candidate: (
264
+ candidate.type_name is not None or candidate.inline_complex_type is not None,
265
+ len(attributes_for_element(candidate, model)),
266
+ ),
267
+ )
268
+
269
+
270
+ def concrete_element_rule(rule: ElementRule, model: SchemaModel) -> ElementRule:
271
+ if rule.ref_name is not None:
272
+ return model.global_elements.get(rule.ref_name, rule)
273
+ if rule.type_name is not None or rule.inline_complex_type is not None:
274
+ return rule
275
+ candidate = best_element_rule(rule.name, model)
276
+ return candidate or rule
277
+
278
+
279
+ def complex_type_for_element(rule: ElementRule, model: SchemaModel) -> ET.Element | None:
280
+ rule = concrete_element_rule(rule, model)
281
+ if rule.inline_complex_type is not None:
282
+ return rule.inline_complex_type
283
+ if rule.type_name is not None:
284
+ return model.complex_types.get(rule.type_name)
285
+ return None
286
+
287
+
288
+ def particle_for_complex_type(complex_type: ET.Element) -> ET.Element | None:
289
+ particle = first_direct_child(complex_type, "sequence", "all", "choice")
290
+ if particle is not None:
291
+ return particle
292
+ for content_name in ("simpleContent", "complexContent"):
293
+ content = first_direct_child(complex_type, content_name)
294
+ if content is None:
295
+ continue
296
+ extension = first_direct_child(content, "extension")
297
+ if extension is not None:
298
+ return first_direct_child(extension, "sequence", "all", "choice")
299
+ return None
300
+
301
+
302
+ def multiplied_max(left: int | None, right: int | None) -> int | None:
303
+ if left is None or right is None:
304
+ return None
305
+ return left * right
306
+
307
+
308
+ def child_rules_for_complex_type(
309
+ complex_type: ET.Element,
310
+ ) -> tuple[list[ChildRule], list[ChoiceRequirement]]:
311
+ particle = particle_for_complex_type(complex_type)
312
+ if particle is None:
313
+ return [], []
314
+
315
+ rules: list[ChildRule] = []
316
+ requirements: list[ChoiceRequirement] = []
317
+ next_order = 0
318
+
319
+ def add_element(
320
+ element: ET.Element,
321
+ *,
322
+ order: int | None,
323
+ optional_by_choice: bool,
324
+ max_multiplier: int | None,
325
+ ) -> None:
326
+ element_rule = parse_element_rule(element)
327
+ if element_rule is None:
328
+ return
329
+ minimum = occurs_value(element.attrib.get("minOccurs"), 1) or 0
330
+ maximum = occurs_value(element.attrib.get("maxOccurs"), 1)
331
+ rules.append(
332
+ ChildRule(
333
+ element=element_rule,
334
+ min_occurs=0 if optional_by_choice else minimum,
335
+ max_occurs=multiplied_max(maximum, max_multiplier),
336
+ order=order,
337
+ )
338
+ )
339
+
340
+ def walk_group(
341
+ group: ET.Element,
342
+ *,
343
+ ordered: bool,
344
+ fixed_order: int | None = None,
345
+ optional_by_choice: bool = False,
346
+ max_multiplier: int | None = 1,
347
+ ) -> None:
348
+ nonlocal next_order
349
+ kind = local_name(group.tag)
350
+ group_min = occurs_value(group.attrib.get("minOccurs"), 1) or 0
351
+ group_max = occurs_value(group.attrib.get("maxOccurs"), 1)
352
+ effective_max = multiplied_max(max_multiplier, group_max)
353
+
354
+ if kind == "choice":
355
+ choice_order = fixed_order
356
+ if choice_order is None and ordered:
357
+ choice_order = next_order
358
+ next_order += 1
359
+ names: list[str] = []
360
+ for child in group:
361
+ child_kind = local_name(child.tag)
362
+ if child_kind == "element":
363
+ parsed = parse_element_rule(child)
364
+ if parsed is not None:
365
+ names.append(parsed.name)
366
+ add_element(
367
+ child,
368
+ order=choice_order,
369
+ optional_by_choice=True,
370
+ max_multiplier=effective_max,
371
+ )
372
+ elif child_kind in {"sequence", "all", "choice"}:
373
+ walk_group(
374
+ child,
375
+ ordered=ordered,
376
+ fixed_order=choice_order,
377
+ optional_by_choice=True,
378
+ max_multiplier=effective_max,
379
+ )
380
+ if names and (group_min > 0 or effective_max is not None):
381
+ requirements.append(ChoiceRequirement(tuple(names), group_min, effective_max))
382
+ return
383
+
384
+ group_ordered = kind == "sequence"
385
+ for child in group:
386
+ child_kind = local_name(child.tag)
387
+ if child_kind == "element":
388
+ child_order = fixed_order
389
+ if child_order is None and ordered and group_ordered:
390
+ child_order = next_order
391
+ next_order += 1
392
+ add_element(
393
+ child,
394
+ order=child_order,
395
+ optional_by_choice=optional_by_choice or group_min == 0,
396
+ max_multiplier=effective_max,
397
+ )
398
+ elif child_kind in {"sequence", "all", "choice"}:
399
+ walk_group(
400
+ child,
401
+ ordered=ordered and group_ordered,
402
+ fixed_order=fixed_order,
403
+ optional_by_choice=optional_by_choice or group_min == 0,
404
+ max_multiplier=effective_max,
405
+ )
406
+
407
+ walk_group(particle, ordered=local_name(particle.tag) == "sequence")
408
+ return rules, requirements
409
+
410
+
411
+ def issue(
412
+ code: str,
413
+ path: str,
414
+ tag: str,
415
+ *,
416
+ attr: str | None,
417
+ expected: str,
418
+ actual: Any,
419
+ message: str,
420
+ hint: str,
421
+ ) -> dict[str, Any]:
422
+ result: dict[str, Any] = {
423
+ "level": "error",
424
+ "code": code,
425
+ "path": path,
426
+ "tag": tag,
427
+ "expected": expected,
428
+ "actual": actual,
429
+ "message": message,
430
+ "hint": hint,
431
+ }
432
+ if attr is not None:
433
+ result["attr"] = attr
434
+ return result
435
+
436
+
437
+ def builtin_scalar_value(type_name: str, value: str) -> Decimal | str | bool:
438
+ if type_name in {"string", "anyURI"}:
439
+ return value
440
+ if type_name == "boolean":
441
+ if value not in {"true", "false", "1", "0"}:
442
+ raise ValueError("expected boolean")
443
+ return value in {"true", "1"}
444
+ if type_name in {"integer", "positiveInteger", "nonNegativeInteger"}:
445
+ if re.fullmatch(r"[+-]?\d+", value) is None:
446
+ raise ValueError("expected integer")
447
+ number = Decimal(value)
448
+ if type_name == "positiveInteger" and number <= 0:
449
+ raise ArithmeticError("expected positive integer")
450
+ if type_name == "nonNegativeInteger" and number < 0:
451
+ raise ArithmeticError("expected non-negative integer")
452
+ return number
453
+ if type_name in {"double", "decimal"}:
454
+ lexical_value = value.strip(" \t\n\r")
455
+ decimal_pattern = r"[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)"
456
+ double_pattern = decimal_pattern + r"(?:[eE][+-]?[0-9]+)?"
457
+ expected_pattern = double_pattern if type_name == "double" else decimal_pattern
458
+ if re.fullmatch(expected_pattern, lexical_value) is None:
459
+ raise ValueError(f"expected {type_name}")
460
+ try:
461
+ number = Decimal(lexical_value)
462
+ except InvalidOperation as error:
463
+ raise ValueError(f"expected {type_name}") from error
464
+ if not math.isfinite(float(number)):
465
+ raise ValueError(f"expected finite {type_name}")
466
+ return number
467
+ return value
468
+
469
+
470
+ def scalar_value_for_type(
471
+ type_name: str,
472
+ value: str,
473
+ model: SchemaModel,
474
+ resolving: set[str] | None = None,
475
+ ) -> Decimal | str | bool:
476
+ resolving = resolving or set()
477
+ if type_name in resolving:
478
+ return value
479
+ rule = model.simple_types.get(type_name)
480
+ if rule is None or rule.base is None:
481
+ return builtin_scalar_value(type_name, value)
482
+ resolving.add(type_name)
483
+ try:
484
+ return scalar_value_for_type(rule.base, value, model, resolving)
485
+ finally:
486
+ resolving.remove(type_name)
487
+
488
+
489
+ @lru_cache(maxsize=32)
490
+ def python_pattern_for_xsd(pattern: str) -> str:
491
+ translated: list[str] = []
492
+ in_character_class = False
493
+ index = 0
494
+ while index < len(pattern):
495
+ char = pattern[index]
496
+ if char == "\\" and index + 1 < len(pattern):
497
+ escaped = pattern[index + 1]
498
+ if escaped in {"s", "S"}:
499
+ body = r" \t\n\r"
500
+ if in_character_class:
501
+ if escaped == "S":
502
+ raise ValueError(
503
+ f"unsupported complemented XSD character class \\{escaped} inside []"
504
+ )
505
+ translated.append(body)
506
+ else:
507
+ prefix = "^" if escaped == "S" else ""
508
+ translated.append(f"[{prefix}{body}]")
509
+ index += 2
510
+ continue
511
+ translated.extend((char, escaped))
512
+ index += 2
513
+ continue
514
+ if char == "[":
515
+ in_character_class = True
516
+ elif char == "]":
517
+ in_character_class = False
518
+ elif char == "." and not in_character_class:
519
+ translated.append(r"[^\n\r]")
520
+ index += 1
521
+ continue
522
+ elif char in "^$" and not in_character_class:
523
+ translated.append(f"\\{char}")
524
+ index += 1
525
+ continue
526
+ translated.append(char)
527
+ index += 1
528
+ return "".join(translated)
529
+
530
+
531
+ def xsd_pattern_matches(pattern: str, value: str) -> bool:
532
+ if pattern == r"[\w.-]+[.:]\S*":
533
+ if any(character in " \t\n\r" for character in value):
534
+ return False
535
+ for index, character in enumerate(value):
536
+ if index > 0 and character in ".:":
537
+ return True
538
+ if not (character == "_" or character.isalnum() or character in ".-"):
539
+ return False
540
+ return False
541
+ return re.fullmatch(python_pattern_for_xsd(pattern), value) is not None
542
+
543
+
544
+ def value_error_for_type(
545
+ type_name: str,
546
+ value: str,
547
+ model: SchemaModel,
548
+ resolving: set[str] | None = None,
549
+ ) -> tuple[str, str] | None:
550
+ resolving = resolving or set()
551
+ if type_name in resolving:
552
+ return None
553
+ rule = model.simple_types.get(type_name)
554
+ if rule is None:
555
+ try:
556
+ builtin_scalar_value(type_name, value)
557
+ except ValueError:
558
+ return "sxsd_invalid_scalar", f"value valid for {type_name}"
559
+ except ArithmeticError:
560
+ return "sxsd_value_out_of_range", f"value in the range allowed by {type_name}"
561
+ return None
562
+
563
+ resolving.add(type_name)
564
+ try:
565
+ if rule.union_members:
566
+ member_errors = [value_error_for_type(member, value, model, resolving) for member in rule.union_members]
567
+ if any(error is None for error in member_errors):
568
+ return None
569
+ unsupported_error = next(
570
+ (error for error in member_errors if error and error[0] == "sxsd_unsupported_pattern"),
571
+ None,
572
+ )
573
+ if unsupported_error is not None:
574
+ return unsupported_error
575
+ if any(error and error[0] == "sxsd_pattern_mismatch" for error in member_errors):
576
+ return "sxsd_pattern_mismatch", f"value matching one member of {type_name}"
577
+ return member_errors[0]
578
+
579
+ if rule.enums and value not in rule.enums:
580
+ return "sxsd_invalid_enum", "one of: " + ", ".join(rule.enums)
581
+
582
+ if rule.patterns:
583
+ unsupported_patterns: list[str] = []
584
+ for pattern in rule.patterns:
585
+ try:
586
+ if xsd_pattern_matches(pattern, value):
587
+ break
588
+ except (ValueError, re.error) as error:
589
+ unsupported_patterns.append(f"{pattern!r}: {error}")
590
+ else:
591
+ if unsupported_patterns:
592
+ return (
593
+ "sxsd_unsupported_pattern",
594
+ "lint support for XSD pattern " + "; ".join(unsupported_patterns),
595
+ )
596
+ return "sxsd_pattern_mismatch", "value matching pattern " + " or ".join(rule.patterns)
597
+
598
+ base_name = rule.base or "string"
599
+ base_error = value_error_for_type(base_name, value, model, resolving)
600
+ if base_error is not None:
601
+ return base_error
602
+ for facet, bound in rule.length_bounds:
603
+ allowed = len(value) >= bound if facet == "minLength" else len(value) <= bound
604
+ if not allowed:
605
+ return "sxsd_value_out_of_range", f"{facet} {bound}"
606
+ scalar = scalar_value_for_type(base_name, value, model)
607
+ if isinstance(scalar, Decimal):
608
+ for facet, bound in rule.bounds:
609
+ allowed = {
610
+ "minInclusive": scalar >= bound,
611
+ "minExclusive": scalar > bound,
612
+ "maxInclusive": scalar <= bound,
613
+ "maxExclusive": scalar < bound,
614
+ }[facet]
615
+ if not allowed:
616
+ return "sxsd_value_out_of_range", f"{facet} {bound}"
617
+ return None
618
+ finally:
619
+ resolving.remove(type_name)
620
+
621
+
622
+ def validate_element_attributes(
623
+ element: ET.Element,
624
+ path: str,
625
+ model: SchemaModel,
626
+ element_rule: ElementRule | None = None,
627
+ ) -> list[dict[str, Any]]:
628
+ tag = local_name(element.tag)
629
+ element_rule = element_rule or best_element_rule(tag, model)
630
+ if element_rule is None:
631
+ return []
632
+ attribute_rules = attributes_for_element(element_rule, model)
633
+ issues: list[dict[str, Any]] = []
634
+ for attr_rule in attribute_rules.values():
635
+ if attr_rule.required and attr_rule.name not in element.attrib:
636
+ issues.append(
637
+ issue(
638
+ "sxsd_missing_required_attr",
639
+ path,
640
+ tag,
641
+ attr=attr_rule.name,
642
+ expected=f"required attribute of type {attr_rule.type_name}",
643
+ actual=None,
644
+ message=f'missing required SXSD attribute "{attr_rule.name}" on <{tag}> at {path}',
645
+ hint=f'Add attribute "{attr_rule.name}" with a value valid for {attr_rule.type_name}.',
646
+ )
647
+ )
648
+ for raw_name, value in element.attrib.items():
649
+ attr_name = local_name(raw_name)
650
+ attr_rule = attribute_rules.get(attr_name)
651
+ if attr_rule is None:
652
+ continue
653
+ validation_error = value_error_for_type(attr_rule.type_name, value, model)
654
+ if validation_error is None:
655
+ continue
656
+ code, expected = validation_error
657
+ if code == "sxsd_unsupported_pattern":
658
+ message = (
659
+ f'unsupported SXSD pattern for attribute "{attr_name}" on <{tag}> at {path}'
660
+ )
661
+ hint = (
662
+ f"Extend the SXSD pattern interpreter for {attr_rule.type_name}; "
663
+ "do not treat this attribute value as validated."
664
+ )
665
+ else:
666
+ message = (
667
+ f'invalid SXSD value {value!r} for attribute "{attr_name}" on <{tag}> at {path}'
668
+ )
669
+ hint = f'Set attribute "{attr_name}" to a value valid for {attr_rule.type_name}.'
670
+ issues.append(
671
+ issue(
672
+ code,
673
+ path,
674
+ tag,
675
+ attr=attr_name,
676
+ expected=expected,
677
+ actual=value,
678
+ message=message,
679
+ hint=hint,
680
+ )
681
+ )
682
+ return issues
683
+
684
+
685
+ def element_namespace(tag: str) -> str | None:
686
+ if not tag.startswith("{"):
687
+ return None
688
+ return tag[1:].split("}", 1)[0]
689
+
690
+
691
+ def validate_element_children(
692
+ element: ET.Element,
693
+ path: str,
694
+ element_rule: ElementRule,
695
+ model: SchemaModel,
696
+ ) -> tuple[list[dict[str, Any]], dict[int, ElementRule]]:
697
+ tag = local_name(element.tag)
698
+ complex_type = complex_type_for_element(element_rule, model)
699
+ child_rules, choice_requirements = (
700
+ child_rules_for_complex_type(complex_type) if complex_type is not None else ([], [])
701
+ )
702
+ rules_by_name: dict[str, list[ChildRule]] = {}
703
+ for child_rule in child_rules:
704
+ rules_by_name.setdefault(child_rule.element.name, []).append(child_rule)
705
+
706
+ issues: list[dict[str, Any]] = []
707
+ matched: dict[int, ElementRule] = {}
708
+ counts: dict[str, int] = {}
709
+ latest_order = -1
710
+ for child in element:
711
+ child_name = local_name(child.tag)
712
+ child_path = f"{path}/{child_name}"
713
+ candidates = rules_by_name.get(child_name, [])
714
+ if not candidates:
715
+ issues.append(
716
+ issue(
717
+ "sxsd_unexpected_child",
718
+ child_path,
719
+ child_name,
720
+ attr=None,
721
+ expected="one of: " + ", ".join(sorted(rules_by_name)) if rules_by_name else "no child elements",
722
+ actual=child_name,
723
+ message=f"unexpected SXSD child <{child_name}> under <{tag}> at {child_path}",
724
+ hint=f"Move or remove <{child_name}> so <{tag}> follows the SXSD child structure.",
725
+ )
726
+ )
727
+ continue
728
+
729
+ child_rule = candidates[0]
730
+ if child_rule.order is not None:
731
+ if child_rule.order < latest_order:
732
+ issues.append(
733
+ issue(
734
+ "sxsd_invalid_child_order",
735
+ child_path,
736
+ child_name,
737
+ attr=None,
738
+ expected="children in xs:sequence order",
739
+ actual=child_name,
740
+ message=f"SXSD child <{child_name}> is out of order under <{tag}> at {child_path}",
741
+ hint=f"Reorder <{child_name}> according to the SXSD sequence for <{tag}>.",
742
+ )
743
+ )
744
+ latest_order = max(latest_order, child_rule.order)
745
+
746
+ counts[child_name] = counts.get(child_name, 0) + 1
747
+ if child_rule.max_occurs is not None and counts[child_name] > child_rule.max_occurs:
748
+ issues.append(
749
+ issue(
750
+ "sxsd_too_many_children",
751
+ child_path,
752
+ child_name,
753
+ attr=None,
754
+ expected=f"at most {child_rule.max_occurs}",
755
+ actual=counts[child_name],
756
+ message=f"too many SXSD <{child_name}> children under <{tag}> at {path}",
757
+ hint=f"Keep at most {child_rule.max_occurs} <{child_name}> children under <{tag}>.",
758
+ )
759
+ )
760
+ matched[id(child)] = concrete_element_rule(child_rule.element, model)
761
+
762
+ for child_rule in child_rules:
763
+ child_name = child_rule.element.name
764
+ actual_count = counts.get(child_name, 0)
765
+ if child_rule.min_occurs <= actual_count:
766
+ continue
767
+ issues.append(
768
+ issue(
769
+ "sxsd_missing_required_child",
770
+ path,
771
+ tag,
772
+ attr=None,
773
+ expected=f"{child_name} (at least {child_rule.min_occurs})",
774
+ actual=actual_count,
775
+ message=f"missing required SXSD child <{child_name}> under <{tag}> at {path}",
776
+ hint=f"Add at least {child_rule.min_occurs} <{child_name}> child under <{tag}>.",
777
+ )
778
+ )
779
+
780
+ for requirement in choice_requirements:
781
+ actual_count = sum(counts.get(name, 0) for name in requirement.names)
782
+ expected_names = ", ".join(requirement.names)
783
+ if actual_count < requirement.min_occurs:
784
+ issues.append(
785
+ issue(
786
+ "sxsd_missing_required_child",
787
+ path,
788
+ tag,
789
+ attr=None,
790
+ expected=f"one of: {expected_names} (at least {requirement.min_occurs})",
791
+ actual=actual_count,
792
+ message=f"missing required SXSD choice child under <{tag}> at {path}",
793
+ hint=f"Add at least {requirement.min_occurs} child from: {expected_names}.",
794
+ )
795
+ )
796
+ if requirement.max_occurs is not None and actual_count > requirement.max_occurs:
797
+ issues.append(
798
+ issue(
799
+ "sxsd_too_many_children",
800
+ path,
801
+ tag,
802
+ attr=None,
803
+ expected=f"at most {requirement.max_occurs} child from: {expected_names}",
804
+ actual=actual_count,
805
+ message=f"too many SXSD choice children under <{tag}> at {path}",
806
+ hint=f"Keep at most {requirement.max_occurs} child from: {expected_names}.",
807
+ )
808
+ )
809
+ return issues, matched
810
+
811
+
812
+ def validate_sxsd(root: ET.Element, schema_path: Path) -> list[dict[str, Any]]:
813
+ model = load_schema_model(str(schema_path.resolve()))
814
+ issues: list[dict[str, Any]] = []
815
+ root_name = local_name(root.tag)
816
+ document_namespace = element_namespace(root.tag)
817
+ is_bare_slide_fragment = root_name == "slide" and document_namespace is None
818
+ has_valid_document_namespace = (
819
+ document_namespace in ACCEPTED_SML_NAMESPACES or is_bare_slide_fragment
820
+ )
821
+
822
+ def visit(element: ET.Element, parent_path: str, element_rule: ElementRule) -> None:
823
+ tag = local_name(element.tag)
824
+ path = f"{parent_path}/{tag}" if parent_path else tag
825
+ namespace = element_namespace(element.tag)
826
+ invalid_root_namespace = (
827
+ not parent_path
828
+ and namespace not in ACCEPTED_SML_NAMESPACES
829
+ and not is_bare_slide_fragment
830
+ )
831
+ invalid_descendant_namespace = (
832
+ bool(parent_path)
833
+ and has_valid_document_namespace
834
+ and namespace != document_namespace
835
+ )
836
+ if invalid_root_namespace or invalid_descendant_namespace:
837
+ expected_namespace = document_namespace if parent_path else SML_NAMESPACE
838
+ namespace_hint = (
839
+ "Keep SXSD descendants without xmlns in a bare <slide> readback fragment."
840
+ if expected_namespace is None
841
+ else f'Use xmlns="{expected_namespace}" for SXSD elements.'
842
+ )
843
+ issues.append(
844
+ issue(
845
+ "sxsd_invalid_namespace",
846
+ path,
847
+ tag,
848
+ attr=None,
849
+ expected=expected_namespace,
850
+ actual=namespace,
851
+ message=f"invalid SXSD namespace on <{tag}> at {path}",
852
+ hint=namespace_hint,
853
+ )
854
+ )
855
+ issues.extend(validate_element_attributes(element, path, model, element_rule))
856
+ child_issues, matched = validate_element_children(element, path, element_rule, model)
857
+ issues.extend(child_issues)
858
+ for child in element:
859
+ child_rule = matched.get(id(child))
860
+ if child_rule is not None:
861
+ visit(child, path, child_rule)
862
+
863
+ if root_name not in {"presentation", "slide"}:
864
+ issues.append(
865
+ issue(
866
+ "sxsd_unexpected_root",
867
+ root_name,
868
+ root_name,
869
+ attr=None,
870
+ expected="presentation or slide",
871
+ actual=root_name,
872
+ message=f"unsupported SXSD root <{root_name}>",
873
+ hint="Use a <presentation> or <slide> root.",
874
+ )
875
+ )
876
+ return issues
877
+ if root_name == "presentation":
878
+ root_rule = model.global_elements.get("presentation")
879
+ elif "SlideType" in model.complex_types:
880
+ root_rule = ElementRule("slide", "SlideType", None, None)
881
+ else:
882
+ root_rule = None
883
+ if root_rule is None:
884
+ issues.append(
885
+ issue(
886
+ "sxsd_unexpected_root",
887
+ root_name,
888
+ root_name,
889
+ attr=None,
890
+ expected="presentation or slide",
891
+ actual=root_name,
892
+ message=f"unsupported SXSD root <{root_name}>",
893
+ hint="Use a <presentation> or <slide> root.",
894
+ )
895
+ )
896
+ return issues
897
+ visit(root, "", root_rule)
898
+ return issues
899
+
900
+
901
+ def load_tag_attributes(schema_path: Path) -> dict[str, set[str]]:
902
+ model = load_schema_model(str(schema_path.resolve()))
903
+ tag_attributes: dict[str, set[str]] = {}
904
+ for tag_name, candidates in model.element_candidates.items():
905
+ attrs = tag_attributes.setdefault(tag_name, set())
906
+ for candidate in candidates:
907
+ attrs.update(attributes_for_element(concrete_element_rule(candidate, model), model))
908
+ return tag_attributes