@event4u/agent-config 16.3.0 → 16.5.0

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 (532) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +178 -78
  5. package/README.md +2 -2
  6. package/agents/templates/.ai-council.yml.example +26 -8
  7. package/dist/agent-src/commands/analyze/inbox.md +10 -7
  8. package/dist/agent-src/commands/bug/investigate.md +1 -1
  9. package/dist/agent-src/commands/commit/in-chunks.md +40 -31
  10. package/dist/agent-src/commands/commit.md +72 -33
  11. package/dist/agent-src/commands/condense.md +2 -8
  12. package/dist/agent-src/commands/council/debate.md +11 -7
  13. package/dist/agent-src/commands/council/default.md +10 -9
  14. package/dist/agent-src/commands/council.md +1 -1
  15. package/dist/agent-src/commands/design.md +1 -1
  16. package/dist/agent-src/commands/estimate-ticket.md +1 -1
  17. package/dist/agent-src/commands/feature/explore.md +3 -3
  18. package/dist/agent-src/commands/feature/plan.md +1 -1
  19. package/dist/agent-src/commands/fix/ci.md +17 -3
  20. package/dist/agent-src/commands/fix/commit-messages.md +48 -23
  21. package/dist/agent-src/commands/fix/pr-comments.md +3 -6
  22. package/dist/agent-src/commands/implement-ticket.md +3 -3
  23. package/dist/agent-src/commands/memory/add.md +4 -16
  24. package/dist/agent-src/commands/memory/load.md +5 -6
  25. package/dist/agent-src/commands/memory/promote.md +3 -16
  26. package/dist/agent-src/commands/mission/upgrade.md +3 -2
  27. package/dist/agent-src/commands/optimize/agents-dir.md +2 -2
  28. package/dist/agent-src/commands/optimize/deep.md +4 -2
  29. package/dist/agent-src/commands/optimize/rtk.md +4 -26
  30. package/dist/agent-src/commands/pr/create/description-only.md +18 -15
  31. package/dist/agent-src/commands/pr/create.md +42 -34
  32. package/dist/agent-src/commands/pr/merge.md +136 -9
  33. package/dist/agent-src/commands/prepare-for-review.md +13 -11
  34. package/dist/agent-src/commands/project/analyze.md +7 -35
  35. package/dist/agent-src/commands/refine-ticket.md +1 -1
  36. package/dist/agent-src/commands/review/changes.md +14 -19
  37. package/dist/agent-src/commands/roadmap/next.md +10 -1
  38. package/dist/agent-src/commands/roadmap/process-full.md +52 -38
  39. package/dist/agent-src/commands/roadmap/resolve-blockers.md +64 -14
  40. package/dist/agent-src/commands/worktree/create.md +7 -3
  41. package/dist/agent-src/commands/worktree/verify.md +2 -1
  42. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +5 -3
  43. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +7 -9
  44. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +8 -6
  45. package/dist/agent-src/contexts/contracts/frugality-charter.md +8 -5
  46. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +1 -1
  47. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +0 -1
  48. package/dist/agent-src/contexts/execution/evidence-discipline.md +2 -2
  49. package/dist/agent-src/contexts/execution/non-interactive-contract.md +5 -8
  50. package/dist/agent-src/contexts/execution/rdp-gate.md +4 -4
  51. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +9 -8
  52. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +67 -52
  53. package/dist/agent-src/contexts/execution/subagent-routing.md +8 -8
  54. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -4
  55. package/dist/agent-src/contexts/execution/terminal-states.md +2 -2
  56. package/dist/agent-src/contexts/execution/toolchain-resolver.md +4 -10
  57. package/dist/agent-src/contexts/subagent-configuration.md +1 -1
  58. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +39 -0
  59. package/dist/agent-src/guidelines/agent-infra/context-hygiene-mechanics.md +1 -1
  60. package/dist/agent-src/guidelines/agent-infra/cross-source-consistency-mechanics.md +8 -10
  61. package/dist/agent-src/guidelines/agent-infra/frontier-reasoning-operating-profile.md +3 -2
  62. package/dist/agent-src/guidelines/agent-infra/layered-settings.md +13 -1
  63. package/dist/agent-src/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  64. package/dist/agent-src/guidelines/agent-infra/self-improvement-pipeline.md +10 -3
  65. package/dist/agent-src/guidelines/agent-infra/size-and-scope.md +36 -0
  66. package/dist/agent-src/guidelines/design-antipatterns.md +1 -1
  67. package/dist/agent-src/guidelines/php/git.md +15 -2
  68. package/dist/agent-src/rules/active-remediation.md +0 -1
  69. package/dist/agent-src/rules/analysis-skill-routing.md +0 -1
  70. package/dist/agent-src/rules/architecture.md +0 -1
  71. package/dist/agent-src/rules/artifact-drafting-protocol.md +0 -1
  72. package/dist/agent-src/rules/artifact-engagement-recording.md +0 -1
  73. package/dist/agent-src/rules/augment-edit-discipline.md +0 -1
  74. package/dist/agent-src/rules/autonomous-execution.md +0 -1
  75. package/dist/agent-src/rules/brand-consistency.md +0 -1
  76. package/dist/agent-src/rules/brand-source-of-truth.md +0 -1
  77. package/dist/agent-src/rules/broken-access-control.md +0 -1
  78. package/dist/agent-src/rules/cli-output-handling.md +0 -1
  79. package/dist/agent-src/rules/code-comment-discipline.md +0 -1
  80. package/dist/agent-src/rules/code-provenance.md +0 -1
  81. package/dist/agent-src/rules/command-suggestion-policy.md +0 -1
  82. package/dist/agent-src/rules/commit-conventions.md +1 -2
  83. package/dist/agent-src/rules/communication-through-line.md +1 -2
  84. package/dist/agent-src/rules/content-quoting-floor.md +1 -2
  85. package/dist/agent-src/rules/context-hygiene.md +5 -5
  86. package/dist/agent-src/rules/copilot-routing.md +1 -2
  87. package/dist/agent-src/rules/council-availability.md +4 -4
  88. package/dist/agent-src/rules/cross-source-consistency.md +4 -6
  89. package/dist/agent-src/rules/decision-revisit-gate.md +2 -3
  90. package/dist/agent-src/rules/delegation-policy.md +2 -3
  91. package/dist/agent-src/rules/design-fidelity.md +0 -1
  92. package/dist/agent-src/rules/design-review-after-ui-write.md +3 -3
  93. package/dist/agent-src/rules/devcontainer-routing.md +1 -2
  94. package/dist/agent-src/rules/doc-screenshot-hygiene.md +0 -1
  95. package/dist/agent-src/rules/docker-commands.md +0 -1
  96. package/dist/agent-src/rules/domain-adoption-policy.md +0 -1
  97. package/dist/agent-src/rules/domain-safety-disclaimer.md +0 -1
  98. package/dist/agent-src/rules/domain-safety-pii.md +0 -1
  99. package/dist/agent-src/rules/domain-safety-retention.md +0 -1
  100. package/dist/agent-src/rules/downstream-changes.md +0 -1
  101. package/dist/agent-src/rules/engineering-safety-floor.md +0 -1
  102. package/dist/agent-src/rules/external-code-graph-interop.md +0 -1
  103. package/dist/agent-src/rules/external-reference-deep-dive.md +0 -1
  104. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -2
  105. package/dist/agent-src/rules/finance-safety-floor.md +0 -1
  106. package/dist/agent-src/rules/fix-what-you-see.md +6 -32
  107. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +0 -1
  108. package/dist/agent-src/rules/git-history-discipline.md +1 -2
  109. package/dist/agent-src/rules/guidelines.md +0 -1
  110. package/dist/agent-src/rules/history-discipline.md +0 -1
  111. package/dist/agent-src/rules/icon-consistency.md +0 -1
  112. package/dist/agent-src/rules/image-likeness-and-rights.md +0 -1
  113. package/dist/agent-src/rules/improve-before-implement.md +0 -1
  114. package/dist/agent-src/rules/invite-challenge.md +0 -1
  115. package/dist/agent-src/rules/laravel-routing.md +1 -2
  116. package/dist/agent-src/rules/laravel-translations.md +0 -1
  117. package/dist/agent-src/rules/legal-safety-floor.md +0 -1
  118. package/dist/agent-src/rules/lethal-trifecta-guard.md +0 -1
  119. package/dist/agent-src/rules/linked-projects-onboarding-gate.md +0 -1
  120. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +0 -1
  121. package/dist/agent-src/rules/markdown-safe-codeblocks.md +0 -1
  122. package/dist/agent-src/rules/media-governance-routing.md +1 -2
  123. package/dist/agent-src/rules/media-sync-ground-truth.md +0 -1
  124. package/dist/agent-src/rules/minimal-safe-diff.md +0 -1
  125. package/dist/agent-src/rules/missing-skill-recovery.md +0 -1
  126. package/dist/agent-src/rules/missing-tool-handling.md +0 -1
  127. package/dist/agent-src/rules/model-recommendation.md +0 -1
  128. package/dist/agent-src/rules/neighbour-precedence.md +64 -0
  129. package/dist/agent-src/rules/no-attribution-footers.md +0 -1
  130. package/dist/agent-src/rules/no-decorative-emojis-in-git-surfaces.md +0 -1
  131. package/dist/agent-src/rules/no-pr-progress-comments.md +0 -1
  132. package/dist/agent-src/rules/no-roadmap-references.md +0 -1
  133. package/dist/agent-src/rules/notes-first-reasoning.md +0 -1
  134. package/dist/agent-src/rules/onboarding-gate.md +0 -1
  135. package/dist/agent-src/rules/output-discipline.md +0 -1
  136. package/dist/agent-src/rules/package-ci-checks.md +0 -1
  137. package/dist/agent-src/rules/persona-governance.md +2 -3
  138. package/dist/agent-src/rules/php-coding.md +0 -1
  139. package/dist/agent-src/rules/prefer-enums-over-literals.md +0 -1
  140. package/dist/agent-src/rules/preservation-guard.md +0 -1
  141. package/dist/agent-src/rules/provider-lifecycle-discipline.md +2 -3
  142. package/dist/agent-src/rules/question-not-instruction.md +0 -1
  143. package/dist/agent-src/rules/reviewer-awareness.md +0 -1
  144. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +0 -1
  145. package/dist/agent-src/rules/roadmap-progress-sync.md +3 -6
  146. package/dist/agent-src/rules/role-mode-adherence.md +0 -1
  147. package/dist/agent-src/rules/rule-type-governance.md +0 -1
  148. package/dist/agent-src/rules/runtime-safety.md +0 -1
  149. package/dist/agent-src/rules/scale-discipline.md +0 -1
  150. package/dist/agent-src/rules/secret-vcs-guard.md +0 -1
  151. package/dist/agent-src/rules/security-sensitive-stop.md +0 -1
  152. package/dist/agent-src/rules/self-repair-loop.md +0 -1
  153. package/dist/agent-src/rules/senior-engineering-discipline.md +0 -1
  154. package/dist/agent-src/rules/session-canary.md +0 -1
  155. package/dist/agent-src/rules/settings-ask-protocol.md +5 -6
  156. package/dist/agent-src/rules/size-enforcement.md +0 -1
  157. package/dist/agent-src/rules/skill-improvement-trigger.md +2 -3
  158. package/dist/agent-src/rules/skill-quality.md +0 -1
  159. package/dist/agent-src/rules/slash-command-routing-policy.md +0 -1
  160. package/dist/agent-src/rules/source-confidentiality.md +0 -1
  161. package/dist/agent-src/rules/source-discovery-gate.md +0 -1
  162. package/dist/agent-src/rules/source-of-truth.md +0 -1
  163. package/dist/agent-src/rules/spreadsheet-source-quality.md +1 -2
  164. package/dist/agent-src/rules/strategy-safety-floor.md +0 -1
  165. package/dist/agent-src/rules/symfony-routing.md +1 -2
  166. package/dist/agent-src/rules/think-before-action.md +0 -1
  167. package/dist/agent-src/rules/token-budget-discipline.md +14 -48
  168. package/dist/agent-src/rules/token-efficiency.md +0 -1
  169. package/dist/agent-src/rules/token-optimizer-maintenance.md +0 -1
  170. package/dist/agent-src/rules/tool-safety.md +0 -1
  171. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  172. package/dist/agent-src/rules/untrusted-input-defense.md +0 -1
  173. package/dist/agent-src/rules/upstream-proposal.md +0 -1
  174. package/dist/agent-src/rules/user-interaction.md +0 -1
  175. package/dist/agent-src/rules/user-interrupt-priority.md +0 -1
  176. package/dist/agent-src/scripts/gate_budget.ts +47 -23
  177. package/dist/agent-src/scripts/gate_execute.ts +12 -4
  178. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +2 -0
  179. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
  180. package/dist/agent-src/skills/ai-council/SKILL.md +7 -4
  181. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +21 -11
  182. package/dist/agent-src/skills/ai-council/references/procedure.md +12 -6
  183. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  184. package/dist/agent-src/skills/bug-analyzer/SKILL.md +1 -1
  185. package/dist/agent-src/skills/canvas-design/SKILL.md +4 -2
  186. package/dist/agent-src/skills/command-routing/SKILL.md +3 -4
  187. package/dist/agent-src/skills/conventional-commits-writing/SKILL.md +127 -139
  188. package/dist/agent-src/skills/corpus-grounding/ATTRIBUTION.md +39 -0
  189. package/dist/agent-src/skills/corpus-grounding/SKILL.md +8 -2
  190. package/dist/agent-src/skills/database/data/manifest.json +2 -2
  191. package/dist/agent-src/skills/database/data/query-tuning.csv +13 -13
  192. package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +13 -10
  193. package/dist/agent-src/skills/design-system-capture/SKILL.md +0 -3
  194. package/dist/agent-src/skills/estimate-ticket/SKILL.md +1 -1
  195. package/dist/agent-src/skills/fe-design/references/design-patterns.md +49 -2
  196. package/dist/agent-src/skills/feature-planning/SKILL.md +1 -1
  197. package/dist/agent-src/skills/frontend-render-security/SKILL.md +9 -0
  198. package/dist/agent-src/skills/git-workflow/SKILL.md +44 -44
  199. package/dist/agent-src/skills/git-workflow/references/branch-update.md +312 -0
  200. package/dist/agent-src/skills/git-workflow/references/commit-subject.md +205 -0
  201. package/dist/agent-src/skills/iconography/SKILL.md +4 -2
  202. package/dist/agent-src/skills/image-provider-routing/SKILL.md +16 -5
  203. package/dist/agent-src/skills/jira-integration/SKILL.md +3 -3
  204. package/dist/agent-src/skills/logo-generation/SKILL.md +12 -8
  205. package/dist/agent-src/skills/merge-conflicts/SKILL.md +10 -4
  206. package/dist/agent-src/skills/po-discovery/SKILL.md +1 -1
  207. package/dist/agent-src/skills/refine-ticket/SKILL.md +4 -4
  208. package/dist/agent-src/skills/roadmap-writing/SKILL.md +2 -2
  209. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +4 -2
  210. package/dist/agent-src/skills/source-discovery/SKILL.md +1 -1
  211. package/dist/agent-src/skills/svg-authoring/SKILL.md +318 -0
  212. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
  213. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
  214. package/dist/agent-src/skills/using-git-worktrees/SKILL.md +1 -1
  215. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +2 -2
  216. package/dist/agent-src/skills/wireframe/SKILL.md +5 -2
  217. package/dist/agent-src/templates/AGENTS.md +1 -1
  218. package/dist/agent-src/templates/agent-settings.md +46 -129
  219. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +3 -3
  220. package/dist/agent-src/templates/command.md +2 -2
  221. package/dist/agent-src/templates/roadmaps.md +6 -1
  222. package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -6
  223. package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +11 -7
  224. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +3 -2
  225. package/dist/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +2 -1
  226. package/dist/agent-src/templates/scripts/work_engine/hooks/settings.ts +0 -6
  227. package/dist/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +9 -12
  228. package/dist/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +2 -1
  229. package/dist/agent-src/templates/skill.md +1 -1
  230. package/dist/cli/commands/uiRender.js +201 -4
  231. package/dist/cli/commands/uiRenderAsset.js +1308 -0
  232. package/dist/cli/commands/uiRenderAssetCompare.js +752 -0
  233. package/dist/cli/commands/uiRenderAssetGrid.js +265 -0
  234. package/dist/cli/commands/uiRenderAssetGrounds.js +323 -0
  235. package/dist/cli/commands/uiRenderAssetIsolation.js +85 -0
  236. package/dist/cli/commands/uiRenderAssetTypes.js +145 -0
  237. package/dist/cli/main.js +47 -1
  238. package/dist/cli/registry.js +2 -1
  239. package/dist/cli-delegate/check_update_banner.js +195 -119
  240. package/dist/cli-delegate/{chunk-S4UMZNES.js → chunk-4DBG5K7M.js} +5 -5
  241. package/dist/cli-delegate/{chunk-TRI6DQZ6.js → chunk-5GPULQBE.js} +60 -42
  242. package/dist/cli-delegate/{chunk-JQVYKIZA.js → chunk-6SRJTYJS.js} +33 -177
  243. package/dist/cli-delegate/chunk-6V5FMA3P.js +186 -0
  244. package/dist/cli-delegate/chunk-6YQC2HLF.js +415 -0
  245. package/dist/cli-delegate/{chunk-E4XIZDIB.js → chunk-7HKRE54K.js} +1 -64
  246. package/dist/cli-delegate/{chunk-7HQ52DHU.js → chunk-AL3RZ22D.js} +1 -1
  247. package/dist/cli-delegate/{chunk-DBH4AHDK.js → chunk-ED7KCMLB.js} +9 -11
  248. package/dist/cli-delegate/{chunk-NXEILF2K.js → chunk-F3QDG2GT.js} +481 -72
  249. package/dist/cli-delegate/{chunk-CQWX336L.js → chunk-G5VVIZRC.js} +3 -3
  250. package/dist/cli-delegate/{chunk-JY2I2DXV.js → chunk-H76PHZBI.js} +137 -60
  251. package/dist/cli-delegate/chunk-IAYTIIQW.js +67 -0
  252. package/dist/cli-delegate/{chunk-LIYTLH6O.js → chunk-KXXWJTDO.js} +5 -10
  253. package/dist/cli-delegate/{chunk-T7L2QK3K.js → chunk-LQOCRFWT.js} +1 -1
  254. package/dist/cli-delegate/{chunk-S4YWNRKO.js → chunk-MDPPKQCQ.js} +34 -1
  255. package/dist/cli-delegate/chunk-OAFSJBHH.js +272 -0
  256. package/dist/cli-delegate/{chunk-GKE7ZVFA.js → chunk-PWX7CKMD.js} +1 -1
  257. package/dist/cli-delegate/{chunk-NDJLNC6Q.js → chunk-T6BP34WI.js} +1 -0
  258. package/dist/cli-delegate/{chunk-RQNNW7IF.js → chunk-T7XSJSUQ.js} +433 -632
  259. package/dist/cli-delegate/{chunk-NQ7WT43P.js → chunk-TMWYUOK4.js} +75 -12
  260. package/dist/cli-delegate/{chunk-EEHDXSYD.js → chunk-WQWXGIWB.js} +12 -273
  261. package/dist/cli-delegate/{chunk-QLDHIPWR.js → chunk-XQTPXKFA.js} +1 -1
  262. package/dist/cli-delegate/cmd_analyze_session.js +223 -43
  263. package/dist/cli-delegate/cmd_brand_status.js +3 -2
  264. package/dist/cli-delegate/cmd_conformance.js +489 -238
  265. package/dist/cli-delegate/cmd_converge.js +3 -3
  266. package/dist/cli-delegate/cmd_doctor.js +15 -12
  267. package/dist/cli-delegate/cmd_doctor_neighbours.js +11 -4
  268. package/dist/cli-delegate/cmd_explain.js +43 -19
  269. package/dist/cli-delegate/cmd_export.js +10 -10
  270. package/dist/cli-delegate/cmd_git_convention.js +1851 -0
  271. package/dist/cli-delegate/cmd_handoff.js +40 -21
  272. package/dist/cli-delegate/cmd_mcp_available.js +3 -2
  273. package/dist/cli-delegate/cmd_migrate.js +3 -3
  274. package/dist/cli-delegate/cmd_packs_active.js +4 -3
  275. package/dist/cli-delegate/cmd_prune.js +10 -10
  276. package/dist/cli-delegate/cmd_route_audit.js +7 -7
  277. package/dist/cli-delegate/cmd_route_explain.js +5 -5
  278. package/dist/cli-delegate/cmd_session_recycle.js +9 -7
  279. package/dist/cli-delegate/cmd_settings_check.js +54 -7
  280. package/dist/cli-delegate/cmd_settings_get.js +18 -8
  281. package/dist/cli-delegate/cmd_settings_migrate.js +10 -10
  282. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  283. package/dist/cli-delegate/cmd_sync.js +10 -10
  284. package/dist/cli-delegate/cmd_uninstall.js +10 -10
  285. package/dist/cli-delegate/cmd_update.js +7 -7
  286. package/dist/cli-delegate/cmd_upgrade.js +7 -7
  287. package/dist/cli-delegate/cmd_validate.js +10 -10
  288. package/dist/cli-delegate/cmd_versions.js +5 -5
  289. package/dist/cli-delegate/gate_budget.js +1 -1
  290. package/dist/cli-delegate/gate_execute.js +2 -2
  291. package/dist/cli-delegate/roadmap_gates.js +2 -2
  292. package/dist/discovery/deprecation-report.md +1 -1
  293. package/dist/discovery/discovery-manifest.json +171 -107
  294. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  295. package/dist/discovery/discovery-manifest.summary.md +7 -7
  296. package/dist/discovery/dormancy-report.md +1 -1
  297. package/dist/discovery/orphan-report.md +1 -1
  298. package/dist/discovery/packs.json +41 -22
  299. package/dist/discovery/trust-report.md +3 -3
  300. package/dist/discovery/workspaces.json +16 -11
  301. package/dist/hooks/dispatch.js +2520 -1924
  302. package/dist/hooks/dispatch.sha256 +1 -1
  303. package/dist/hooks/svg_rules.chunk.cjs +1626 -0
  304. package/dist/hooks/svg_rules.chunk.cjs.sha256 +1 -0
  305. package/dist/install/install.mjs +462 -431
  306. package/dist/install/installThinLayer.js +40 -12
  307. package/dist/install/installedRuleLinks.js +105 -19
  308. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  309. package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
  310. package/dist/mcp/registry-manifest.json +5 -5
  311. package/dist/mcp/server.json +2 -2
  312. package/dist/mcp/server.mjs +3063 -2160
  313. package/dist/router.json +1 -1
  314. package/dist/scripts/_lib/agent_settings.js +15 -120
  315. package/dist/scripts/_lib/compile_time_toggles.js +12 -21
  316. package/dist/scripts/_lib/git_convention.js +232 -0
  317. package/dist/scripts/_lib/global_deploy_inventory.js +730 -0
  318. package/dist/scripts/_lib/installed_layer.js +344 -18
  319. package/dist/scripts/_lib/package_root.js +68 -0
  320. package/dist/scripts/_lib/rule_law_section.js +1 -1
  321. package/dist/scripts/_lib/scan_scope.js +105 -0
  322. package/dist/scripts/_lib/settings_removed_keys.js +168 -0
  323. package/dist/scripts/_lib/svg_dom.js +284 -0
  324. package/dist/scripts/_lib/svg_geometry.js +477 -0
  325. package/dist/scripts/_lib/svg_motion.js +179 -0
  326. package/dist/scripts/_lib/svg_rules.js +1337 -0
  327. package/dist/scripts/_lib/svg_urls.js +67 -0
  328. package/dist/scripts/_lib/thin_rules.js +80 -3
  329. package/dist/scripts/lint_svg_asset.js +324 -0
  330. package/dist/server/app.js +2 -1
  331. package/dist/server/gitKeysGate.js +105 -0
  332. package/dist/server/routes/settings.js +67 -8
  333. package/dist/server/routes/wizard.js +4 -0
  334. package/dist/server/schemas/settings.js +23 -81
  335. package/dist/shared/skillRanking.js +30 -4
  336. package/dist/ui/assets/{index-Cwq4d0hX.js → index-C3djEJah.js} +2 -2
  337. package/dist/ui/index.html +1 -1
  338. package/docs/MIGRATION.md +72 -1
  339. package/docs/contracts/settings-classes.md +35 -65
  340. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +39 -0
  341. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +1 -1
  342. package/docs/guidelines/agent-infra/cross-source-consistency-mechanics.md +8 -10
  343. package/docs/guidelines/agent-infra/frontier-reasoning-operating-profile.md +3 -2
  344. package/docs/guidelines/agent-infra/layered-settings.md +13 -1
  345. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  346. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +10 -3
  347. package/docs/guidelines/agent-infra/size-and-scope.md +36 -0
  348. package/docs/guidelines/design-antipatterns.md +1 -1
  349. package/docs/guidelines/php/git.md +15 -2
  350. package/package.json +4 -3
  351. package/src/agent-src/scripts/gate_budget.ts +47 -23
  352. package/src/agent-src/scripts/gate_execute.ts +12 -4
  353. package/src/agent-src/templates/AGENTS.md +1 -1
  354. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -6
  355. package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +11 -7
  356. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.ts +3 -2
  357. package/src/agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.ts +2 -1
  358. package/src/agent-src/templates/scripts/work_engine/hooks/settings.ts +0 -6
  359. package/src/agent-src/templates/scripts/work_engine/scoring/decision_engine.ts +9 -12
  360. package/src/agent-src/templates/scripts/work_engine/scoring/memory_visibility.ts +2 -1
  361. package/src/config/agent-settings.template.yml +79 -312
  362. package/src/config/agents-paths.yml +16 -0
  363. package/src/config/assurance-capability-registry.json +7 -3
  364. package/src/config/ci-local-parity.yml +3 -0
  365. package/src/config/continuity-surface.json +3 -3
  366. package/src/config/discovery/packs.yml +17 -1
  367. package/src/config/evaluator-budgets.json +19 -6
  368. package/src/config/gate-coverage.yml +65 -23
  369. package/src/config/gate-violation-baselines.json +56 -16
  370. package/src/config/gitignore-block.txt +19 -0
  371. package/src/config/hook-bundle-budget.json +1 -1
  372. package/src/config/release-gate-locality.yml +2 -1
  373. package/src/config/routing-coverage-seed.json +55 -6
  374. package/src/config/rule-law-ceilings.json +12 -6
  375. package/src/config/rule-obligations.json +833 -0
  376. package/src/install/installThinLayer.ts +76 -10
  377. package/src/install/installedRuleLinks.ts +105 -17
  378. package/src/scripts/_cli/cmd_analyze_session.ts +9 -15
  379. package/src/scripts/_cli/cmd_conformance.ts +13 -2
  380. package/src/scripts/_cli/cmd_doctor.ts +10 -10
  381. package/src/scripts/_cli/cmd_doctor_neighbours.ts +133 -0
  382. package/src/scripts/_cli/cmd_explain.ts +72 -14
  383. package/src/scripts/_cli/cmd_git_convention.ts +637 -0
  384. package/src/scripts/_cli/cmd_settings_check.ts +66 -8
  385. package/src/scripts/_cli/cmd_settings_get.ts +18 -3
  386. package/src/scripts/_cli/doctor_execution.ts +72 -1
  387. package/src/scripts/_dispatch.bash +37 -2
  388. package/src/scripts/_lib/agent_settings.ts +15 -124
  389. package/src/scripts/_lib/blocked_by_marker.ts +1 -1
  390. package/src/scripts/_lib/compile_time_toggles.ts +12 -21
  391. package/src/scripts/_lib/dispatch_import_closure.ts +163 -0
  392. package/src/scripts/_lib/git_base_ref.ts +92 -0
  393. package/src/scripts/_lib/git_convention.ts +291 -0
  394. package/src/scripts/_lib/git_convention_carrier.ts +389 -0
  395. package/src/scripts/_lib/git_convention_grammar.ts +246 -0
  396. package/src/scripts/_lib/git_convention_measure.ts +344 -0
  397. package/src/scripts/_lib/global_deploy_inventory.ts +74 -0
  398. package/src/scripts/_lib/graph_feeder_record.ts +236 -13
  399. package/src/scripts/_lib/installed_layer.ts +448 -23
  400. package/src/scripts/_lib/knowledge_global.ts +14 -1
  401. package/src/scripts/_lib/module_reach.ts +456 -0
  402. package/src/scripts/_lib/neighbour_contradictions.ts +236 -0
  403. package/src/scripts/_lib/neighbour_tool_use.ts +8 -10
  404. package/src/scripts/_lib/ratification_artifact.ts +220 -1
  405. package/src/scripts/_lib/ratification_subject.ts +92 -0
  406. package/src/scripts/_lib/release_findings_ingest.ts +15 -7
  407. package/src/scripts/_lib/release_highlights.ts +5 -0
  408. package/src/scripts/_lib/rule_law_section.ts +1 -1
  409. package/src/scripts/_lib/rule_obligations.ts +205 -0
  410. package/src/scripts/_lib/self_repair.ts +1 -1
  411. package/src/scripts/_lib/settings_removed_keys.ts +173 -0
  412. package/src/scripts/_lib/svg_dom.ts +318 -0
  413. package/src/scripts/_lib/svg_geometry.ts +480 -0
  414. package/src/scripts/_lib/svg_motion.ts +208 -0
  415. package/src/scripts/_lib/svg_rules.ts +1453 -0
  416. package/src/scripts/_lib/svg_urls.ts +72 -0
  417. package/src/scripts/_lib/test_red_state.ts +5 -0
  418. package/src/scripts/_lib/thin_rules.ts +83 -3
  419. package/src/scripts/_lib/touched_file_quality.ts +142 -6
  420. package/src/scripts/_lib/verification_command.ts +59 -0
  421. package/src/scripts/_lib/verify_clause.ts +27 -17
  422. package/src/scripts/_lib/worker_budget.ts +1 -1
  423. package/src/scripts/adr/regenerate_index.ts +16 -1
  424. package/src/scripts/ai_council/config.ts +7 -7
  425. package/src/scripts/ai_council/modification_review.ts +100 -0
  426. package/src/scripts/ai_council/modification_review_questions.ts +90 -0
  427. package/src/scripts/ai_council/orchestrator.ts +5 -5
  428. package/src/scripts/ai_council/spend_gate.ts +18 -7
  429. package/src/scripts/ai_team/config.ts +11 -16
  430. package/src/scripts/audit_user_type_axis.ts +20 -8
  431. package/src/scripts/bench_ab_clone.ts +1 -2
  432. package/src/scripts/bench_concern_spawn_path.ts +129 -0
  433. package/src/scripts/bench_graph_feeder_latency.ts +584 -0
  434. package/src/scripts/check_agent_artifact_location.ts +36 -0
  435. package/src/scripts/check_branch_freshness.ts +147 -24
  436. package/src/scripts/check_enforcement_coverage.ts +130 -0
  437. package/src/scripts/check_evaluator_budgets.ts +4 -0
  438. package/src/scripts/check_finding_dispositions.ts +88 -25
  439. package/src/scripts/check_gate_coverage.ts +4 -3
  440. package/src/scripts/check_gate_reachability.ts +48 -1
  441. package/src/scripts/check_generator_sync.ts +48 -0
  442. package/src/scripts/check_hook_bundle_composition.ts +107 -0
  443. package/src/scripts/check_kernel_edit_ratified.ts +86 -6
  444. package/src/scripts/check_pack_size.ts +1 -1
  445. package/src/scripts/check_release_manifest_content.ts +189 -0
  446. package/src/scripts/check_standing_rule_delivery.ts +96 -0
  447. package/src/scripts/check_suppression_hygiene.ts +2 -5
  448. package/src/scripts/ci_settle.ts +42 -5
  449. package/src/scripts/classify_merge_risk.ts +320 -0
  450. package/src/scripts/closure_scan.ts +91 -1
  451. package/src/scripts/command_suggester/settings.ts +8 -108
  452. package/src/scripts/command_suggester/types.ts +4 -5
  453. package/src/scripts/consumer_matrix.ts +102 -0
  454. package/src/scripts/context_hygiene_hook.ts +13 -23
  455. package/src/scripts/cost/preflight.mjs +12 -1
  456. package/src/scripts/council_cli.ts +7 -7
  457. package/src/scripts/dispatch_r2_reviewer.ts +24 -11
  458. package/src/scripts/explain_run.ts +8 -13
  459. package/src/scripts/generate_git_convention_grammar.ts +127 -0
  460. package/src/scripts/generate_host_cost_table.ts +20 -3
  461. package/src/scripts/handoff_context_hook.ts +86 -37
  462. package/src/scripts/hook_manifest.json +1 -1
  463. package/src/scripts/hook_manifest.yaml +25 -6
  464. package/src/scripts/hooks/block_config_weakening.ts +48 -5
  465. package/src/scripts/hooks/block_plumbing_writes.ts +8 -4
  466. package/src/scripts/hooks/block_speaking_inbox_dir.ts +16 -1
  467. package/src/scripts/hooks/concern_registry.ts +2 -0
  468. package/src/scripts/hooks/delegation_nudge_hook.ts +2 -3
  469. package/src/scripts/hooks/design_pass_hook.ts +157 -3
  470. package/src/scripts/hooks/evidence_independence.ts +12 -4
  471. package/src/scripts/hooks/mcp_usage_observation_hook.ts +121 -0
  472. package/src/scripts/hooks/push_settle_hook.ts +4 -3
  473. package/src/scripts/hooks/rule_inject_hook.ts +44 -3
  474. package/src/scripts/hooks/state_io.ts +8 -4
  475. package/src/scripts/hooks/svg_obligation.ts +497 -0
  476. package/src/scripts/hooks/svg_rules_loader.ts +133 -0
  477. package/src/scripts/hooks/telemetry_usage_hook.ts +7 -46
  478. package/src/scripts/hooks/turn_end_gate_hook.ts +6 -3
  479. package/src/scripts/install-hooks.sh +42 -14
  480. package/src/scripts/install.ts +7 -31
  481. package/src/scripts/lint_blocker_citations.ts +198 -0
  482. package/src/scripts/lint_consolidation_lineage.ts +6 -3
  483. package/src/scripts/lint_design_quality.ts +60 -12
  484. package/src/scripts/lint_evidence_artifacts.ts +9 -0
  485. package/src/scripts/lint_hook_concern_budget.ts +10 -21
  486. package/src/scripts/lint_motion_authority_drift.ts +27 -0
  487. package/src/scripts/lint_pack_boundaries.ts +406 -28
  488. package/src/scripts/lint_roadmap_blockers.ts +282 -29
  489. package/src/scripts/lint_roadmap_complexity.ts +4 -2
  490. package/src/scripts/lint_rule_law_section.ts +151 -21
  491. package/src/scripts/lint_rule_skill_pack_reach.ts +17 -0
  492. package/src/scripts/lint_settings_classes.ts +1 -1
  493. package/src/scripts/lint_svg_asset.ts +365 -0
  494. package/src/scripts/lint_ticked_unmet_criteria.ts +165 -0
  495. package/src/scripts/lint_workflow_security.ts +12 -1
  496. package/src/scripts/measure_skill_ranker_baseline.ts +5 -0
  497. package/src/scripts/model_rule_injection.ts +3 -3
  498. package/src/scripts/onboarding_gate_hook.ts +7 -6
  499. package/src/scripts/print_required_checks.ts +15 -3
  500. package/src/scripts/probe_host_compliance.ts +33 -5
  501. package/src/scripts/probe_turnaround.ts +183 -3
  502. package/src/scripts/ratification_header.ts +77 -0
  503. package/src/scripts/refusal_envelope.ts +138 -0
  504. package/src/scripts/release_publication.ts +29 -3
  505. package/src/scripts/report_dispatch_import_closure.ts +76 -0
  506. package/src/scripts/report_installed_rule_links.ts +118 -1
  507. package/src/scripts/report_module_reach.ts +212 -0
  508. package/src/scripts/report_obligation_mechanism.ts +16 -18
  509. package/src/scripts/report_parked_blockers.ts +222 -0
  510. package/src/scripts/report_required_checks_drift.ts +196 -0
  511. package/src/scripts/report_skill_ranker_confusion.ts +103 -2
  512. package/src/scripts/report_standing_payload_by_host.ts +1 -1
  513. package/src/scripts/routing_doctor.ts +2 -2
  514. package/src/scripts/schemas/agent-settings.schema.json +17 -31
  515. package/src/scripts/schemas/command.schema.json +1 -1
  516. package/src/scripts/schemas/skill.schema.json +1 -1
  517. package/src/scripts/self_review_gate.ts +39 -2
  518. package/src/scripts/skill_tools/score_skill_relevance.ts +28 -19
  519. package/src/scripts/sync_pr_branch.ts +403 -132
  520. package/src/scripts/trigger_eval_grandfather.json +0 -16
  521. package/src/server/schemas/settings.ts +33 -177
  522. package/src/shared/skillRanking.ts +53 -4
  523. package/dist/cli-delegate/chunk-NYX3L2PZ.js +0 -99
  524. package/src/scripts/_lib/config_chain.ts +0 -477
  525. package/src/scripts/_lib/conformance_report.ts +0 -403
  526. package/src/scripts/_lib/eval_discrimination.ts +0 -95
  527. package/src/scripts/_lib/evaluator_promotion.ts +0 -225
  528. package/src/scripts/_lib/file_slicer.ts +0 -117
  529. package/src/scripts/_lib/legacy_boundary_map.ts +0 -228
  530. package/src/scripts/_lib/minimality_tiebreak.ts +0 -168
  531. package/src/scripts/_lib/overbuild_lens_contract.ts +0 -196
  532. package/src/scripts/_lib/role_split.ts +0 -338
