periplus-map 0.2.1__py3-none-any.whl

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 (272) hide show
  1. periplus/__init__.py +70 -0
  2. periplus/checkmap.py +80 -0
  3. periplus/cli.py +658 -0
  4. periplus/contract/schema/map.schema.json +138 -0
  5. periplus/contract/schema/navigation.schema.json +200 -0
  6. periplus/contract/schema/pack-file.schema.json +1405 -0
  7. periplus/contract/schema/pack-manifest.schema.json +245 -0
  8. periplus/contract/schema/project-settings.schema.json +85 -0
  9. periplus/contract/spec/engine.md +151 -0
  10. periplus/contract/spec/rules.md +352 -0
  11. periplus/detect.py +228 -0
  12. periplus/engine/__init__.py +5 -0
  13. periplus/engine/check.py +481 -0
  14. periplus/engine/emit.py +368 -0
  15. periplus/engine/packload.py +2321 -0
  16. periplus/engine/pattern.py +27 -0
  17. periplus/engine/rules.py +1946 -0
  18. periplus/engine/select.py +491 -0
  19. periplus/engine/source.py +172 -0
  20. periplus/engine/tree.py +424 -0
  21. periplus/errors.py +332 -0
  22. periplus/init.py +358 -0
  23. periplus/layout.py +28 -0
  24. periplus/manifest.py +498 -0
  25. periplus/map.py +364 -0
  26. periplus/navigate/__init__.py +1 -0
  27. periplus/navigate/find.py +143 -0
  28. periplus/navigate/ids.py +30 -0
  29. periplus/navigate/listing.py +42 -0
  30. periplus/navigate/mapfile.py +189 -0
  31. periplus/navigate/path.py +175 -0
  32. periplus/navigate/show.py +175 -0
  33. periplus/navigate/types.py +83 -0
  34. periplus/navigate/walk.py +138 -0
  35. periplus/packs/advancedqueue_basic@0.0.1/pack.yaml +14 -0
  36. periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type.yaml +9 -0
  37. periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type_annotation.yaml +43 -0
  38. periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type_attribute.yaml +43 -0
  39. periplus/packs/ai_basic@0.0.1/ai_agent/ai_agent.yaml +8 -0
  40. periplus/packs/ai_basic@0.0.1/ai_agent/ai_agent_attribute.yaml +43 -0
  41. periplus/packs/ai_basic@0.0.1/ai_function_call/ai_function_call.yaml +8 -0
  42. periplus/packs/ai_basic@0.0.1/ai_function_call/ai_function_call_attribute.yaml +43 -0
  43. periplus/packs/ai_basic@0.0.1/pack.yaml +15 -0
  44. periplus/packs/config_pages_basic@0.0.1/bundle/config_pages_bundle_config.yaml +54 -0
  45. periplus/packs/config_pages_basic@0.0.1/pack.yaml +11 -0
  46. periplus/packs/crop_basic@0.0.1/bundle/crop_bundle_config.yaml +54 -0
  47. periplus/packs/crop_basic@0.0.1/pack.yaml +11 -0
  48. periplus/packs/drupal@0.1.0/block/block_placement.yaml +25 -0
  49. periplus/packs/drupal@0.1.0/block/block_placement_code.yaml +58 -0
  50. periplus/packs/drupal@0.1.0/block/block_placement_config.yaml +53 -0
  51. periplus/packs/drupal@0.1.0/block/block_plugin.yaml +52 -0
  52. periplus/packs/drupal@0.1.0/block/block_plugin_altered.yaml +61 -0
  53. periplus/packs/drupal@0.1.0/block/block_plugin_annotation.yaml +69 -0
  54. periplus/packs/drupal@0.1.0/block/block_plugin_attribute.yaml +56 -0
  55. periplus/packs/drupal@0.1.0/block/block_plugin_content.yaml +46 -0
  56. periplus/packs/drupal@0.1.0/block/block_plugin_derived.yaml +110 -0
  57. periplus/packs/drupal@0.1.0/block/block_plugin_deriver.yaml +30 -0
  58. periplus/packs/drupal@0.1.0/block/block_type.yaml +14 -0
  59. periplus/packs/drupal@0.1.0/block/block_type_code.yaml +43 -0
  60. periplus/packs/drupal@0.1.0/block/block_type_config.yaml +45 -0
  61. periplus/packs/drupal@0.1.0/cache/cache.yaml +18 -0
  62. periplus/packs/drupal@0.1.0/cache/cache_backend.yaml +31 -0
  63. periplus/packs/drupal@0.1.0/component/sdc.yaml +19 -0
  64. periplus/packs/drupal@0.1.0/component/sdc_definition.yaml +25 -0
  65. periplus/packs/drupal@0.1.0/component/sdc_use.yaml +25 -0
  66. periplus/packs/drupal@0.1.0/config/config_object.yaml +65 -0
  67. periplus/packs/drupal@0.1.0/display/display.yaml +32 -0
  68. periplus/packs/drupal@0.1.0/display/display_code.yaml +53 -0
  69. periplus/packs/drupal@0.1.0/display/display_config.yaml +95 -0
  70. periplus/packs/drupal@0.1.0/entity/entity.yaml +33 -0
  71. periplus/packs/drupal@0.1.0/entity/entity_class.yaml +60 -0
  72. periplus/packs/drupal@0.1.0/entity/entity_save.yaml +27 -0
  73. periplus/packs/drupal@0.1.0/entity/entity_type_annotation.yaml +85 -0
  74. periplus/packs/drupal@0.1.0/entity/entity_type_attribute.yaml +76 -0
  75. periplus/packs/drupal@0.1.0/field/field_instance_code.yaml +40 -0
  76. periplus/packs/drupal@0.1.0/field/field_instance_config.yaml +58 -0
  77. periplus/packs/drupal@0.1.0/field/field_storage.yaml +37 -0
  78. periplus/packs/drupal@0.1.0/field/field_storage_code.yaml +39 -0
  79. periplus/packs/drupal@0.1.0/field/field_storage_config.yaml +24 -0
  80. periplus/packs/drupal@0.1.0/form/form.yaml +48 -0
  81. periplus/packs/drupal@0.1.0/form/form_class.yaml +50 -0
  82. periplus/packs/drupal@0.1.0/form/form_id.yaml +48 -0
  83. periplus/packs/drupal@0.1.0/form/form_methods.yaml +39 -0
  84. periplus/packs/drupal@0.1.0/hook/hook.yaml +8 -0
  85. periplus/packs/drupal@0.1.0/hook/hook_attribute.yaml +28 -0
  86. periplus/packs/drupal@0.1.0/module/module.yaml +8 -0
  87. periplus/packs/drupal@0.1.0/module/module_info.yaml +50 -0
  88. periplus/packs/drupal@0.1.0/pack.yaml +184 -0
  89. periplus/packs/drupal@0.1.0/plugin/plugin.yaml +17 -0
  90. periplus/packs/drupal@0.1.0/plugin/plugin_annotation.yaml +120 -0
  91. periplus/packs/drupal@0.1.0/recommendations/recommendations.yaml +14 -0
  92. periplus/packs/drupal@0.1.0/service/service.yaml +19 -0
  93. periplus/packs/drupal@0.1.0/service/service_autowire.yaml +28 -0
  94. periplus/packs/drupal@0.1.0/service/service_definition.yaml +24 -0
  95. periplus/packs/drupal@0.1.0/service/service_use.yaml +43 -0
  96. periplus/packs/drupal_basic@0.3.0/action/action.yaml +9 -0
  97. periplus/packs/drupal_basic@0.3.0/action/action_annotation.yaml +43 -0
  98. periplus/packs/drupal_basic@0.3.0/action/action_attribute.yaml +43 -0
  99. periplus/packs/drupal_basic@0.3.0/block/block_placement.yaml +16 -0
  100. periplus/packs/drupal_basic@0.3.0/block/block_placement_config.yaml +51 -0
  101. periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin.yaml +14 -0
  102. periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin_annotation.yaml +47 -0
  103. periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin_attribute.yaml +43 -0
  104. periplus/packs/drupal_basic@0.3.0/block_type/block_type.yaml +8 -0
  105. periplus/packs/drupal_basic@0.3.0/block_type/block_type_config.yaml +54 -0
  106. periplus/packs/drupal_basic@0.3.0/boundary/boundary_types.yaml +19 -0
  107. periplus/packs/drupal_basic@0.3.0/bundle/bundle.yaml +20 -0
  108. periplus/packs/drupal_basic@0.3.0/bundle/bundle_config.yaml +393 -0
  109. periplus/packs/drupal_basic@0.3.0/call/call.yaml +349 -0
  110. periplus/packs/drupal_basic@0.3.0/component/component.yaml +30 -0
  111. periplus/packs/drupal_basic@0.3.0/config/config_object.yaml +119 -0
  112. periplus/packs/drupal_basic@0.3.0/constraint/constraint.yaml +8 -0
  113. periplus/packs/drupal_basic@0.3.0/constraint/constraint_annotation.yaml +43 -0
  114. periplus/packs/drupal_basic@0.3.0/constraint/constraint_attribute.yaml +43 -0
  115. periplus/packs/drupal_basic@0.3.0/display/display.yaml +30 -0
  116. periplus/packs/drupal_basic@0.3.0/display/display_config.yaml +203 -0
  117. periplus/packs/drupal_basic@0.3.0/display/display_mode_config.yaml +60 -0
  118. periplus/packs/drupal_basic@0.3.0/entity_type/config_entity_type_annotation.yaml +46 -0
  119. periplus/packs/drupal_basic@0.3.0/entity_type/config_entity_type_attribute.yaml +43 -0
  120. periplus/packs/drupal_basic@0.3.0/entity_type/content_entity_type_annotation.yaml +46 -0
  121. periplus/packs/drupal_basic@0.3.0/entity_type/content_entity_type_attribute.yaml +43 -0
  122. periplus/packs/drupal_basic@0.3.0/entity_type/entity_type.yaml +16 -0
  123. periplus/packs/drupal_basic@0.3.0/field/field_instance_config.yaml +116 -0
  124. periplus/packs/drupal_basic@0.3.0/field/field_storage.yaml +21 -0
  125. periplus/packs/drupal_basic@0.3.0/field/field_storage_config.yaml +45 -0
  126. periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter.yaml +8 -0
  127. periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter_annotation.yaml +46 -0
  128. periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter_attribute.yaml +43 -0
  129. periplus/packs/drupal_basic@0.3.0/field_type/field_type_annotation.yaml +46 -0
  130. periplus/packs/drupal_basic@0.3.0/field_type/field_type_attribute.yaml +50 -0
  131. periplus/packs/drupal_basic@0.3.0/field_widget/field_widget_annotation.yaml +46 -0
  132. periplus/packs/drupal_basic@0.3.0/field_widget/field_widget_attribute.yaml +50 -0
  133. periplus/packs/drupal_basic@0.3.0/form/form_id.yaml +80 -0
  134. periplus/packs/drupal_basic@0.3.0/hook/hook.yaml +100 -0
  135. periplus/packs/drupal_basic@0.3.0/hook/hook_attribute.yaml +64 -0
  136. periplus/packs/drupal_basic@0.3.0/hook/hook_theme_include.yaml +34 -0
  137. periplus/packs/drupal_basic@0.3.0/library/library.yaml +12 -0
  138. periplus/packs/drupal_basic@0.3.0/library/library_definition.yaml +58 -0
  139. periplus/packs/drupal_basic@0.3.0/local_action/local_action.yaml +12 -0
  140. periplus/packs/drupal_basic@0.3.0/local_action/local_action_definition.yaml +49 -0
  141. periplus/packs/drupal_basic@0.3.0/mail/mail.yaml +8 -0
  142. periplus/packs/drupal_basic@0.3.0/mail/mail_attribute.yaml +43 -0
  143. periplus/packs/drupal_basic@0.3.0/media_source/media_source.yaml +8 -0
  144. periplus/packs/drupal_basic@0.3.0/media_source/media_source_annotation.yaml +43 -0
  145. periplus/packs/drupal_basic@0.3.0/media_source/media_source_attribute.yaml +79 -0
  146. periplus/packs/drupal_basic@0.3.0/menu_link/menu_link.yaml +18 -0
  147. periplus/packs/drupal_basic@0.3.0/menu_link/menu_link_definition.yaml +48 -0
  148. periplus/packs/drupal_basic@0.3.0/module/module.yaml +7 -0
  149. periplus/packs/drupal_basic@0.3.0/module/module_info.yaml +58 -0
  150. periplus/packs/drupal_basic@0.3.0/pack.yaml +528 -0
  151. periplus/packs/drupal_basic@0.3.0/permission/permission.yaml +7 -0
  152. periplus/packs/drupal_basic@0.3.0/permission/permission_definition.yaml +27 -0
  153. periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker.yaml +9 -0
  154. periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker_annotation.yaml +43 -0
  155. periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker_attribute.yaml +43 -0
  156. periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource.yaml +8 -0
  157. periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource_annotation.yaml +43 -0
  158. periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource_attribute.yaml +43 -0
  159. periplus/packs/drupal_basic@0.3.0/role/role.yaml +14 -0
  160. periplus/packs/drupal_basic@0.3.0/role/role_config.yaml +44 -0
  161. periplus/packs/drupal_basic@0.3.0/route/route.yaml +42 -0
  162. periplus/packs/drupal_basic@0.3.0/route/route_definition.yaml +113 -0
  163. periplus/packs/drupal_basic@0.3.0/route/route_options.yaml +55 -0
  164. periplus/packs/drupal_basic@0.3.0/route/route_requirements.yaml +108 -0
  165. periplus/packs/drupal_basic@0.3.0/service/service.yaml +56 -0
  166. periplus/packs/drupal_basic@0.3.0/service/service_definition.yaml +181 -0
  167. periplus/packs/drupal_basic@0.3.0/template/template.yaml +86 -0
  168. periplus/packs/drupal_basic@0.3.0/template/template_route_link.yaml +30 -0
  169. periplus/packs/drupal_basic@0.3.0/theme/theme.yaml +15 -0
  170. periplus/packs/drupal_basic@0.3.0/theme/theme_info.yaml +61 -0
  171. periplus/packs/drupal_basic@0.3.0/theme_hook/template_suggestion.yaml +29 -0
  172. periplus/packs/drupal_basic@0.3.0/theme_hook/theme_hook.yaml +87 -0
  173. periplus/packs/drupal_basic@0.3.0/theme_hook/theme_hook_render.yaml +20 -0
  174. periplus/packs/drupal_basic@0.3.0/url/url.yaml +88 -0
  175. periplus/packs/drupal_basic@0.3.0/views_plugin/views_field_attribute.yaml +80 -0
  176. periplus/packs/drupal_basic@0.3.0/views_plugin/views_filter_attribute.yaml +80 -0
  177. periplus/packs/drupal_basic@0.3.0/views_plugin/views_plugin.yaml +15 -0
  178. periplus/packs/drupal_basic@0.3.0/views_plugin/views_style_attribute.yaml +80 -0
  179. periplus/packs/drupal_js_basic@0.0.1/attachment/attachment.yaml +12 -0
  180. periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_php.yaml +15 -0
  181. periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_php_array.yaml +52 -0
  182. periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_twig.yaml +26 -0
  183. periplus/packs/drupal_js_basic@0.0.1/attachment/php_function_holder.yaml +13 -0
  184. periplus/packs/drupal_js_basic@0.0.1/behavior/behavior.yaml +198 -0
  185. periplus/packs/drupal_js_basic@0.0.1/pack.yaml +153 -0
  186. periplus/packs/drupal_js_basic@0.0.1/setting/setting.yaml +13 -0
  187. periplus/packs/drupal_js_basic@0.0.1/setting/setting_in_js.yaml +15 -0
  188. periplus/packs/drupal_js_basic@0.0.1/setting/setting_in_php.yaml +92 -0
  189. periplus/packs/drush_basic@0.0.1/command/command.yaml +15 -0
  190. periplus/packs/drush_basic@0.0.1/command/command_annotation.yaml +60 -0
  191. periplus/packs/drush_basic@0.0.1/command/command_attribute.yaml +88 -0
  192. periplus/packs/drush_basic@0.0.1/pack.yaml +29 -0
  193. periplus/packs/eck_basic@0.0.1/bundle/eck_bundle_config.yaml +56 -0
  194. periplus/packs/eck_basic@0.0.1/entity_type/eck_entity_type_config.yaml +26 -0
  195. periplus/packs/eck_basic@0.0.1/pack.yaml +28 -0
  196. periplus/packs/go@0.0.2/interfaces/satisfies.yaml +47 -0
  197. periplus/packs/go@0.0.2/pack.yaml +51 -0
  198. periplus/packs/go@0.0.2/types/find_declarations.yaml +137 -0
  199. periplus/packs/go@0.0.2/types/node_types.yaml +36 -0
  200. periplus/packs/go_basic@0.0.3/pack.yaml +104 -0
  201. periplus/packs/go_basic@0.0.3/rules/calls.yaml +25 -0
  202. periplus/packs/go_basic@0.0.3/rules/declarations.yaml +40 -0
  203. periplus/packs/go_basic@0.0.3/rules/imports.yaml +16 -0
  204. periplus/packs/go_basic@0.0.3/types/node_types.yaml +15 -0
  205. periplus/packs/js_basic@0.0.1/function/function.yaml +68 -0
  206. periplus/packs/js_basic@0.0.1/pack.yaml +50 -0
  207. periplus/packs/laravel_basic@0.0.5/pack.yaml +313 -0
  208. periplus/packs/laravel_basic@0.0.5/rules/classes.yaml +42 -0
  209. periplus/packs/laravel_basic@0.0.5/rules/config.yaml +229 -0
  210. periplus/packs/laravel_basic@0.0.5/rules/routes.yaml +210 -0
  211. periplus/packs/laravel_basic@0.0.5/rules/types.yaml +50 -0
  212. periplus/packs/laravel_basic@0.0.5/rules/views.yaml +100 -0
  213. periplus/packs/paragraphs_basic@0.0.1/bundle/paragraph_bundle_config.yaml +54 -0
  214. periplus/packs/paragraphs_basic@0.0.1/pack.yaml +12 -0
  215. periplus/packs/php@0.1.0/edges/edge_kinds.yaml +64 -0
  216. periplus/packs/php@0.1.0/edges/find_calls.yaml +34 -0
  217. periplus/packs/php@0.1.0/edges/find_imports.yaml +18 -0
  218. periplus/packs/php@0.1.0/edges/find_member_access.yaml +22 -0
  219. periplus/packs/php@0.1.0/edges/find_structure.yaml +38 -0
  220. periplus/packs/php@0.1.0/embedded/embedded_regions.yaml +19 -0
  221. periplus/packs/php@0.1.0/imports/imports.yaml +20 -0
  222. periplus/packs/php@0.1.0/pack.yaml +117 -0
  223. periplus/packs/php@0.1.0/resolution/excluded_tokens.yaml +23 -0
  224. periplus/packs/php@0.1.0/resolution/resolution.yaml +35 -0
  225. periplus/packs/php@0.1.0/types/find_declarations.yaml +102 -0
  226. periplus/packs/php@0.1.0/types/node_types.yaml +55 -0
  227. periplus/packs/php_basic@0.2.0/edges/edge_kinds.yaml +48 -0
  228. periplus/packs/php_basic@0.2.0/edges/find_structure.yaml +750 -0
  229. periplus/packs/php_basic@0.2.0/imports/imports.yaml +19 -0
  230. periplus/packs/php_basic@0.2.0/pack.yaml +94 -0
  231. periplus/packs/php_basic@0.2.0/types/find_declarations.yaml +267 -0
  232. periplus/packs/php_basic@0.2.0/types/node_types.yaml +55 -0
  233. periplus/packs/profile_basic@0.0.1/bundle/profile_bundle_config.yaml +54 -0
  234. periplus/packs/profile_basic@0.0.1/pack.yaml +12 -0
  235. periplus/packs/salesforce_basic@0.0.1/event/salesforce_event.yaml +36 -0
  236. periplus/packs/salesforce_basic@0.0.1/pack.yaml +31 -0
  237. periplus/packs/salesforce_basic@0.0.1/salesforce_mapping/salesforce_mapping.yaml +19 -0
  238. periplus/packs/salesforce_basic@0.0.1/salesforce_mapping/salesforce_mapping_config.yaml +83 -0
  239. periplus/packs/salesforce_basic@0.0.1/salesforce_mapping_field/salesforce_mapping_field.yaml +8 -0
  240. periplus/packs/salesforce_basic@0.0.1/salesforce_mapping_field/salesforce_mapping_field_annotation.yaml +43 -0
  241. periplus/packs/twig@0.0.1/pack.yaml +26 -0
  242. periplus/packs/twig@0.0.1/types/template.yaml +57 -0
  243. periplus/packs/twig_basic@0.1.0/pack.yaml +11 -0
  244. periplus/packs/twig_tweak_basic@0.0.1/pack.yaml +27 -0
  245. periplus/packs/twig_tweak_basic@0.0.1/rules/block.yaml +20 -0
  246. periplus/packs/twig_tweak_basic@0.0.1/rules/entity.yaml +35 -0
  247. periplus/packs/twig_tweak_basic@0.0.1/rules/field.yaml +21 -0
  248. periplus/packs/twig_tweak_basic@0.0.1/rules/menu.yaml +21 -0
  249. periplus/packs/webform_basic@0.0.1/bundle/webform_submission_bundle_config.yaml +54 -0
  250. periplus/packs/webform_basic@0.0.1/pack.yaml +20 -0
  251. periplus/packs/webform_basic@0.0.1/webform_handler/webform_handler.yaml +8 -0
  252. periplus/packs/webform_basic@0.0.1/webform_handler/webform_handler_annotation.yaml +43 -0
  253. periplus/packs/yaml@0.0.1/pack.yaml +72 -0
  254. periplus/packs/yaml@0.0.1/types/find_mapping_entry.yaml +21 -0
  255. periplus/packs/yaml@0.0.1/types/yaml_types.yaml +44 -0
  256. periplus/packs/yaml_basic@0.1.0/pack.yaml +10 -0
  257. periplus/packs.py +447 -0
  258. periplus/preflight.py +102 -0
  259. periplus/py.typed +0 -0
  260. periplus/report.py +596 -0
  261. periplus/resolution.py +355 -0
  262. periplus/settings.py +617 -0
  263. periplus/settings.stub.yml +99 -0
  264. periplus/spec.py +774 -0
  265. periplus/update.py +197 -0
  266. periplus/validate.py +655 -0
  267. periplus_map-0.2.1.dist-info/METADATA +566 -0
  268. periplus_map-0.2.1.dist-info/RECORD +272 -0
  269. periplus_map-0.2.1.dist-info/WHEEL +4 -0
  270. periplus_map-0.2.1.dist-info/entry_points.txt +2 -0
  271. periplus_map-0.2.1.dist-info/licenses/LICENSE +202 -0
  272. periplus_map-0.2.1.dist-info/licenses/NOTICE +2 -0
@@ -0,0 +1,368 @@
1
+ """Step four: merge nodes and edges, then build the map document and its canonical text."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections import Counter
7
+ from collections.abc import Mapping, Sequence
8
+ from dataclasses import replace
9
+ from typing import Any
10
+
11
+ from periplus.engine.packload import BoundaryEntry, EdgeKind, TypeInfo
12
+ from periplus.engine.rules import FoundEdge, FoundNode, Skipped
13
+ from periplus.errors import ExitCode, HarnessError
14
+ from periplus.manifest import LoadedPack
15
+
16
+ __all__ = ["EdgeIllegal", "TypeCollision", "build_document", "render", "type_chain"]
17
+
18
+
19
+ class TypeCollision(HarnessError):
20
+ """Rules found types for one id that do not lie on one ancestor chain."""
21
+
22
+ code = ExitCode.TYPE_COLLISION
23
+
24
+
25
+ class EdgeIllegal(HarnessError):
26
+ """An edge's end has a type its kind does not allow there."""
27
+
28
+ code = ExitCode.EDGE_ILLEGAL
29
+
30
+
31
+ def build_document(
32
+ nodes: Sequence[FoundNode],
33
+ edges: Sequence[FoundEdge],
34
+ types: Mapping[str, TypeInfo],
35
+ edge_kinds: Mapping[str, EdgeKind],
36
+ packs: Sequence[LoadedPack],
37
+ tool_version: str,
38
+ settings: str,
39
+ boundary: Mapping[str, BoundaryEntry] | None = None,
40
+ skipped: list[Skipped] | None = None,
41
+ ) -> dict[str, Any]:
42
+ """The map document.
43
+
44
+ Nodes merge on id and the most specific type found wins. An attribute whose contributions
45
+ differ is written as ``{"differs": [...]}``. Edges merge on kind, from and to, and an end no
46
+ rule found becomes a referenced node, of the deepest type its edges claim, an edge's named
47
+ ``target_type`` among them; an end written ``{declared: <type>}`` claims that type alone. An
48
+ end no rule found that an edge declared ``on_miss`` for is
49
+ unresolved instead, with the first such edge's rule, by pack and rule, in its detail, and the
50
+ types that edge's end tried, in order. An end that lists none tried its one declared type,
51
+ else its kind's types at that end. An end with candidate ids lands on the first a rule
52
+ found, else on the first. An end no rule found whose id ``boundary`` lists is instead a
53
+ declared node, its one provenance the group's pack, of the deepest type the group's type and
54
+ its edges' claimed types that share a chain with it agree on, as for a referenced end; an edge
55
+ whose kind has no type on a chain with the group's type at that end raises ``EdgeIllegal``.
56
+ Then each mapped node whose ancestry reaches such a node with a classification is classified,
57
+ as ``_classify`` says, and its rows join ``skipped``. Locations and provenance de-duplicate and
58
+ sort.
59
+ """
60
+ groups: dict[str, list[FoundNode]] = {}
61
+ for found in nodes:
62
+ groups.setdefault(found.id, []).append(found)
63
+ merged = {node_id: _merge_node(group, types) for node_id, group in groups.items()}
64
+ edges = [_landed(edge, merged) for edge in edges]
65
+
66
+ merged_edges: dict[tuple[str, str, str], list[FoundEdge]] = {}
67
+ for edge in edges:
68
+ merged_edges.setdefault((edge.kind, edge.source, edge.target), []).append(edge)
69
+ referring: dict[str, set[tuple[str, str, str]]] = {}
70
+ claimed: dict[str, set[str]] = {}
71
+ for kind, source, target in sorted(merged_edges):
72
+ allowed = edge_kinds[kind]
73
+ group = merged_edges[(kind, source, target)]
74
+ named = tuple(e.target_type for e in group if e.target_type)
75
+ starts = tuple(e.source_type for e in group if e.source_type)
76
+ # An end with candidate types, or written {declared: <type>}, takes the type it names, not
77
+ # the kind's.
78
+ listed = any(e.candidates for e in group)
79
+ alone = {side for e in group for side in e.declared_ends}
80
+ for at, end, ends, kind_types in (
81
+ (
82
+ "from",
83
+ source,
84
+ starts if (listed or "source" in alone) and starts else allowed.from_types + starts,
85
+ allowed.from_types,
86
+ ),
87
+ (
88
+ "to",
89
+ target,
90
+ named if (listed or "target" in alone) and named else allowed.to_types + named,
91
+ allowed.to_types,
92
+ ),
93
+ ):
94
+ if end not in merged and boundary and end in boundary:
95
+ merged[end] = _declared(end, boundary[end])
96
+ if end not in merged:
97
+ merged[end] = {"id": end, "type": "", "state": "referenced", "locations": []}
98
+ if boundary and end in boundary and merged[end]["state"] == "declared":
99
+ entry = boundary[end]
100
+ chain = type_chain(types, entry.type)
101
+ fits = {
102
+ t
103
+ for t in {*kind_types, *ends}
104
+ if t in chain or entry.type in type_chain(types, t)
105
+ }
106
+ if not fits & set(kind_types):
107
+ raise EdgeIllegal(
108
+ f"the edge {kind} from {source} to {target} needs its {at} end to be one "
109
+ f"of {', '.join(kind_types)}, and the boundary group {entry.group} of "
110
+ f"{entry.pack} lists {end} as {entry.type}",
111
+ {"kind": kind, "from": source, "to": target, "group": entry.group},
112
+ )
113
+ claimed.setdefault(end, {entry.type}).update(fits & set(ends))
114
+ if merged[end]["state"] == "referenced":
115
+ claimed.setdefault(end, set()).update(ends)
116
+ referring.setdefault(end, set()).update(
117
+ (e.pack, e.rule, e.confidence) for e in merged_edges[(kind, source, target)]
118
+ )
119
+ for end, names in claimed.items():
120
+ merged[end]["type"] = _referenced_type(names, types)
121
+ for end, rules in referring.items():
122
+ merged[end]["provenance"] = [
123
+ {"pack": pack, "rule": rule, "confidence": confidence, "sets": ["id"]}
124
+ for pack, rule, confidence in sorted(rules)
125
+ ]
126
+ if boundary:
127
+ rows = _classify(merged, merged_edges, boundary, types)
128
+ if skipped is not None:
129
+ skipped.extend(rows)
130
+ missed = sorted(
131
+ (
132
+ (edge.source if edge.on_miss == "source" else edge.target, edge.pack, edge.rule, edge)
133
+ for edge in edges
134
+ if edge.on_miss
135
+ ),
136
+ key=lambda item: (*item[:3], item[3].searched_for),
137
+ )
138
+ for end, pack, rule, edge in missed:
139
+ if merged[end]["state"] == "referenced":
140
+ allowed = edge_kinds[edge.kind]
141
+ if edge.on_miss == "source":
142
+ declared, kind_types = edge.source_type, allowed.from_types
143
+ else:
144
+ declared, kind_types = edge.target_type, allowed.to_types
145
+ merged[end]["state"] = "unresolved"
146
+ merged[end]["unresolved_detail"] = {
147
+ "rule": f"{pack}/{rule}",
148
+ "searched_for": edge.searched_for,
149
+ "found": "no mapped node of this name",
150
+ "types_tried": list(edge.types_tried or ((declared,) if declared else kind_types)),
151
+ }
152
+ ordered = [merged[node_id] for node_id in sorted(merged)]
153
+ written = [
154
+ _edge(key, group, merged, edge_kinds, types) for key, group in sorted(merged_edges.items())
155
+ ]
156
+ return {
157
+ "periplus_map_version": 0,
158
+ "generated_by": f"periplus {tool_version}",
159
+ "packs": sorted(
160
+ ({"pack": p.name.pack, "version": p.name.version} for p in packs),
161
+ key=lambda entry: entry["pack"],
162
+ ),
163
+ "settings": settings,
164
+ "roots": ["."],
165
+ "type_hierarchy": _hierarchy({str(node["type"]) for node in ordered}, types),
166
+ "counts": {
167
+ "nodes": len(ordered),
168
+ "edges": len(written),
169
+ "edges_by_kind": dict(sorted(Counter(str(edge["kind"]) for edge in written).items())),
170
+ "nodes_by_state": dict(sorted(Counter(str(n["state"]) for n in ordered).items())),
171
+ "nodes_by_type": dict(sorted(Counter(str(node["type"]) for node in ordered).items())),
172
+ },
173
+ "nodes": ordered,
174
+ "edges": written,
175
+ }
176
+
177
+
178
+ def _landed(edge: FoundEdge, merged: Mapping[str, Any]) -> FoundEdge:
179
+ """The edge with its composed end on the first candidate id a rule found, else the first."""
180
+ if not edge.candidates:
181
+ return edge
182
+ chosen = next((c for c in edge.candidates if c in merged), edge.candidates[0])
183
+ # A composed end that is the from end names its type in source_type.
184
+ return replace(edge, source=chosen) if edge.source_type else replace(edge, target=chosen)
185
+
186
+
187
+ def _declared(node_id: str, entry: BoundaryEntry) -> dict[str, Any]:
188
+ """The node a boundary group draws for a name an edge reached."""
189
+ rule = f"boundary.{entry.group}"
190
+ return {
191
+ "id": node_id,
192
+ "type": entry.type,
193
+ "state": "declared",
194
+ "locations": [],
195
+ "provenance": [
196
+ {"pack": entry.pack, "rule": rule, "confidence": "declared", "sets": ["id", "type"]}
197
+ ],
198
+ }
199
+
200
+
201
+ def _classify(
202
+ merged: dict[str, dict[str, Any]],
203
+ merged_edges: Mapping[tuple[str, str, str], Sequence[FoundEdge]],
204
+ boundary: Mapping[str, BoundaryEntry],
205
+ types: Mapping[str, TypeInfo],
206
+ ) -> list[Skipped]:
207
+ """Classify each mapped node whose ancestry reaches a declared boundary node that classifies.
208
+
209
+ From each such boundary node the walk follows its entry's ancestry edges backwards, through
210
+ mapped nodes only. A node reached takes the deepest classification that reaches it when its
211
+ type is on that one's chain, and keeps a type equal to or below it; either way it gains an
212
+ inferred provenance row per listing group. A node whose type, or whose classifications, do not
213
+ lie on one chain keeps its type and gives one skipped row.
214
+ """
215
+ sources: dict[tuple[str, str], set[str]] = {}
216
+ for kind, source, target in merged_edges:
217
+ sources.setdefault((kind, target), set()).add(source)
218
+ reached: dict[str, set[str]] = {}
219
+ for start in sorted(boundary):
220
+ entry = boundary[start]
221
+ if not entry.classifies or start not in merged or merged[start]["state"] != "declared":
222
+ continue
223
+ seen, pending = {start}, [start]
224
+ while pending:
225
+ current = pending.pop(0)
226
+ below = {s for kind in entry.ancestry for s in sources.get((kind, current), ())}
227
+ for node_id in sorted(below):
228
+ if node_id not in seen and merged[node_id]["state"] == "mapped":
229
+ seen.add(node_id)
230
+ reached.setdefault(node_id, set()).add(start)
231
+ pending.append(node_id)
232
+ rows: list[Skipped] = []
233
+ for node_id in sorted(reached):
234
+ node, starts = merged[node_id], sorted(reached[node_id])
235
+ deepest = max(starts, key=lambda s: len(type_chain(types, str(boundary[s].classifies))))
236
+ target = str(boundary[deepest].classifies)
237
+ chain = type_chain(types, target)
238
+ off = [s for s in starts if boundary[s].classifies not in chain]
239
+ if off or node["type"] not in chain and target not in type_chain(types, node["type"]):
240
+ start = off[0] if off else deepest
241
+ reason = (
242
+ f"{node_id}: classification {boundary[start].classifies} from {start} "
243
+ f"does not descend from {target if off else node['type']}"
244
+ )
245
+ place = node["locations"][0]
246
+ rows.append((place["file"], place["line"], f"boundary.{boundary[start].group}", reason))
247
+ continue
248
+ if node["type"] in chain:
249
+ node["type"] = target
250
+ node["provenance"] += [
251
+ {"pack": pack, "rule": f"boundary.{group}", "confidence": "inferred", "sets": ["type"]}
252
+ for pack, group in sorted({(boundary[s].pack, boundary[s].group) for s in starts})
253
+ ]
254
+ return rows
255
+
256
+
257
+ def type_chain(types: Mapping[str, TypeInfo], name: str) -> list[str]:
258
+ """The type and its ancestors, nearest first."""
259
+ chain: list[str] = []
260
+ while name in types and name not in chain:
261
+ chain.append(name)
262
+ parent = types[name].parent
263
+ if parent is None:
264
+ break
265
+ name = parent
266
+ return chain or [name]
267
+
268
+
269
+ def _referenced_type(names: set[str], types: Mapping[str, TypeInfo]) -> str:
270
+ """The deepest of the types the edges claim when one chain holds them all, else the first."""
271
+ deepest = max(sorted(names), key=lambda name: len(type_chain(types, name)))
272
+ if all(name in type_chain(types, deepest) for name in names):
273
+ return deepest
274
+ return min(names)
275
+
276
+
277
+ def _merge_node(group: Sequence[FoundNode], types: Mapping[str, TypeInfo]) -> dict[str, Any]:
278
+ found = sorted({node.type for node in group})
279
+ deepest = max(found, key=lambda name: len(type_chain(types, name)))
280
+ if any(name not in type_chain(types, deepest) for name in found):
281
+ raise TypeCollision(
282
+ f"{group[0].id} was found as {', '.join(found)}, which do not lie on one type chain",
283
+ {"id": group[0].id, "types": ", ".join(found)},
284
+ )
285
+ values: dict[str, dict[tuple[str, int], Any]] = {}
286
+ named: dict[tuple[str, str, str], set[str]] = {}
287
+ for node in group:
288
+ sets = named.setdefault((node.pack, node.rule, node.confidence), set())
289
+ for name, value, line in node.attributes:
290
+ values.setdefault(name, {})[(node.file, line)] = value
291
+ sets.add(name)
292
+ result: dict[str, Any] = {
293
+ "id": group[0].id,
294
+ "type": deepest,
295
+ "state": "mapped",
296
+ "locations": [
297
+ {"file": f, "line": n} for f, n in sorted({(node.file, node.line) for node in group})
298
+ ],
299
+ "provenance": [
300
+ {"pack": p, "rule": r, "confidence": c, "sets": ["id", "type", *sorted(names)]}
301
+ for (p, r, c), names in sorted(named.items())
302
+ ],
303
+ }
304
+ attributes = {name: _attribute(contributions) for name, contributions in values.items()}
305
+ if attributes:
306
+ result["attributes"] = dict(sorted(attributes.items()))
307
+ return result
308
+
309
+
310
+ def _attribute(contributions: Mapping[tuple[str, int], Any]) -> Any:
311
+ """The value when every contribution agrees; otherwise every contribution, none winning."""
312
+ ordered = sorted(contributions.items(), key=lambda item: item[0])
313
+ if len({json.dumps(value, sort_keys=True) for _, value in ordered}) == 1:
314
+ return ordered[0][1]
315
+ return {"differs": [{"file": f, "line": n, "value": value} for (f, n), value in ordered]}
316
+
317
+
318
+ def _edge(
319
+ key: tuple[str, str, str],
320
+ group: Sequence[FoundEdge],
321
+ merged: Mapping[str, Mapping[str, Any]],
322
+ edge_kinds: Mapping[str, EdgeKind],
323
+ types: Mapping[str, TypeInfo],
324
+ ) -> dict[str, Any]:
325
+ kind, source, target = key
326
+ for end, allowed in (
327
+ (source, edge_kinds[kind].from_types),
328
+ (target, edge_kinds[kind].to_types),
329
+ ):
330
+ if merged[end]["state"] != "mapped":
331
+ continue
332
+ if not set(allowed) & set(type_chain(types, merged[end]["type"])):
333
+ raise EdgeIllegal(
334
+ f"the edge {kind} from {source} to {target} needs {end} to be one of "
335
+ f"{', '.join(allowed)}, and it is {merged[end]['type']}",
336
+ {"kind": kind, "from": source, "to": target},
337
+ )
338
+ places = sorted({(edge.file, edge.line) for edge in group})
339
+ return {
340
+ "kind": kind,
341
+ "from": source,
342
+ "to": target,
343
+ "occurrences": len(places),
344
+ "locations": [{"file": f, "line": n} for f, n in places],
345
+ "provenance": [
346
+ {"pack": p, "rule": r, "confidence": c}
347
+ for p, r, c in sorted({(e.pack, e.rule, e.confidence) for e in group})
348
+ ],
349
+ }
350
+
351
+
352
+ def _hierarchy(used: set[str], types: Mapping[str, TypeInfo]) -> list[dict[str, str | None]]:
353
+ """The used types and every ancestor of them, each with its parent, sorted by type."""
354
+ closure: set[str] = set()
355
+ pending = sorted(used)
356
+ while pending:
357
+ name = pending.pop()
358
+ if name not in closure:
359
+ closure.add(name)
360
+ parent = types[name].parent
361
+ if parent is not None:
362
+ pending.append(parent)
363
+ return [{"type": name, "parent": types[name].parent} for name in sorted(closure)]
364
+
365
+
366
+ def render(document: Mapping[str, object]) -> str:
367
+ """The canonical text: sorted keys, two-space indent, ASCII, one trailing LF."""
368
+ return json.dumps(document, sort_keys=True, indent=2, ensure_ascii=True) + "\n"