@@ -0,0 +1,1337 @@
1
+ /**
2
+ * svg_rules — the SVG rule set, with no command-line surface attached.
3
+ *
4
+ * Split out of `lint_svg_asset.ts` for a measured reason, not a tidy one, and
5
+ * the routing it was split FOR IS NOW LIVE.
6
+ *
7
+ * THIS PARAGRAPH USED TO SAY THE OPPOSITE, and it was false the moment the
8
+ * routing landed — finding 10 of that change's own completion review, which is
9
+ * the cheapest kind of defect to leave behind and the most expensive to read: a
10
+ * reader opening this file to decide whether it sits on a hot path was told it
11
+ * does not.
12
+ *
13
+ * It does. `hooks/design_pass_hook.ts` routes every `.svg` write here through
14
+ * `hooks/svg_rules_loader.ts`, and `package.json`'s `build:hook-chunks` emits
15
+ * this module as `dist/hooks/svg_rules.chunk.cjs` for exactly that. The chunk
16
+ * is why it is live without being expensive: step 1.4 of
17
+ * `road-to-svg-with-a-witness` imported these rules STATICALLY into the one
18
+ * bundle every hook event loads — 30,924 bytes, 12,931 past the ceiling — and
19
+ * an independent council refused it 2/2. Loaded as a chunk the same rules cost
20
+ * 2,128 bytes in that bundle and are `require`d only for a path ending in
21
+ * `.svg`.
22
+ *
23
+ * So: this module is on the `.svg` write path and on no other. Anything added
24
+ * here is paid by an SVG write and by nothing else — a budget, not a licence.
25
+ * Its other consumers are `lint_svg_asset` and the fixture report.
26
+ *
27
+ * `lint_svg_asset.ts` is now the CLI over this module and re-exports it, so
28
+ * every existing import keeps resolving.
29
+ */
30
+ import { attrsOf, parseDocument, tagEnd } from './svg_dom.js';
31
+ import { cssUnescape, isActiveUrl, localName, rawAttrs, resolvedUrl, svgPrefixes, URL_ATTRS } from './svg_urls.js';
32
+ import { elementExtent, pathDataError, pathExtent, pointsExtent, staticBox, tokenisePath, } from './svg_geometry.js';
33
+ export { elementExtent, pathDataError, pathExtent, pointsExtent, staticBox, tokenisePath };
34
+ import { SMIL_TAGS, declarationsOf, firstSmilLine, hasCssAnimation, hasReducedMotionQuery, hasSmilMarkup, readCssRules, smilVerdict, } from './svg_motion.js';
35
+ export const DELIVERY_MODES = ['inline', 'document', 'img', 'css-bg'];
36
+ // ---------------------------------------------------------------------------
37
+ // A deliberately small scanner
38
+ // ---------------------------------------------------------------------------
39
+ /**
40
+ * Elements in document order.
41
+ *
42
+ * Not a conformant XML parser and not trying to be: it skips comments, CDATA
43
+ * and the prolog, and reads tag names plus attributes. Everything the rules
44
+ * below need is attribute-level or text-level, and pulling in a real parser
45
+ * would make a dependency-free linter depend on one.
46
+ */
47
+ export function parseElements(content) {
48
+ const out = [];
49
+ const lineOf = lineIndexer(content);
50
+ let i = 0;
51
+ while (i < content.length) {
52
+ const lt = content.indexOf('<', i);
53
+ if (lt === -1)
54
+ break;
55
+ // Comments, CDATA and the prolog carry no elements.
56
+ if (content.startsWith('<!--', lt)) {
57
+ const end = content.indexOf('-->', lt + 4);
58
+ i = end === -1 ? content.length : end + 3;
59
+ continue;
60
+ }
61
+ if (content.startsWith('<![CDATA[', lt)) {
62
+ const end = content.indexOf(']]>', lt + 9);
63
+ i = end === -1 ? content.length : end + 3;
64
+ continue;
65
+ }
66
+ if (content.startsWith('<?', lt)) {
67
+ // A PROCESSING INSTRUCTION ENDS AT `?>`, NEVER AT THE FIRST `>`.
68
+ //
69
+ // Finding 3 of the third completion review, reproduced by running
70
+ // `scanSvg`: this branch shared the DOCTYPE's first-`>` search, so
71
+ // `<?xml-stylesheet href="a.css?x=1><script/>"?>` — a legal PI
72
+ // whose quoted pseudo-attribute contains a `>` — ended early and
73
+ // the remainder was parsed as markup. `<script/>` then became a
74
+ // real element and the file was reported
75
+ // `svg-safety-active-content` at severity `error`, on a document
76
+ // that contains no script. Under finding 4's static-only path that
77
+ // false error is a BLOCKING refusal, so the two compound.
78
+ //
79
+ // `svg_dom.parseDocument` always did this correctly; the two
80
+ // scanners disagreeing about where the prolog ends is what the
81
+ // module header below now says plainly instead of claiming they
82
+ // are one scan.
83
+ const end = content.indexOf('?>', lt + 2);
84
+ i = end === -1 ? content.length : end + 2;
85
+ continue;
86
+ }
87
+ if (content.startsWith('<!', lt)) {
88
+ // A DOCTYPE may carry a bracketed internal subset; skip past it.
89
+ const bracket = content.indexOf('[', lt);
90
+ const close = content.indexOf('>', lt);
91
+ if (bracket !== -1 && close !== -1 && bracket < close) {
92
+ const endBracket = content.indexOf(']', bracket);
93
+ const after = endBracket === -1 ? close : content.indexOf('>', endBracket);
94
+ i = after === -1 ? content.length : after + 1;
95
+ }
96
+ else {
97
+ i = close === -1 ? content.length : close + 1;
98
+ }
99
+ continue;
100
+ }
101
+ const gt = tagEnd(content, lt);
102
+ if (gt === -1)
103
+ break;
104
+ const raw = content.slice(lt, gt + 1);
105
+ const closing = raw.startsWith('</');
106
+ const selfClosing = raw.endsWith('/>');
107
+ const nameMatch = /^<\/?\s*([A-Za-z_][\w.:-]*)/.exec(raw);
108
+ if (!nameMatch) {
109
+ i = gt + 1;
110
+ continue;
111
+ }
112
+ out.push({
113
+ name: (nameMatch[1] ?? '').toLowerCase(),
114
+ attrs: closing ? {} : attrsOf(raw),
115
+ line: lineOf(lt),
116
+ start: lt,
117
+ selfClosing,
118
+ closing,
119
+ raw,
120
+ });
121
+ i = gt + 1;
122
+ }
123
+ return out;
124
+ }
125
+ /** End of the tag, honouring quoted attribute values that may contain `>`. */
126
+ /**
127
+ * Blank out every XML comment, preserving offsets and line breaks.
128
+ *
129
+ * Finding 2.3 of `road-to-svg-rules-in-the-dispatch-path`, from the R2
130
+ * completion review of the parent change: `parseElements` skips comments and
131
+ * the content-level regex rules did not, so the two disagreed about what the
132
+ * file contains. A commented `url(#x)` marked a `<defs>` entry used, and a
133
+ * commented `@media (prefers-reduced-motion)` suppressed the
134
+ * no-reduced-motion rule — in both directions a finding was lost to markup
135
+ * that does not render.
136
+ *
137
+ * Every character inside `<!--` … `-->` becomes a space, the delimiters
138
+ * included, and newlines are kept. That keeps every offset and every line
139
+ * number identical to the raw text, so a rule can report a position in the
140
+ * masked string and have it mean the same thing in the file. An unterminated
141
+ * comment masks to end of input, which is what a browser does with it too.
142
+ *
143
+ * NOT applied to `parseIgnores`, deliberately: the inline
144
+ * `svg-asset-disable` markers LIVE in comments, so masking them would make every one of them silently stop
145
+ * working. The raw text is the right input there and the masked text is the
146
+ * right input everywhere else.
147
+ */
148
+ export function maskComments(content) {
149
+ if (!content.includes('<!--'))
150
+ return content;
151
+ const out = content.split('');
152
+ let i = 0;
153
+ while (i < out.length) {
154
+ const start = content.indexOf('<!--', i);
155
+ if (start === -1)
156
+ break;
157
+ const close = content.indexOf('-->', start + 4);
158
+ const end = close === -1 ? content.length : close + 3;
159
+ for (let j = start; j < end; j += 1) {
160
+ if (out[j] !== '\n' && out[j] !== '\r')
161
+ out[j] = ' ';
162
+ }
163
+ i = end;
164
+ }
165
+ return out.join('');
166
+ }
167
+ function lineIndexer(content) {
168
+ const starts = [0];
169
+ for (let i = 0; i < content.length; i++)
170
+ if (content[i] === '\n')
171
+ starts.push(i + 1);
172
+ return (offset) => {
173
+ let lo = 0;
174
+ let hi = starts.length - 1;
175
+ while (lo < hi) {
176
+ const mid = (lo + hi + 1) >> 1;
177
+ if (starts[mid] <= offset)
178
+ lo = mid;
179
+ else
180
+ hi = mid - 1;
181
+ }
182
+ return lo + 1;
183
+ };
184
+ }
185
+ /** The text between an opening and its matching closing tag, for simple leaves. */
186
+ function textOf(content, elements, index) {
187
+ const el = elements[index];
188
+ if (!el || el.selfClosing)
189
+ return '';
190
+ const openEnd = content.indexOf('>', el.start);
191
+ const closeTag = `</${el.name}`;
192
+ const closeAt = content.toLowerCase().indexOf(closeTag, openEnd);
193
+ if (openEnd === -1 || closeAt === -1)
194
+ return '';
195
+ return content.slice(openEnd + 1, closeAt);
196
+ }
197
+ export function readViewBox(el) {
198
+ const raw = el?.attrs['viewbox'];
199
+ if (!raw)
200
+ return null;
201
+ const parts = raw.trim().split(/[\s,]+/).map(Number);
202
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n)))
203
+ return null;
204
+ const [x, y, w, h] = parts;
205
+ if (w <= 0 || h <= 0)
206
+ return null;
207
+ return { x, y, w, h };
208
+ }
209
+ /** Every `#id` this file points at, from `href`/`xlink:href` and from `url(#…)`. */
210
+ export function referencedIds(content, elements) {
211
+ const ids = new Set();
212
+ for (const el of elements) {
213
+ for (const key of ['href', 'xlink:href']) {
214
+ const v = el.attrs[key];
215
+ if (v && v.startsWith('#'))
216
+ ids.add(v.slice(1));
217
+ }
218
+ }
219
+ const urlRe = /url\(\s*['"]?#([^)'"\s]+)/g;
220
+ let m;
221
+ while ((m = urlRe.exec(content)) !== null)
222
+ ids.add(m[1]);
223
+ // `begin="other.end"` and `<set attributeName=…>` name elements too.
224
+ const beginRe = /\b(?:begin|end)\s*=\s*"([A-Za-z_][\w.:-]*)\./g;
225
+ while ((m = beginRe.exec(content)) !== null)
226
+ ids.add(m[1]);
227
+ return ids;
228
+ }
229
+ // ---------------------------------------------------------------------------
230
+ // Rule family — structure
231
+ // ---------------------------------------------------------------------------
232
+ function ruleMissingXmlns(elements, file) {
233
+ const root = elements.find((el) => el.name === 'svg' && !el.closing);
234
+ if (!root)
235
+ return [];
236
+ if (root.attrs['xmlns'])
237
+ return [];
238
+ return [
239
+ {
240
+ rule: 'svg-structure-missing-xmlns',
241
+ file,
242
+ line: root.line,
243
+ snippet: 'root <svg> has no xmlns — the file does not render as a standalone document or through <img>',
244
+ severity: 'error',
245
+ },
246
+ ];
247
+ }
248
+ /** Is this element inside a `<defs>`, `<symbol>`, `<clipPath>`, `<mask>` or `<marker>`? */
249
+ function buildContainerDepth(elements) {
250
+ const stack = [];
251
+ const out = new Map();
252
+ elements.forEach((el, idx) => {
253
+ if (el.closing) {
254
+ if (stack[stack.length - 1] === el.name)
255
+ stack.pop();
256
+ out.set(idx, [...stack]);
257
+ return;
258
+ }
259
+ out.set(idx, [...stack]);
260
+ if (!el.selfClosing)
261
+ stack.push(el.name);
262
+ });
263
+ return out;
264
+ }
265
+ const NON_RENDERED_CONTAINERS = new Set(['defs', 'symbol', 'clippath', 'mask', 'marker', 'pattern']);
266
+ function ruleOutsideViewBox(elements, file) {
267
+ const root = elements.find((el) => el.name === 'svg' && !el.closing);
268
+ const vb = readViewBox(root);
269
+ if (!vb)
270
+ return [];
271
+ const ancestry = buildContainerDepth(elements);
272
+ const findings = [];
273
+ elements.forEach((el, idx) => {
274
+ if (el.closing)
275
+ return;
276
+ const chain = ancestry.get(idx) ?? [];
277
+ if (chain.some((c) => NON_RENDERED_CONTAINERS.has(c)))
278
+ return;
279
+ // A transform anywhere up the chain moves the box; staticBox already
280
+ // refuses an element with its own, and this refuses an inherited one.
281
+ if (chain.length && hasTransformedAncestor(elements, idx))
282
+ return;
283
+ const box = staticBox(el);
284
+ if (!box)
285
+ return;
286
+ const outside = box.maxX <= vb.x || box.minX >= vb.x + vb.w || box.maxY <= vb.y || box.minY >= vb.y + vb.h;
287
+ if (!outside)
288
+ return;
289
+ findings.push({
290
+ rule: 'svg-structure-outside-viewbox',
291
+ file,
292
+ line: el.line,
293
+ snippet: `<${el.name}> box (${box.minX},${box.minY})-(${box.maxX},${box.maxY}) lies entirely outside ` +
294
+ `viewBox ${vb.x} ${vb.y} ${vb.w} ${vb.h} — it will never paint`,
295
+ severity: 'error',
296
+ });
297
+ });
298
+ return findings;
299
+ }
300
+ function hasTransformedAncestor(elements, index) {
301
+ const stack = [];
302
+ for (let i = 0; i < index; i++) {
303
+ const el = elements[i];
304
+ if (el.closing) {
305
+ if (stack[stack.length - 1]?.name === el.name)
306
+ stack.pop();
307
+ continue;
308
+ }
309
+ if (!el.selfClosing)
310
+ stack.push(el);
311
+ }
312
+ return stack.some((el) => Boolean(el.attrs['transform']));
313
+ }
314
+ /**
315
+ * Path-data grammar check.
316
+ *
317
+ * Reports only data that no conformant parser could accept — a command letter
318
+ * followed by the wrong number of coordinates, or a stray token. A path whose
319
+ * numbers are odd but well-formed is a design question, not a parse error.
320
+ */
321
+ function ruleUnparseablePath(elements, file) {
322
+ const findings = [];
323
+ for (const el of elements) {
324
+ if (el.closing)
325
+ continue;
326
+ const d = el.attrs['d'];
327
+ if (d === undefined)
328
+ continue;
329
+ const err = pathDataError(d);
330
+ if (err) {
331
+ findings.push({
332
+ rule: 'svg-structure-unparseable-path',
333
+ file,
334
+ line: el.line,
335
+ snippet: `<${el.name} d="…"> ${err}`,
336
+ severity: 'error',
337
+ });
338
+ }
339
+ }
340
+ return findings;
341
+ }
342
+ function ruleUnusedDefs(content, elements, file) {
343
+ const used = referencedIds(content, elements);
344
+ const ancestry = buildContainerDepth(elements);
345
+ const findings = [];
346
+ elements.forEach((el, idx) => {
347
+ if (el.closing)
348
+ return;
349
+ const id = el.attrs['id'];
350
+ if (!id)
351
+ return;
352
+ const chain = ancestry.get(idx) ?? [];
353
+ if (!chain.includes('defs'))
354
+ return;
355
+ // A `<symbol>` is a sprite-sheet entry addressed from a HOST page, so
356
+ // the file cannot see its own reference. Exempt by construction —
357
+ // flagging it would red every sprite sheet in existence.
358
+ if (el.name === 'symbol')
359
+ return;
360
+ if (used.has(id))
361
+ return;
362
+ findings.push({
363
+ rule: 'svg-structure-unused-defs',
364
+ file,
365
+ line: el.line,
366
+ snippet: `<${el.name} id="${id}"> sits in <defs> and nothing in this file references it`,
367
+ severity: 'warning',
368
+ });
369
+ });
370
+ return findings;
371
+ }
372
+ function ruleDanglingReference(content, elements, file) {
373
+ const defined = new Set();
374
+ for (const el of elements)
375
+ if (!el.closing && el.attrs['id'])
376
+ defined.add(el.attrs['id']);
377
+ const findings = [];
378
+ const lineOf = lineIndexer(content);
379
+ for (const el of elements) {
380
+ if (el.closing)
381
+ continue;
382
+ for (const key of ['href', 'xlink:href']) {
383
+ const v = el.attrs[key];
384
+ if (!v || !v.startsWith('#'))
385
+ continue;
386
+ const id = v.slice(1);
387
+ if (defined.has(id))
388
+ continue;
389
+ findings.push({
390
+ rule: 'svg-structure-dangling-reference',
391
+ file,
392
+ line: el.line,
393
+ snippet: `<${el.name} ${key}="${v}"> points at an id this file does not define`,
394
+ severity: 'error',
395
+ });
396
+ }
397
+ }
398
+ const urlRe = /url\(\s*['"]?#([^)'"\s]+)/g;
399
+ let m;
400
+ while ((m = urlRe.exec(content)) !== null) {
401
+ const id = m[1];
402
+ if (defined.has(id))
403
+ continue;
404
+ findings.push({
405
+ rule: 'svg-structure-dangling-reference',
406
+ file,
407
+ line: lineOf(m.index),
408
+ snippet: `url(#${id}) points at an id this file does not define`,
409
+ severity: 'error',
410
+ });
411
+ }
412
+ return dedupe(findings);
413
+ }
414
+ // ---------------------------------------------------------------------------
415
+ // Rule family — safety, graded per delivery mode (step 1.5)
416
+ // ---------------------------------------------------------------------------
417
+ /**
418
+ * What a delivery mode can execute.
419
+ *
420
+ * `inline` and `document` run script in an origin that matters, so active
421
+ * content is an ERROR there. `img` and `css-bg` run nothing and load nothing,
422
+ * so the same file is `inert` — reported, never called clean, because the same
423
+ * bytes can be inlined tomorrow with no edit to the file (decision E12).
424
+ */
425
+ export function safetySeverity(mode) {
426
+ return mode === 'img' || mode === 'css-bg' ? 'inert' : 'error';
427
+ }
428
+ // An UNQUOTED value counts. Finding 1 of the fifth completion review: this
429
+ // fallback exists because the attribute parser only sees quoted values, and it
430
+ // required a quote itself — so `onload=alert(1)`, which every browser runs,
431
+ // produced zero findings while `onload="alert(1)"` produced an error. The
432
+ // safety family missing the unquoted spelling is the one miss that matters,
433
+ // because an attacker writes whichever spelling is not checked.
434
+ const ACTIVE_ATTR_RE = /\son[a-z]+\s*=\s*(["']|[^\s>"'])/i;
435
+ /** Elements that run or embed foreign content when the graphic is inlined. */
436
+ const ACTIVE_ELEMENTS = new Set(['script', 'foreignobject', 'iframe', 'embed', 'object']);
437
+ function ruleActiveContent(content, elements, file, mode) {
438
+ const severity = safetySeverity(mode);
439
+ const findings = [];
440
+ const lineOf = lineIndexer(content);
441
+ const prefixes = svgPrefixes(content);
442
+ for (const el of elements) {
443
+ if (el.closing)
444
+ continue;
445
+ const local = localName(el.name, prefixes);
446
+ // SMIL that writes an active URL into an href is the same href, later.
447
+ const smil = local === 'set' || local === 'animate' ? rawAttrs(el.raw) : null;
448
+ const animatesHref = smil !== null && /^(xlink:)?href$/i.test(resolvedUrl(smil['attributename'] ?? ''));
449
+ if (animatesHref &&
450
+ ['to', 'from', 'by', 'values'].some((k) => (smil[k] ?? '').split(';').some(isActiveUrl))) {
451
+ findings.push({
452
+ rule: 'svg-safety-active-content',
453
+ file,
454
+ line: el.line,
455
+ snippet: `<${el.name} attributeName="href"> animates in an active URL; delivery mode "${mode}" grades it ${severity}`,
456
+ severity,
457
+ });
458
+ }
459
+ if (ACTIVE_ELEMENTS.has(local)) {
460
+ findings.push({
461
+ rule: 'svg-safety-active-content',
462
+ file,
463
+ line: el.line,
464
+ snippet: `<${el.name}> is active content; delivery mode "${mode}" grades it ${severity}`,
465
+ severity,
466
+ });
467
+ }
468
+ for (const [key, value] of Object.entries(el.attrs)) {
469
+ if (/^on[a-z]+$/.test(key)) {
470
+ findings.push({
471
+ rule: 'svg-safety-active-content',
472
+ file,
473
+ line: el.line,
474
+ snippet: `<${el.name} ${key}="…"> is an event handler; delivery mode "${mode}" grades it ${severity}`,
475
+ severity,
476
+ });
477
+ }
478
+ if (/^javascript:/i.test(resolvedUrl(value)) || (URL_ATTRS.has(key) && isActiveUrl(value))) {
479
+ findings.push({
480
+ rule: 'svg-safety-active-content',
481
+ file,
482
+ line: el.line,
483
+ snippet: `<${el.name} ${key}="${resolvedUrl(value).slice(0, 16)}…"> is an active URL; delivery mode "${mode}" grades it ${severity}`,
484
+ severity,
485
+ });
486
+ }
487
+ }
488
+ }
489
+ // A handler written on an element the scanner did not turn into attributes
490
+ // (an unquoted value, say) still has to be seen.
491
+ if (!findings.length && ACTIVE_ATTR_RE.test(content)) {
492
+ const idx = content.search(ACTIVE_ATTR_RE);
493
+ findings.push({
494
+ rule: 'svg-safety-active-content',
495
+ file,
496
+ line: lineOf(idx),
497
+ snippet: `an on* event handler attribute is present; delivery mode "${mode}" grades it ${severity}`,
498
+ severity,
499
+ });
500
+ }
501
+ return dedupe(findings);
502
+ }
503
+ /**
504
+ * An internal DTD subset or an entity declaration.
505
+ *
506
+ * A BARE public DOCTYPE is clean and deliberately so — exported assets carry
507
+ * one routinely and it is harmless. What is not harmless is the bracketed
508
+ * subset, which is where billion-laughs and external-entity payloads live.
509
+ */
510
+ function ruleInternalSubset(content, file) {
511
+ const lineOf = lineIndexer(content);
512
+ const findings = [];
513
+ const entity = /<!ENTITY\b/i.exec(content);
514
+ if (entity) {
515
+ findings.push({
516
+ rule: 'svg-safety-internal-subset',
517
+ file,
518
+ line: lineOf(entity.index),
519
+ snippet: '<!ENTITY declaration — an entity-expansion surface; a bare public DOCTYPE is clean, this is not',
520
+ severity: 'error',
521
+ });
522
+ }
523
+ const doctype = /<!DOCTYPE[^>[]*\[/i.exec(content);
524
+ if (doctype) {
525
+ findings.push({
526
+ rule: 'svg-safety-internal-subset',
527
+ file,
528
+ line: lineOf(doctype.index),
529
+ snippet: 'DOCTYPE carries an internal subset "[…]" — strip it; a bare public DOCTYPE is clean',
530
+ severity: 'error',
531
+ });
532
+ }
533
+ return dedupe(findings);
534
+ }
535
+ const EXTERNAL_URL_RE = /^(?:https?:|ftp:|\/\/)/i;
536
+ function ruleExternalReference(content, elements, file, mode) {
537
+ const severity = safetySeverity(mode);
538
+ const findings = [];
539
+ const lineOf = lineIndexer(content);
540
+ for (const el of elements) {
541
+ if (el.closing)
542
+ continue;
543
+ for (const key of ['href', 'xlink:href', 'src']) {
544
+ const v = el.attrs[key];
545
+ if (v && EXTERNAL_URL_RE.test(resolvedUrl(v))) {
546
+ findings.push({
547
+ rule: 'svg-safety-external-reference',
548
+ file,
549
+ line: el.line,
550
+ snippet: `<${el.name} ${key}="${v.slice(0, 60)}"> loads from outside the file; mode "${mode}" grades it ${severity}`,
551
+ severity,
552
+ });
553
+ }
554
+ }
555
+ }
556
+ const urlRe = /url\(\s*['"]?((?:https?:|ftp:|\/\/)[^)'"\s]+)/gi;
557
+ let m;
558
+ while ((m = urlRe.exec(content)) !== null) {
559
+ findings.push({
560
+ rule: 'svg-safety-external-reference',
561
+ file,
562
+ line: lineOf(m.index),
563
+ snippet: `url(${(m[1] ?? '').slice(0, 60)}) loads from outside the file; mode "${mode}" grades it ${severity}`,
564
+ severity,
565
+ });
566
+ }
567
+ // `@import "https://…"` names its target without `url()`. The target is
568
+ // resolved before the scheme test — XML references and CSS escapes both
569
+ // reach the stylesheet loader decoded.
570
+ const importRe = /@import\s+(?:url\(\s*)?(['"])([^'"]*)\1/gi;
571
+ while ((m = importRe.exec(content)) !== null) {
572
+ const target = resolvedUrl(cssUnescape(resolvedUrl(m[2] ?? '')));
573
+ if (!EXTERNAL_URL_RE.test(target))
574
+ continue;
575
+ findings.push({
576
+ rule: 'svg-safety-external-reference',
577
+ file,
578
+ line: lineOf(m.index),
579
+ snippet: `@import "${target.slice(0, 60)}" loads from outside the file; mode "${mode}" grades it ${severity}`,
580
+ severity,
581
+ });
582
+ }
583
+ return dedupe(findings);
584
+ }
585
+ // ---------------------------------------------------------------------------
586
+ // Rule family — vector honesty (defect D3)
587
+ // ---------------------------------------------------------------------------
588
+ /** Geometry elements that ARE vector drawing. `rect`/`circle` count (D3's false red). */
589
+ export const VECTOR_GEOMETRY = new Set([
590
+ 'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'text', 'textpath', 'use',
591
+ ]);
592
+ /**
593
+ * Replaces the presence-of-`<path>` check (`logo-generation/SKILL.md:53-55`),
594
+ * which was false-green and false-red at once: a raster beside one dummy path
595
+ * passed, and a mark built from `rect`/`circle` — the very thing that file
596
+ * recommends — failed.
597
+ */
598
+ export function vectorVerdict(elements) {
599
+ let raster = 0;
600
+ let vector = 0;
601
+ for (const el of elements) {
602
+ if (el.closing)
603
+ continue;
604
+ if (el.name === 'image') {
605
+ // EVERY `<image>` counts, whatever it points at. Finding 7 of the
606
+ // fifth completion review: both arms of the `if` added 1, so the
607
+ // test and the regex behind it decided nothing — a dead constant
608
+ // and a dead branch dressed as a discrimination, which is the
609
+ // class this change has now found five times under other names.
610
+ // The comment on the else arm was the correct rule all along: an
611
+ // `<image>` of unknown type is still not vector geometry, and an
612
+ // href this scanner cannot classify is exactly the case where
613
+ // guessing "vector" would be the unsafe answer.
614
+ raster += 1;
615
+ continue;
616
+ }
617
+ if (el.name === 'foreignobject') {
618
+ raster += 1;
619
+ continue;
620
+ }
621
+ if (VECTOR_GEOMETRY.has(el.name))
622
+ vector += 1;
623
+ }
624
+ if (raster > 0) {
625
+ return {
626
+ trueVector: false,
627
+ rasterElements: raster,
628
+ vectorElements: vector,
629
+ reason: `carries ${raster} raster/foreignObject element(s) — not a true vector whatever else it contains`,
630
+ };
631
+ }
632
+ return {
633
+ trueVector: vector > 0,
634
+ rasterElements: 0,
635
+ vectorElements: vector,
636
+ reason: vector > 0
637
+ ? `${vector} vector geometry element(s), no embedded raster`
638
+ : 'no painted geometry at all',
639
+ };
640
+ }
641
+ function ruleEmbeddedRaster(elements, file) {
642
+ const verdict = vectorVerdict(elements);
643
+ if (verdict.rasterElements === 0)
644
+ return [];
645
+ const first = elements.find((el) => !el.closing && (el.name === 'image' || el.name === 'foreignobject'));
646
+ return [
647
+ {
648
+ rule: 'svg-vector-embedded-raster',
649
+ file,
650
+ line: first?.line ?? 1,
651
+ snippet: `${verdict.reason}; ${verdict.vectorElements} vector element(s) beside it does not make it one`,
652
+ severity: 'error',
653
+ },
654
+ ];
655
+ }
656
+ // ---------------------------------------------------------------------------
657
+ // Rule family — accessibility
658
+ // ---------------------------------------------------------------------------
659
+ /**
660
+ * Ids that collide the moment two graphics are inlined on one page (R7: two
661
+ * inline graphics both using `id="title"` made the accessibility tree name
662
+ * both after the first).
663
+ */
664
+ export const GENERIC_IDS = new Set([
665
+ 'title', 'desc', 'a', 'b', 'c', 'g', 'gradient', 'grad', 'mask', 'clip', 'clippath',
666
+ 'shadow', 'icon', 'path', 'path1', 'layer1', 'layer_1', 'svg', 'defs', 'filter', 'glow',
667
+ ]);
668
+ function ruleGenericId(elements, file) {
669
+ const findings = [];
670
+ for (const el of elements) {
671
+ if (el.closing)
672
+ continue;
673
+ const id = el.attrs['id'];
674
+ if (!id)
675
+ continue;
676
+ if (!GENERIC_IDS.has(id.toLowerCase()))
677
+ continue;
678
+ findings.push({
679
+ rule: 'svg-a11y-generic-id',
680
+ file,
681
+ line: el.line,
682
+ snippet: `id="${id}" collides with any second graphic inlined on the same page — namespace it`,
683
+ severity: 'warning',
684
+ });
685
+ }
686
+ return findings;
687
+ }
688
+ /** Is `elements[index]` a direct child of the root `<svg>`? */
689
+ /**
690
+ * Is the element at `index` a direct child of the root graphic?
691
+ *
692
+ * Counts open/close depth over the flat list. Step 3.1 of
693
+ * `road-to-svg-review-followups` moved the AUTHORITY for this question onto
694
+ * the parse tree ({@link directChildrenOfRoot}); this stays as the flat
695
+ * reading it is cross-checked against, because a depth counter and a tree
696
+ * agreeing on every fixture is evidence the tree is right, while replacing one
697
+ * with the other is evidence of nothing.
698
+ */
699
+ function isDirectChildOfRoot(elements, index) {
700
+ let depth = 0;
701
+ for (let i = 0; i < index; i++) {
702
+ const el = elements[i];
703
+ if (el.closing)
704
+ depth -= 1;
705
+ else if (!el.selfClosing)
706
+ depth += 1;
707
+ }
708
+ return depth === 1;
709
+ }
710
+ /**
711
+ * The root graphic's direct element children, from the parse tree.
712
+ *
713
+ * Parentage decided by ancestry rather than by a depth count: the two agree on
714
+ * a well-formed file and the tree is the one that stays right on a file that
715
+ * is not, because a stray closing tag moves a depth counter and does not move
716
+ * an ancestor chain.
717
+ */
718
+ export function directChildrenOfRoot(doc) {
719
+ const root = doc.elements.find((el) => el.name === 'svg');
720
+ if (root === undefined)
721
+ return [];
722
+ return root.children.filter((n) => n.kind === 'element');
723
+ }
724
+ /**
725
+ * Well-formedness, reported rather than assumed.
726
+ *
727
+ * Step 3.1. `<g><path></g></path>` carries both tags, both spelled correctly,
728
+ * and is not a tree — which is precisely what a flat element list cannot see
729
+ * and what a pattern over the raw text cannot decide. A file that does not
730
+ * parse is a FINDING, never a skipped file: a skip reads as clean in every
731
+ * count that matters.
732
+ *
733
+ * Capped at the first five errors. One mistake inside a deep document can
734
+ * produce an error per remaining tag, and a wall of them buries the first,
735
+ * which is the only one a reader can act on.
736
+ */
737
+ /**
738
+ * A gradient whose stops are all the same colour is a flat fill in costume.
739
+ *
740
+ * Step 3.1 of `road-to-svg-from-a-live-session`. A rejected 195 KB trace
741
+ * carried 355 gradient definitions, nine of them with identical stops, and the
742
+ * static witness reported one finding: a missing accessible name. Two external
743
+ * reviews counted the gradients by hand. This is cheap to decide from the
744
+ * bytes and nothing was deciding it.
745
+ *
746
+ * WARNING, not an error. A degenerate gradient renders correctly; it costs
747
+ * bytes and makes a file harder to edit, which is a reading rather than a
748
+ * defect — and the cost family above it is deliberately ungated for the same
749
+ * reason.
750
+ *
751
+ * Colour comparison is textual after whitespace and case normalisation, so
752
+ * `#FFF` and `#ffffff` are NOT reported as identical. That is the conservative
753
+ * direction: a false negative costs a reading, a false positive costs trust in
754
+ * every other finding beside it.
755
+ */
756
+ function ruleDegenerateGradient(elements, file) {
757
+ const findings = [];
758
+ const open = [];
759
+ for (const el of elements) {
760
+ if (el.name === 'lineargradient' || el.name === 'radialgradient') {
761
+ if (el.closing) {
762
+ const g = open.pop();
763
+ if (!g || g.stops.length < 2)
764
+ continue;
765
+ const first = g.stops[0];
766
+ const same = g.stops.every((s) => s.color === first.color && s.opacity === first.opacity);
767
+ if (!same)
768
+ continue;
769
+ findings.push({
770
+ rule: 'svg-structure-degenerate-gradient',
771
+ file,
772
+ line: g.el.line,
773
+ snippet: `<${g.el.name} id="${g.el.attrs['id'] ?? ''}"> has ${String(g.stops.length)} stops, all ` +
774
+ `${first.color || '(inherited)'} at opacity ${first.opacity} — a flat fill in a gradient's ` +
775
+ 'clothes, which costs bytes and makes the file harder to edit',
776
+ severity: 'warning',
777
+ });
778
+ }
779
+ else if (!el.selfClosing) {
780
+ open.push({ el, stops: [] });
781
+ }
782
+ continue;
783
+ }
784
+ if (el.name === 'stop' && !el.closing) {
785
+ const host = open[open.length - 1];
786
+ if (!host)
787
+ continue;
788
+ const style = el.attrs['style'] ?? '';
789
+ const fromStyle = /stop-color\s*:\s*([^;]+)/i.exec(style)?.[1];
790
+ const opacityFromStyle = /stop-opacity\s*:\s*([^;]+)/i.exec(style)?.[1];
791
+ host.stops.push({
792
+ color: (fromStyle ?? el.attrs['stop-color'] ?? '').trim().toLowerCase(),
793
+ opacity: (opacityFromStyle ?? el.attrs['stop-opacity'] ?? '1').trim(),
794
+ });
795
+ }
796
+ }
797
+ return findings;
798
+ }
799
+ function ruleWellFormed(doc, file) {
800
+ if (doc.errors.length === 0)
801
+ return [];
802
+ const shown = doc.errors.slice(0, 5);
803
+ const more = doc.errors.length - shown.length;
804
+ return shown.map((e, i) => ({
805
+ rule: 'svg-structure-not-well-formed',
806
+ file,
807
+ line: e.line,
808
+ snippet: `${e.message} (column ${String(e.column)})` +
809
+ (i === shown.length - 1 && more > 0 ? ` — and ${String(more)} further error(s)` : ''),
810
+ severity: 'error',
811
+ }));
812
+ }
813
+ function rootNameState(content, doc, elements) {
814
+ const rootIdx = elements.findIndex((el) => el.name === 'svg' && !el.closing);
815
+ const root = rootIdx === -1 ? undefined : elements[rootIdx];
816
+ // A DIRECT child of the root only. The first `<title>` ANYWHERE was wrong
817
+ // in both directions: a chart's nested series title read as the graphic's
818
+ // name, producing a false `svg-a11y-double-name` beside an `aria-label`,
819
+ // and hiding a genuinely unnamed graphic behind a descendant's title.
820
+ const titleIdx = elements.findIndex((el, i) => el.name === 'title' && !el.closing && rootIdx !== -1 && isDirectChildOfRoot(elements, i));
821
+ // Step 3.1: the PARSE TREE decides parentage, and the depth count above is
822
+ // kept as the reading it is checked against. The two agree on every
823
+ // well-formed file; where they disagree the file is not well-formed, which
824
+ // `svg-structure-not-well-formed` already reports, and the tree is the one
825
+ // that is still right — a stray closing tag moves a depth counter and does
826
+ // not move an ancestor chain.
827
+ const titleIsDirectChild = directChildrenOfRoot(doc).some((n) => n.name === 'title');
828
+ const hasTitle = titleIsDirectChild && titleIdx !== -1 && textOf(content, elements, titleIdx).trim().length > 0;
829
+ // `aria-labelledby="x"` + `<title id="x">` is the CANONICAL pattern, not a
830
+ // collision: the attribute POINTS AT the title rather than competing with
831
+ // it. Counting the pair as two names reds the one shape the accessibility
832
+ // guidance actually asks for, which is the false red that gets a linter
833
+ // switched off (Risk 1).
834
+ const titleIds = new Set(elements
835
+ .filter((el) => el.name === 'title' && !el.closing && el.attrs['id'])
836
+ .map((el) => el.attrs['id']));
837
+ const labelledBy = (root?.attrs['aria-labelledby'] ?? '').trim();
838
+ const labelledByTargets = labelledBy.split(/\s+/).filter(Boolean);
839
+ const labelledByOwnTitle = labelledByTargets.length > 0 && labelledByTargets.every((id) => titleIds.has(id));
840
+ return {
841
+ root,
842
+ hidden: (root?.attrs['aria-hidden'] ?? '').toLowerCase() === 'true',
843
+ hasTitle,
844
+ hasLabel: Boolean(root?.attrs['aria-label']?.trim()),
845
+ hasLabelledBy: labelledBy.length > 0,
846
+ labelledByOwnTitle,
847
+ hasRoleImg: (root?.attrs['role'] ?? '').toLowerCase() === 'img',
848
+ };
849
+ }
850
+ function ruleAccessibleName(s, file) {
851
+ if (!s.root)
852
+ return [];
853
+ // A graphic declared decorative is correct with no name — that is the
854
+ // other half of the pattern, not a gap in it.
855
+ if (s.hidden)
856
+ return [];
857
+ if (s.hasTitle || s.hasLabel || s.hasLabelledBy)
858
+ return [];
859
+ return [
860
+ {
861
+ rule: 'svg-a11y-no-accessible-name',
862
+ file,
863
+ line: s.root.line,
864
+ snippet: 'graphic has no accessible name and is not aria-hidden="true" — add role="img" with a <title>, ' +
865
+ 'or declare it decorative',
866
+ severity: 'error',
867
+ },
868
+ ];
869
+ }
870
+ function ruleDoubleName(s, file) {
871
+ if (!s.root)
872
+ return [];
873
+ // A labelledby that resolves to this graphic's own <title> is the SAME
874
+ // name reached through a reference, so it is not counted twice.
875
+ const labelledByIsSeparate = s.hasLabelledBy && !s.labelledByOwnTitle;
876
+ const names = [s.hasLabel, labelledByIsSeparate, s.hasTitle].filter(Boolean).length;
877
+ if (names < 2)
878
+ return [];
879
+ return [
880
+ {
881
+ rule: 'svg-a11y-double-name',
882
+ file,
883
+ line: s.root.line,
884
+ snippet: 'graphic declares more than one accessible name (aria-label / aria-labelledby / <title>) — ' +
885
+ 'only one wins and which one is renderer-dependent',
886
+ severity: 'error',
887
+ },
888
+ ];
889
+ }
890
+ // ---------------------------------------------------------------------------
891
+ // Rule family — motion. Every rule binds per element or per selector (R9).
892
+ // ---------------------------------------------------------------------------
893
+ function ruleKeySplinesRange(elements, file) {
894
+ const findings = [];
895
+ for (const el of elements) {
896
+ if (el.closing)
897
+ continue;
898
+ const raw = el.attrs['keysplines'];
899
+ if (!raw)
900
+ continue;
901
+ const nums = raw.split(/[;\s,]+/).filter(Boolean).map(Number);
902
+ const bad = nums.filter((n) => Number.isFinite(n) && (n < 0 || n > 1));
903
+ if (!bad.length)
904
+ continue;
905
+ findings.push({
906
+ rule: 'svg-motion-keysplines-range',
907
+ file,
908
+ line: el.line,
909
+ snippet: `<${el.name} keySplines="${raw}"> holds ${bad.join(', ')} outside [0,1] — SMIL rejects the ` +
910
+ 'whole animation rather than clamping it, so the element never moves',
911
+ severity: 'error',
912
+ });
913
+ }
914
+ return findings;
915
+ }
916
+ /**
917
+ * `transform-origin` without `transform-box`.
918
+ *
919
+ * PER ELEMENT and PER SELECTOR, which is the R9 correction: a file-scoped
920
+ * "does `transform-box` appear anywhere" test passes a file whose wrong
921
+ * element sits beside a correct twin, and the fixture plants exactly that.
922
+ */
923
+ function ruleTransformBox(content, elements, file) {
924
+ const findings = [];
925
+ for (const el of elements) {
926
+ if (el.closing)
927
+ continue;
928
+ const style = el.attrs['style'] ?? '';
929
+ const decls = declarationsOf(style);
930
+ const origin = decls['transform-origin'] ?? el.attrs['transform-origin'];
931
+ if (!origin)
932
+ continue;
933
+ if (isOriginDefault(origin))
934
+ continue;
935
+ const box = decls['transform-box'] ?? el.attrs['transform-box'];
936
+ if (box)
937
+ continue;
938
+ findings.push({
939
+ rule: 'svg-motion-transform-box',
940
+ file,
941
+ line: el.line,
942
+ snippet: `<${el.name}> sets transform-origin: ${origin} with no transform-box — in SVG the origin ` +
943
+ 'resolves against the view box, not the element, so the pivot lands somewhere else',
944
+ severity: 'error',
945
+ });
946
+ }
947
+ for (const rule of readCssRules(content)) {
948
+ const decls = declarationsOf(rule.body);
949
+ const origin = decls['transform-origin'];
950
+ if (!origin || isOriginDefault(origin))
951
+ continue;
952
+ if (decls['transform-box'])
953
+ continue;
954
+ findings.push({
955
+ rule: 'svg-motion-transform-box',
956
+ file,
957
+ line: rule.line,
958
+ snippet: `selector "${rule.selector}" sets transform-origin: ${origin} with no transform-box — ` +
959
+ 'the pivot resolves against the view box',
960
+ severity: 'error',
961
+ });
962
+ }
963
+ return dedupe(findings);
964
+ }
965
+ function isOriginDefault(value) {
966
+ const v = value.trim().toLowerCase();
967
+ return v === '0 0' || v === '0px 0px' || v === '0' || v === 'left top' || v === '0% 0%';
968
+ }
969
+ function ruleNoReducedMotion(content, elements, file) {
970
+ if (!hasSmilMarkup(content) && !hasCssAnimation(content))
971
+ return [];
972
+ if (hasReducedMotionQuery(content))
973
+ return [];
974
+ const line = hasSmilMarkup(content)
975
+ ? firstSmilLine(content)
976
+ : (elements.find((el) => !el.closing)?.line ?? 1);
977
+ return [
978
+ {
979
+ rule: 'svg-motion-no-reduced-motion',
980
+ file,
981
+ line,
982
+ snippet: 'the file animates and carries no prefers-reduced-motion route at all — a viewer who asked ' +
983
+ 'the OS to stop motion still gets it',
984
+ severity: 'error',
985
+ },
986
+ ];
987
+ }
988
+ /**
989
+ * SMIL whose only reduced-motion route is CSS (R4).
990
+ *
991
+ * Measured: under emulated `reduce`, a page-level `svg * { animation: none
992
+ * !important }` left an `<animate>` moving `cx` from 20.2 to 76.8 in 700 ms.
993
+ * CSS `animation` does not reach SMIL, so a file that believes it is covered
994
+ * is covered by nothing. The reading itself lives in `_lib/svg_motion.ts`, so
995
+ * DQ4's inline half (step 1.3) cannot drift away from this one.
996
+ */
997
+ function ruleSmilCssOnlyBranch(content, file) {
998
+ if (smilVerdict(content) !== 'css-only')
999
+ return [];
1000
+ return [
1001
+ {
1002
+ rule: 'svg-motion-smil-css-only',
1003
+ file,
1004
+ line: firstSmilLine(content),
1005
+ snippet: 'this file animates with SMIL and its only reduced-motion route is a CSS declaration — ' +
1006
+ 'CSS animation/transition properties do not reach SMIL, so the animation keeps running',
1007
+ severity: 'error',
1008
+ },
1009
+ ];
1010
+ }
1011
+ const PATH_COMMAND_RE = /[a-zA-Z]/g;
1012
+ /**
1013
+ * An `<animate attributeName="d">` whose `values` entries are not the same
1014
+ * shape. SVG morphs a path by interpolating coordinate lists, so two values
1015
+ * with a different command count or a different command sequence do not
1016
+ * interpolate — the browser snaps instead of morphing, or refuses outright.
1017
+ */
1018
+ function ruleDMorphMismatch(elements, file) {
1019
+ const findings = [];
1020
+ for (const el of elements) {
1021
+ if (el.closing)
1022
+ continue;
1023
+ if (!SMIL_TAGS.has(el.name))
1024
+ continue;
1025
+ if ((el.attrs['attributename'] ?? '').toLowerCase() !== 'd')
1026
+ continue;
1027
+ const raw = el.attrs['values'];
1028
+ if (!raw)
1029
+ continue;
1030
+ const entries = raw.split(';').map((s) => s.trim()).filter(Boolean);
1031
+ if (entries.length < 2)
1032
+ continue;
1033
+ const shapes = entries.map((e) => (e.match(PATH_COMMAND_RE) ?? []).join('').toUpperCase());
1034
+ const first = shapes[0];
1035
+ const differing = shapes.findIndex((s) => s !== first);
1036
+ if (differing === -1)
1037
+ continue;
1038
+ findings.push({
1039
+ rule: 'svg-motion-d-morph-mismatch',
1040
+ file,
1041
+ line: el.line,
1042
+ snippet: `<${el.name} attributeName="d"> value ${differing + 1} has command sequence "${shapes[differing]}" ` +
1043
+ `against "${first}" in value 1 — a path morph interpolates coordinates, not commands`,
1044
+ severity: 'error',
1045
+ });
1046
+ }
1047
+ return findings;
1048
+ }
1049
+ // ---------------------------------------------------------------------------
1050
+ // Composition
1051
+ // ---------------------------------------------------------------------------
1052
+ function dedupe(findings) {
1053
+ const seen = new Set();
1054
+ const out = [];
1055
+ for (const f of findings) {
1056
+ const key = `${f.rule}::${f.file}::${f.line}::${f.snippet}`;
1057
+ if (seen.has(key))
1058
+ continue;
1059
+ seen.add(key);
1060
+ out.push(f);
1061
+ }
1062
+ return out;
1063
+ }
1064
+ /** Every static finding for one file's bytes. */
1065
+ export function scanSvg(content, relPath, opts = {}) {
1066
+ const mode = opts.delivery ?? 'inline';
1067
+ const elements = parseElements(content);
1068
+ // ONE parse, and ONE name state. Finding 10 of the third completion
1069
+ // review, re-measured because the previous round's disposition was taken
1070
+ // on the wrong regime: over the seven small real fixtures the duplicate
1071
+ // parse is 0.03% of the dispatch, which is why it was accepted then — but
1072
+ // on a 760 KB file `parseDocument` is 8.2 ms against a 163 ms `scanSvg`,
1073
+ // so two redundant parses plus a recomputed name state are about 10% of a
1074
+ // pass that now runs on every `.svg` write. The document and the state are
1075
+ // passed rather than cached: a module-level cache in a hook is shared
1076
+ // mutable state, and the only thing that needed fixing was recomputation
1077
+ // inside one call.
1078
+ const doc = parseDocument(content);
1079
+ const nameState = rootNameState(content, doc, elements);
1080
+ // Finding 2.3: the content-level rules read the SAME file the element
1081
+ // parser reads. `maskComments` keeps every offset, so a line number taken
1082
+ // here still names the right line in the raw text.
1083
+ const text = maskComments(content);
1084
+ const raw = [
1085
+ ...ruleWellFormed(doc, relPath),
1086
+ // No `text` argument: the rule reads elements only, and comment
1087
+ // skipping already happened inside `parseElements`. Passing the masked
1088
+ // text told a reader the parameter was load-bearing (finding 12);
1089
+ // `no-unused-vars` could not catch it because two later parameters were
1090
+ // used.
1091
+ ...ruleDegenerateGradient(elements, relPath),
1092
+ ...ruleMissingXmlns(elements, relPath),
1093
+ ...ruleOutsideViewBox(elements, relPath),
1094
+ ...ruleUnparseablePath(elements, relPath),
1095
+ ...ruleUnusedDefs(text, elements, relPath),
1096
+ ...ruleDanglingReference(text, elements, relPath),
1097
+ ...ruleActiveContent(text, elements, relPath, mode),
1098
+ ...ruleInternalSubset(text, relPath),
1099
+ ...ruleExternalReference(text, elements, relPath, mode),
1100
+ ...ruleEmbeddedRaster(elements, relPath),
1101
+ ...ruleGenericId(elements, relPath),
1102
+ ...ruleAccessibleName(nameState, relPath),
1103
+ ...ruleDoubleName(nameState, relPath),
1104
+ ...ruleKeySplinesRange(elements, relPath),
1105
+ ...ruleTransformBox(text, elements, relPath),
1106
+ ...ruleNoReducedMotion(text, elements, relPath),
1107
+ ...ruleSmilCssOnlyBranch(text, relPath),
1108
+ ...ruleDMorphMismatch(elements, relPath),
1109
+ ];
1110
+ // RAW, not masked — the inline `svg-asset-disable` markers live in comments.
1111
+ const ignores = parseIgnores(content.split('\n'));
1112
+ return dedupe(raw).filter((f) => !isIgnored(ignores, f));
1113
+ }
1114
+ /**
1115
+ * Inline SVG inside a host HTML page.
1116
+ *
1117
+ * Each `<svg>…</svg>` is scanned on its own so a per-element rule keeps its
1118
+ * own line numbers, and the id rules then run once ACROSS the graphics —
1119
+ * which is the whole of R7: two graphics are each individually fine and
1120
+ * collide only together.
1121
+ */
1122
+ /**
1123
+ * The TOP-LEVEL `<svg>` elements of a host document, by offset.
1124
+ *
1125
+ * Depth-counted, not matched. Finding 3 of the fifth completion review: the
1126
+ * non-greedy `/<svg\b[\s\S]*?<\/svg\s*>/` ended at the FIRST `</svg>`, which
1127
+ * on a graphic containing a nested `<svg>` — legal, and how a sprite or a
1128
+ * symbol is embedded — is the inner one. The slice was then a truncated
1129
+ * fragment, so the host reported a FALSE `svg-structure-not-well-formed` for
1130
+ * an unclosed root AND missed everything after the inner close, including a
1131
+ * real `<script>` that the same bytes report correctly when scanned flat. A
1132
+ * false structure error and a missed active-content error, from one regex.
1133
+ *
1134
+ * A nested graphic is part of its parent and is not returned separately: the
1135
+ * rules read the whole thing, exactly as a browser does.
1136
+ */
1137
+ /**
1138
+ * Blank the BODY of every `<script>` and `<style>`, offsets and newlines kept.
1139
+ *
1140
+ * Finding 1 of the review of this branch, and it is a regression this branch
1141
+ * introduced: emitting an unterminated graphic made `topLevelGraphics` count
1142
+ * `<svg …>` wherever it appears — including inside a JavaScript string, where
1143
+ * it is text rather than markup. A host containing NO SVG element at all, but
1144
+ * mentioning one in a script, went from zero findings to five, four of them in
1145
+ * the blocking families the static witness counts. Fail-CLOSED rather than
1146
+ * fail-open, so it refuses a valid document instead of passing a bad one, but
1147
+ * a gate that refuses a correct turn is the failure this change has spent six
1148
+ * rounds removing.
1149
+ *
1150
+ * The tags survive; only what is between them is blanked. `ruleActiveContent`
1151
+ * keys on the element NAME, so a real `<script>` inside a graphic is still
1152
+ * found — masking its body hides nothing the rules read.
1153
+ */
1154
+ function maskScriptBodies(content) {
1155
+ const out = content.split('');
1156
+ const re = /<(script|style)\b[^>]*>/gi;
1157
+ let m;
1158
+ while ((m = re.exec(content)) !== null) {
1159
+ const bodyStart = m.index + m[0].length;
1160
+ const close = content.toLowerCase().indexOf(`</${m[1].toLowerCase()}`, bodyStart);
1161
+ const bodyEnd = close === -1 ? content.length : close;
1162
+ for (let i = bodyStart; i < bodyEnd; i += 1) {
1163
+ if (out[i] !== '\n' && out[i] !== '\r')
1164
+ out[i] = ' ';
1165
+ }
1166
+ re.lastIndex = bodyEnd;
1167
+ }
1168
+ return out.join('');
1169
+ }
1170
+ function topLevelGraphics(text) {
1171
+ const out = [];
1172
+ const tagRe = /<(\/?)svg\b[^>]*?(\/?)>/gi;
1173
+ let depth = 0;
1174
+ let start = -1;
1175
+ let t;
1176
+ while ((t = tagRe.exec(text)) !== null) {
1177
+ const closing = t[1] === '/';
1178
+ const selfClosing = t[2] === '/';
1179
+ if (closing) {
1180
+ if (depth === 0)
1181
+ continue;
1182
+ depth -= 1;
1183
+ if (depth === 0 && start !== -1) {
1184
+ out.push({ start, end: t.index + t[0].length });
1185
+ start = -1;
1186
+ }
1187
+ continue;
1188
+ }
1189
+ if (selfClosing) {
1190
+ // An empty graphic at top level is still a graphic.
1191
+ if (depth === 0)
1192
+ out.push({ start: t.index, end: t.index + t[0].length });
1193
+ continue;
1194
+ }
1195
+ if (depth === 0)
1196
+ start = t.index;
1197
+ depth += 1;
1198
+ }
1199
+ // AN UNTERMINATED GRAPHIC IS STILL A GRAPHIC, to end of input.
1200
+ //
1201
+ // Finding 1 of the sixth completion review, and it FAILS OPEN, which is
1202
+ // why it is the one that matters: a host whose inline `<svg>` is never
1203
+ // closed was dropped whole, so `scanHostHtml` returned zero findings — a
1204
+ // planted `<script>` included — `staticWitness` then reported `errors: 0`,
1205
+ // and that is exactly the condition `dischargeFor` uses to discharge the
1206
+ // BLOCKING turn-end obligation on the static-only path. A malformed file
1207
+ // discharged a gate that a well-formed one with the same script does not.
1208
+ //
1209
+ // NOT a regression of the depth counter that replaced the regex: the old
1210
+ // `/<svg\b[\s\S]*?<\/svg\s*>/` required a closing tag and matched nothing
1211
+ // here either. The rewrite preserved the hole rather than opening it, and
1212
+ // the honest version of that sentence is worth more than the flattering
1213
+ // one. Emitting the fragment hands `scanSvg` a document whose root is
1214
+ // never closed — it reports that AND everything inside it, which is both
1215
+ // answers the reader needs.
1216
+ if (depth > 0 && start !== -1)
1217
+ out.push({ start, end: text.length });
1218
+ return out;
1219
+ }
1220
+ export function scanHostHtml(content, relPath, opts = {}) {
1221
+ const mode = opts.delivery ?? 'inline';
1222
+ const findings = [];
1223
+ const lineOf = lineIndexer(content);
1224
+ const graphics = [];
1225
+ // Finding 2.3 on the host side: a graphic inside an HTML comment is not on
1226
+ // the page, so it is not scanned and its ids do not collide with anything.
1227
+ // Masking preserves offsets, so `lineOf` — built from the raw text — still
1228
+ // answers correctly for the graphics that survive.
1229
+ const hostText = maskScriptBodies(maskComments(content));
1230
+ for (const m of topLevelGraphics(hostText)) {
1231
+ // The MATCH is found in the masked text — a graphic inside an HTML
1232
+ // comment is not on the page — but the BODY is sliced from the raw
1233
+ // text at the same offsets.
1234
+ //
1235
+ // Finding 16 of this change's completion review: handing the masked
1236
+ // slice to `scanSvg` meant its own `parseIgnores` saw every
1237
+ // `svg-asset-disable` marker blanked, which is precisely the failure
1238
+ // `maskComments`' doc warns about. Suppression survived only because
1239
+ // this function re-filters on absolute lines at the end — the inner
1240
+ // pass was inert, and silently so, which made the outer re-filter
1241
+ // load-bearing without anything saying it was.
1242
+ graphics.push({ body: content.slice(m.start, m.end), offset: m.start });
1243
+ }
1244
+ for (const g of graphics) {
1245
+ const baseLine = lineOf(g.offset) - 1;
1246
+ // Two families are dropped for an inline graphic, each for a reason
1247
+ // the slice itself cannot see:
1248
+ // · the host supplies the namespace, so a missing xmlns is correct;
1249
+ // · the reduced-motion ROUTE may live in the host's own <style> or
1250
+ // in a <picture> wrapper, and this slice ends at </svg>. Judging
1251
+ // the route from inside the slice would red every correctly
1252
+ // covered inline animation. DQ4 owns the inline case (step 1.3)
1253
+ // and reads the whole document, which is where that answer is.
1254
+ for (const f of scanSvg(g.body, relPath, { delivery: mode })) {
1255
+ if (f.rule === 'svg-structure-missing-xmlns')
1256
+ continue;
1257
+ if (f.rule === 'svg-motion-no-reduced-motion')
1258
+ continue;
1259
+ if (f.rule === 'svg-motion-smil-css-only')
1260
+ continue;
1261
+ findings.push({ ...f, line: f.line + baseLine });
1262
+ }
1263
+ }
1264
+ // Cross-graphic id collisions — the finding R7 named.
1265
+ const seen = new Map();
1266
+ for (const g of graphics) {
1267
+ const baseLine = lineOf(g.offset) - 1;
1268
+ for (const el of parseElements(g.body)) {
1269
+ if (el.closing)
1270
+ continue;
1271
+ const id = el.attrs['id'];
1272
+ if (!id)
1273
+ continue;
1274
+ const key = id.toLowerCase();
1275
+ if (seen.has(key)) {
1276
+ findings.push({
1277
+ // ITS OWN RULE ID. Finding 4 of the fifth completion
1278
+ // review: this error shared `svg-a11y-generic-id` with the
1279
+ // single-graphic WARNING about a generic name, so one
1280
+ // `svg-asset-disable svg-a11y-generic-id` — a reasonable
1281
+ // thing to write when you have deliberately named
1282
+ // something `icon` — silenced a real collision between two
1283
+ // graphics as well. Two findings, two severities, two
1284
+ // remedies; one id was one too few.
1285
+ rule: 'svg-a11y-duplicate-id',
1286
+ file: relPath,
1287
+ line: el.line + baseLine,
1288
+ snippet: `id="${id}" is already used by another inline graphic on this page (line ${seen.get(key)}) — ` +
1289
+ 'the accessibility tree names both after the first',
1290
+ severity: 'error',
1291
+ });
1292
+ }
1293
+ else {
1294
+ seen.set(key, el.line + baseLine);
1295
+ }
1296
+ }
1297
+ }
1298
+ const ignores = parseIgnores(content.split('\n'));
1299
+ return dedupe(findings).filter((f) => !isIgnored(ignores, f));
1300
+ }
1301
+ export function parseIgnores(lines) {
1302
+ const ignores = new Map();
1303
+ const slot = (rule) => {
1304
+ const found = ignores.get(rule) ?? { lines: new Set(), wholeFile: false };
1305
+ ignores.set(rule, found);
1306
+ return found;
1307
+ };
1308
+ const add = (rule, line) => {
1309
+ slot(rule).lines.add(line);
1310
+ };
1311
+ for (let i = 0; i < lines.length; i++) {
1312
+ const line = lines[i] ?? '';
1313
+ const next = /svg-asset-disable-next-line\s+([\w-]+)/i.exec(line);
1314
+ if (next?.[1])
1315
+ add(next[1].toLowerCase(), i + 2);
1316
+ const same = /svg-asset-disable-line\s+([\w-]+)/i.exec(line);
1317
+ if (same?.[1])
1318
+ add(same[1].toLowerCase(), i + 1);
1319
+ const whole = /svg-asset-disable\s+([\w-]+)/i.exec(line);
1320
+ if (whole?.[1] && !/-next-line|-line\s/.test(line)) {
1321
+ slot(whole[1].toLowerCase()).wholeFile = true;
1322
+ }
1323
+ }
1324
+ return ignores;
1325
+ }
1326
+ function isIgnored(ignores, finding) {
1327
+ // The safety family is not suppressible from inside the file it judges: the
1328
+ // markers are the scanned file's own bytes, and a file that silences its
1329
+ // own script zeroes the error count a blocking gate discharges on.
1330
+ if (finding.rule.startsWith('svg-safety-'))
1331
+ return false;
1332
+ const hit = ignores.get(finding.rule.toLowerCase());
1333
+ if (hit === undefined)
1334
+ return false;
1335
+ return hit.wholeFile || hit.lines.has(finding.line);
1336
+ }
1337
+ //# sourceMappingURL=svg_rules.js.map