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
periplus/cli.py ADDED
@@ -0,0 +1,658 @@
1
+ """The console script: argument parsing and wiring, and nothing else.
2
+
3
+ Every unit of logic lives in the package. This module reads ``sys.argv``, calls one entry point,
4
+ and turns what comes back into a process status. A branch here that is not parsing or wiring
5
+ belongs in a module beside this one.
6
+
7
+ Three modules are imported at module level and no others: ``errors``, ``preflight``, and the
8
+ standard library. Everything else is imported inside ``main()``, after the dependency check has
9
+ run. That ordering is the whole of "a missing dependency fails at startup, named" — a module-level
10
+ ``import periplus.resolution`` would pull ``ruamel.yaml`` onto this file's import path and the run
11
+ would die of an ``ImportError`` before the check that exists to name the problem ever executed.
12
+
13
+ ``periplus.report`` is imported inside ``main()`` for the same reason, and that is also why
14
+ ``--format``'s choices are written here as a literal rather than read off ``report.RENDERERS``. A
15
+ test asserts the two agree.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import io
22
+ import os
23
+ import sys
24
+ from collections.abc import Sequence
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ import periplus
29
+ from periplus.errors import ExitCode
30
+ from periplus.preflight import check_runtime_dependencies
31
+
32
+ __all__ = ["build_parser", "main"]
33
+
34
+ #: argparse gained ``color`` in 3.14 and defaults it to ``True``, consulting ``os.isatty()`` — so
35
+ #: the same source writes ANSI escapes into a usage line on 3.14 and plain text on 3.11. This tool
36
+ #: declares 3.11 through 3.14 and promises output that does not depend on where it ran,
37
+ #: and `_pin_the_output_stream` below already pays for the same property on stdout.
38
+ #:
39
+ #: A mapping rather than a branch inside ``build_parser``, because on 3.11 through 3.13 the keyword
40
+ #: is a ``TypeError`` and the parser has to be built without it — measured on both interpreters.
41
+ #:
42
+ #: ``dict[str, Any]`` and not ``dict[str, bool]``: a ``**mapping`` splat is checked against every
43
+ #: parameter of the callee, so the narrower type is four ``arg-type`` errors rather than none —
44
+ #: measured — against ``prog``, ``usage``, ``parents`` and ``formatter_class``, only two of which
45
+ #: are strings. What that gives up is that a misspelled key typechecks — inert below 3.14, a
46
+ #: ``TypeError`` on every invocation above it. That trade is recorded here and asserted nowhere:
47
+ #: no test reads the mapping's contents.
48
+ #: The parser is built from configuration the way ``MERGE_RULES`` is a table rather than a chain of
49
+ #: conditions, and ``build_parser`` stays a sequence of ``add_argument`` calls.
50
+ #:
51
+ #: One keyword on the top parser covers the subparsers too: ``add_subparsers`` assigns
52
+ #: ``action._color = self.color``, overriding the ``True`` that ``_SubParsersAction.__init__``
53
+ #: hardcodes. Verified by running the coloured and uncoloured cases in one command.
54
+ _COLOUR: dict[str, Any] = {"color": False} if sys.version_info >= (3, 14) else {}
55
+
56
+
57
+ def build_parser() -> argparse.ArgumentParser:
58
+ """Return the argument parser.
59
+
60
+ ``--version`` is a plain flag rather than ``action="version"`` on purpose: argparse's version
61
+ action calls ``sys.exit`` from inside the parser, and the entrypoint contract says ``main``
62
+ returns an integer and never exits the process. A test that called a parser which exits could
63
+ only assert on ``SystemExit``, which is a worse test of the same thing.
64
+
65
+ The subparsers are added without ``required=True``, and that is forced rather than chosen.
66
+ argparse checks a required subcommand *after* it has consumed the options, so
67
+ ``periplus --version`` on a required-subparser parser is a usage error — which would break
68
+ the two tests that pin ``main(["--version"]) == 0``. ``main`` raises the
69
+ usage error itself, through ``parser.error``, which is argparse's own message, argparse's own
70
+ stream and argparse's own status; what it is not is argparse's own *decision*.
71
+ """
72
+ parser = argparse.ArgumentParser(
73
+ prog="periplus",
74
+ description="A schema-first, deterministic codebase mapper, configurable to any stack.",
75
+ **_COLOUR,
76
+ )
77
+ parser.add_argument(
78
+ "--version",
79
+ action="store_true",
80
+ help="print the installed version and exit",
81
+ )
82
+ # No hand-written ``metavar``. It read ``{resolve}`` and existed only because a one-member
83
+ # list read badly; with three members argparse's own default renders ``{init,spec,status}``.
84
+ # Deleting it is the twelfth rename site, and the one a search for the quoted literal misses
85
+ # because the name sat inside a longer string.
86
+ subcommands = parser.add_subparsers(dest="command")
87
+ subcommands.add_parser(
88
+ "init",
89
+ help="create this project's .periplus directory and a stub settings file",
90
+ description="Create the project directory, its pack directory and a stub settings file.",
91
+ )
92
+ spec = subcommands.add_parser(
93
+ "spec",
94
+ help="print the pack contract: the schemas, the rule format, the engine, and what is open",
95
+ description="Publish the contract a pack is authored against, and stop.",
96
+ )
97
+ spec.add_argument(
98
+ "pack",
99
+ nargs="?",
100
+ default=None,
101
+ metavar="PACK",
102
+ help="a pack name, to add what its manifest declares and what it inherits",
103
+ )
104
+ spec.add_argument(
105
+ "--format",
106
+ # The same two names again, written out for the same reason the block below gives:
107
+ # `periplus.spec` imports `report`, which reaches `ruamel.yaml`, so reading
108
+ # `spec.RENDERERS` here would put a runtime distribution on this module's import path. A
109
+ # test asserts these two names equal `tuple(spec.RENDERERS)`.
110
+ choices=("text", "json"),
111
+ default="text",
112
+ help="how to print the contract (default: text)",
113
+ )
114
+ spec.add_argument(
115
+ "--summary",
116
+ action="store_true",
117
+ help="print the summary block only, without the documents' contents",
118
+ )
119
+ check = subcommands.add_parser(
120
+ "validate",
121
+ help="check one pack's files against the shipped schemas and name what disagrees",
122
+ description="Check a pack against the schemas it is written against, and stop.",
123
+ )
124
+ # Required, unlike `spec`'s. A bare `periplus validate` would check the file `init` writes,
125
+ # and a test that loads that file asserts that property rather than a second command surface.
126
+ check.add_argument(
127
+ "pack",
128
+ metavar="PACK",
129
+ help="the pack to check: NAME, or NAME@VERSION to name one of several copies",
130
+ )
131
+ check.add_argument(
132
+ "--format",
133
+ # The same two names as a literal, for the third time and for the reason the two blocks
134
+ # around it give: `periplus.validate` imports `jsonschema` and reaches `ruamel.yaml`
135
+ # through `resolution`, so reading `validate.RENDERERS` here would put two runtime
136
+ # distributions on this module's import path. A test asserts these two names equal
137
+ # `tuple(validate.RENDERERS)`.
138
+ choices=("text", "json"),
139
+ default="text",
140
+ help="how to print the findings (default: text)",
141
+ )
142
+ refresh = subcommands.add_parser(
143
+ "update",
144
+ help="move this project's pins to the rutter versions that are installed",
145
+ description="Rewrite each pin whose rutter is installed at another version, and stop.",
146
+ )
147
+ refresh.add_argument(
148
+ "--format",
149
+ # A literal for the reason `validate`'s is: `periplus.update` reaches `ruamel.yaml`.
150
+ choices=("text", "json"),
151
+ default="text",
152
+ help="how to print the report (default: text)",
153
+ )
154
+ extract = subcommands.add_parser(
155
+ "map",
156
+ help="extract the map of this project and write it",
157
+ description="Run the loaded packs' rules over the project, write the map, and stop.",
158
+ )
159
+ extract.add_argument(
160
+ "--format",
161
+ # The same two names as a literal, for the reason the blocks above give: `periplus.map`
162
+ # imports `jsonschema`. A test asserts these two names equal `tuple(map.RENDERERS)`.
163
+ choices=("text", "json"),
164
+ default="text",
165
+ help="how to print the report (default: text)",
166
+ )
167
+ extract.add_argument(
168
+ "--settings",
169
+ type=Path,
170
+ default=None,
171
+ metavar="PATH",
172
+ help="the project settings file to read, instead of the one the project root implies",
173
+ )
174
+ extract.add_argument(
175
+ "--output",
176
+ type=Path,
177
+ default=None,
178
+ metavar="PATH",
179
+ help="where to write the map (default: .periplus/map.json under the project root)",
180
+ )
181
+ types = subcommands.add_parser(
182
+ "types",
183
+ help="list the node types and edge kinds in the written map",
184
+ description="Count the node types and edge kinds the map holds, and stop.",
185
+ )
186
+ types.add_argument(
187
+ "--format",
188
+ # A literal for the reason `map`'s is: `periplus.navigate` imports `jsonschema`. A test
189
+ # asserts these names equal `tuple(types.RENDERERS)`.
190
+ choices=("text", "json"),
191
+ default="text",
192
+ help="how to print the report (default: text)",
193
+ )
194
+ types.add_argument(
195
+ "--map",
196
+ type=Path,
197
+ default=None,
198
+ metavar="PATH",
199
+ help="the map file to read (default: .periplus/map.json under the project root)",
200
+ )
201
+ check = subcommands.add_parser(
202
+ "check-map",
203
+ help="check the written map against the map schema",
204
+ description="Check every node and edge of the map against the map schema, and stop.",
205
+ )
206
+ check.add_argument(
207
+ "--format",
208
+ # A literal for the reason `map`'s is: `periplus.checkmap` imports `jsonschema`. A test
209
+ # asserts these names equal `tuple(checkmap.RENDERERS)`.
210
+ choices=("text", "json"),
211
+ default="text",
212
+ help="how to print the report (default: text)",
213
+ )
214
+ check.add_argument(
215
+ "--map",
216
+ type=Path,
217
+ default=None,
218
+ metavar="PATH",
219
+ help="the map file to check (default: .periplus/map.json under the project root)",
220
+ )
221
+ find = subcommands.add_parser(
222
+ "find",
223
+ help="find the nodes whose id holds a text",
224
+ description="List the nodes whose id holds TEXT, closest match first, and stop.",
225
+ )
226
+ find.add_argument("text", metavar="TEXT", help="the text to look for in node ids")
227
+ find.add_argument("--type", default=None, metavar="T", help="keep type T and its subtypes")
228
+ find.add_argument(
229
+ "--limit", type=int, default=50, metavar="N", help="rows to show; 0 shows all (default: 50)"
230
+ )
231
+ find.add_argument(
232
+ "--map",
233
+ type=Path,
234
+ default=None,
235
+ metavar="PATH",
236
+ help="the map file to read (default: .periplus/map.json under the project root)",
237
+ )
238
+ find.add_argument(
239
+ "--format",
240
+ # A literal for the reason `types`' is; a test asserts it equals `tuple(find.RENDERERS)`.
241
+ choices=("text", "json"),
242
+ default="text",
243
+ help="how to print the report (default: text)",
244
+ )
245
+ show = subcommands.add_parser(
246
+ "show",
247
+ help="show one node and the edges at it",
248
+ description="Show the node ID and every edge leaving or arriving at it, and stop.",
249
+ )
250
+ show.add_argument("id", metavar="ID", help="the node id, as periplus find prints it")
251
+ show.add_argument(
252
+ "--limit", type=int, default=50, metavar="N", help="rows to show; 0 shows all (default: 50)"
253
+ )
254
+ show.add_argument(
255
+ "--map",
256
+ type=Path,
257
+ default=None,
258
+ metavar="PATH",
259
+ help="the map file to read (default: .periplus/map.json under the project root)",
260
+ )
261
+ show.add_argument(
262
+ "--format",
263
+ # A literal for the reason `types`' is; a test asserts it equals `tuple(show.RENDERERS)`.
264
+ choices=("text", "json"),
265
+ default="text",
266
+ help="how to print the report (default: text)",
267
+ )
268
+ for name, helptext in (
269
+ ("neighbors", "list the items an item's edges lead to"),
270
+ ("affected", "list the items whose edges lead to an item"),
271
+ ):
272
+ walking = subcommands.add_parser(name, help=helptext, description=f"{helptext}, and stop.")
273
+ walking.add_argument("id", metavar="ID", help="the node id, as periplus find prints it")
274
+ walking.add_argument(
275
+ "--depth",
276
+ type=int,
277
+ default=1,
278
+ metavar="N",
279
+ help="edges to follow, 1 or more (default: 1)",
280
+ )
281
+ walking.add_argument(
282
+ "--kind",
283
+ action="append",
284
+ default=[],
285
+ metavar="K",
286
+ help="keep only edges of kind K; repeat for several",
287
+ )
288
+ walking.add_argument(
289
+ "--limit",
290
+ type=int,
291
+ default=50,
292
+ metavar="N",
293
+ help="rows to show; 0 shows all (default: 50)",
294
+ )
295
+ walking.add_argument(
296
+ "--map",
297
+ type=Path,
298
+ default=None,
299
+ metavar="PATH",
300
+ help="the map file to read (default: .periplus/map.json under the project root)",
301
+ )
302
+ walking.add_argument(
303
+ "--format",
304
+ # A literal for the reason `types`' is; a test asserts it equals the RENDERERS.
305
+ choices=("text", "json"),
306
+ default="text",
307
+ help="how to print the report (default: text)",
308
+ )
309
+ path = subcommands.add_parser(
310
+ "path",
311
+ help="show every shortest chain of edges from one item to another",
312
+ description="Show the shortest directed chains of edges from FROM to TO, and stop.",
313
+ )
314
+ path.add_argument("source", metavar="FROM", help="the node id the chains start at")
315
+ path.add_argument("target", metavar="TO", help="the node id the chains end at")
316
+ path.add_argument(
317
+ "--kind",
318
+ action="append",
319
+ default=[],
320
+ metavar="K",
321
+ help="follow only edges of kind K; repeat for several",
322
+ )
323
+ path.add_argument(
324
+ "--limit",
325
+ type=int,
326
+ default=10,
327
+ metavar="N",
328
+ help="chains to show; 0 shows all (default: 10)",
329
+ )
330
+ path.add_argument(
331
+ "--map",
332
+ type=Path,
333
+ default=None,
334
+ metavar="PATH",
335
+ help="the map file to read (default: .periplus/map.json under the project root)",
336
+ )
337
+ path.add_argument(
338
+ "--format",
339
+ # A literal for the reason `types`' is; a test asserts it equals `tuple(path.RENDERERS)`.
340
+ choices=("text", "json"),
341
+ default="text",
342
+ help="how to print the report (default: text)",
343
+ )
344
+ status = subcommands.add_parser(
345
+ "status",
346
+ help="report the settings, the pack search path and the directories on it",
347
+ description="Report what this project resolves to, and stop.",
348
+ )
349
+ status.add_argument(
350
+ "--format",
351
+ # The two names are written out here and read off `report.RENDERERS` nowhere. Reading them
352
+ # would put `from periplus.report import RENDERERS` on this module's import path, and
353
+ # `report` imports `resolution`, which imports `settings`, which imports `ruamel.yaml` — so
354
+ # a missing distribution would raise `ImportError` before the check that exists to name it.
355
+ # That is the one property this whole package is arranged around. A test asserts these two
356
+ # names equal `tuple(RENDERERS)`, which is the tie that a duplicated constant needs.
357
+ choices=("text", "json"),
358
+ default="text",
359
+ help="how to print the report (default: text)",
360
+ )
361
+ status.add_argument(
362
+ "--settings",
363
+ type=Path,
364
+ default=None,
365
+ metavar="PATH",
366
+ help="the project settings file to read, instead of the one the project root implies",
367
+ )
368
+ return parser
369
+
370
+
371
+ def main(argv: Sequence[str] | None = None) -> int:
372
+ """Parse arguments, dispatch, and return the process status.
373
+
374
+ Returns an integer for every path this function decides, and the interpreter turns it into
375
+ the exit status because ``[project.scripts]`` points here.
376
+
377
+ One honest limit, because an earlier version of this docstring overstated it: ``SystemExit`` is
378
+ raised from inside argparse for ``--help``, for an unrecognised argument, and now for a bare
379
+ ``periplus`` — the last through the ``parser.error`` call below, which is argparse's own way of
380
+ reporting a usage error. The design accepts that for usage errors. So the guarantee is that
381
+ nothing *here* exits the process for a result it computed, not that no call under it can — an
382
+ embedder calling ``main(["--help"])`` or ``main([])`` gets ``SystemExit``, and a test asserting
383
+ on those paths must expect it.
384
+ """
385
+ # Before `parse_args`, and not merely before the branches below it. argparse writes `--help`
386
+ # to stdout from inside `parse_args` and exits there, so a pin placed after it leaves that one
387
+ # path writing through whatever the console handed over — measured: `main(["--help"])` left
388
+ # `sys.stdout.encoding` at `ascii` and emitted CRLF terminators, while `main(["--version"])`
389
+ # left it `utf-8`. This is the first statement in the function for that reason.
390
+ _pin_the_output_stream()
391
+
392
+ parser = build_parser()
393
+ args = parser.parse_args(argv)
394
+
395
+ if args.version:
396
+ # Through the package's own accessor rather than a second importlib.metadata call.
397
+ # Two code paths reading one number is how they drift, and the accessor exists for this.
398
+ # Answered before the dependency check, so `--version` works on a broken install — which
399
+ # is what a person runs first when they are trying to find out what they have.
400
+ print(periplus.__version__)
401
+ return ExitCode.OK
402
+
403
+ # Startup step 1, still. A bare `periplus` is a usage error and is decided here, above the
404
+ # dependency check, so it exits 2 on a broken install as well as on a working one. The two
405
+ # paths cannot reach each other: the parse decides the bare case and the check decides the
406
+ # broken-install case.
407
+ if args.command is None:
408
+ parser.error("a subcommand is required")
409
+
410
+ # Startup step 2. Every absent distribution is named, not just the first, because a person who
411
+ # installs one and re-runs to be told about the second has been made to do the tool's work.
412
+ #
413
+ # To stderr, and stdout stays empty. The design's rule is that the report is the data and goes
414
+ # to stdout in whichever format, *including the problems it found*, and that stderr carries
415
+ # only what is not part of the report. This message is not part of the report, because on this
416
+ # path there is no report: step 2 runs before step 3 calls `resolve()`, nothing is constructed
417
+ # and nothing is rendered. It is a startup diagnostic about an install, not a finding about a
418
+ # codebase.
419
+ #
420
+ # The rule's own purpose settles it the same way. It exists so that "a consumer parsing
421
+ # --format json reads one stream and gets one document whether the run succeeded or failed".
422
+ # A bare text line on stdout would hand that consumer a malformed document — strictly worse
423
+ # than an empty stdout and a status of 9.
424
+ dependencies = check_runtime_dependencies()
425
+ missing = [status.distribution for status in dependencies if not status.present]
426
+ if missing:
427
+ for distribution in missing:
428
+ print(
429
+ f"{parser.prog}: required distribution is not installed: {distribution}",
430
+ file=sys.stderr,
431
+ )
432
+ return ExitCode.MISSING_DEPENDENCY
433
+
434
+ # This lazy import is load-bearing, not tidiness. `init.py` imports `_escape`
435
+ # from `periplus.report`, which pulls `resolution`, `settings` and `packs` behind it, so
436
+ # `import periplus.init` reaches both runtime distributions. At module level that would put
437
+ # `ruamel.yaml` and `platformdirs` on this file's import path and the run would die of an
438
+ # `ImportError` before the check above ever executed — the exact failure "a missing dependency
439
+ # fails at startup, named" exists to stop. Measured, not reasoned: with a module-level import
440
+ # reaching a runtime distribution planted here,
441
+ # `test_a_missing_dependency_ends_the_run_at_exit_9_naming_every_one` fails on its
442
+ # `ImportError` assertion.
443
+ #
444
+ # `periplus init` therefore exits 9 on an install missing either distribution: one startup order
445
+ # for the whole tool rather than a branch deciding which subcommand is exempt, and `--version`
446
+ # above remains the question answered before any check.
447
+ if args.command == "init":
448
+ from periplus.detect import detect
449
+ from periplus.init import initialise_project, render_text
450
+
451
+ # Detection runs here rather than inside `initialise_project`, so that the command that
452
+ # creates a directory is not also the command that reads every pack manifest on the search
453
+ # path. `detect` never raises and reports nothing: a repository it recognises nothing in is
454
+ # the ordinary case, and `periplus status` is what reports on the search path.
455
+ here = Path.cwd()
456
+ detected = detect(here, os.environ, dependencies)
457
+ # Named apart from the resolution report below. One `report` for both is a type error
458
+ # under strict, and it would read as one thing being two.
459
+ # The detections, not their pins. `initialise_project` derives what to write from what was
460
+ # found, so the pins in the file and the evidence in the report cannot disagree.
461
+ created = initialise_project(here, detected=detected)
462
+ sys.stdout.write(render_text(created))
463
+ return created.exit_code
464
+
465
+ # Imported here for the same reason `init` is, and dispatched here rather than below because
466
+ # `spec` is not a resolution report and shares no rendering with one. Whether this run touches
467
+ # the filesystem outside the package at all is `spec()`'s decision and not a branch in this
468
+ # file: with no pack named there is nothing to look up, and the module states that as a
469
+ # property with a test behind it.
470
+ if args.command == "spec":
471
+ from periplus.spec import RENDERERS as SPEC_RENDERERS
472
+ from periplus.spec import SUMMARY_RENDERERS as SPEC_SUMMARY_RENDERERS
473
+ from periplus.spec import spec
474
+
475
+ contract = spec(
476
+ start=Path.cwd(),
477
+ env=os.environ,
478
+ dependencies=dependencies,
479
+ requested=args.pack,
480
+ )
481
+ # `--summary` picks the table and `--format` picks the entry in it, so the flag is one
482
+ # lookup here rather than an argument threaded through two renderers. Whole documents stay
483
+ # the default: the consumer is a model, and making it run the command twice to reach the
484
+ # schemas it came for costs more than the bytes it saves.
485
+ renderers = SPEC_SUMMARY_RENDERERS if args.summary else SPEC_RENDERERS
486
+ sys.stdout.write(renderers[args.format](contract))
487
+ return contract.exit_code
488
+
489
+ # Imported here for the same reason `init` and `spec` are, and more sharply: `periplus.validate`
490
+ # imports `jsonschema` at its module level. That is the third runtime distribution, it arrived
491
+ # after this file's arrangement was settled, and a module-level import of this module here would
492
+ # break the startup property for it exactly as one reaching `ruamel.yaml` broke it for the
493
+ # first two. A test imports `periplus.cli` in a child and asserts that neither
494
+ # `periplus.validate` nor `jsonschema` is in `sys.modules` afterwards.
495
+ if args.command == "validate":
496
+ from periplus.validate import RENDERERS as VALIDATE_RENDERERS
497
+ from periplus.validate import validate
498
+
499
+ findings = validate(
500
+ start=Path.cwd(),
501
+ env=os.environ,
502
+ dependencies=dependencies,
503
+ requested=args.pack,
504
+ )
505
+ sys.stdout.write(VALIDATE_RENDERERS[args.format](findings))
506
+ return findings.exit_code
507
+
508
+ # Imported here because `periplus.update` imports `ruamel.yaml`.
509
+ if args.command == "update":
510
+ from periplus.update import RENDERERS as UPDATE_RENDERERS
511
+ from periplus.update import update_pins
512
+
513
+ updated = update_pins(start=Path.cwd(), env=os.environ, dependencies=dependencies)
514
+ sys.stdout.write(UPDATE_RENDERERS[args.format](updated))
515
+ return updated.exit_code
516
+
517
+ # Imported here for the reason `validate` is: `periplus.map` imports `jsonschema`.
518
+ if args.command == "map":
519
+ from periplus.map import RENDERERS as MAP_RENDERERS
520
+ from periplus.map import run
521
+
522
+ written = run(
523
+ start=Path.cwd(),
524
+ env=os.environ,
525
+ dependencies=dependencies,
526
+ settings_path=args.settings,
527
+ output=args.output,
528
+ )
529
+ sys.stdout.write(MAP_RENDERERS[args.format](written))
530
+ return written.exit_code
531
+
532
+ if getattr(args, "limit", 0) < 0:
533
+ parser.error("--limit must be 0 or more")
534
+
535
+ # Imported here for the reason `map` is: `periplus.navigate` imports `jsonschema`.
536
+ if args.command == "types":
537
+ from periplus.navigate.types import RENDERERS as TYPES_RENDERERS
538
+ from periplus.navigate.types import run as run_types
539
+
540
+ listed = run_types(start=Path.cwd(), env=os.environ, map_path=args.map)
541
+ sys.stdout.write(TYPES_RENDERERS[args.format](listed))
542
+ return listed.exit_code
543
+
544
+ if args.command == "check-map":
545
+ from periplus.checkmap import RENDERERS as CHECK_RENDERERS
546
+ from periplus.checkmap import run as run_check
547
+
548
+ checked = run_check(start=Path.cwd(), env=os.environ, map_path=args.map)
549
+ sys.stdout.write(CHECK_RENDERERS[args.format](checked))
550
+ return checked.exit_code
551
+
552
+ if args.command == "find":
553
+ from periplus.navigate.find import RENDERERS as FIND_RENDERERS
554
+ from periplus.navigate.find import run as run_find
555
+
556
+ found = run_find(
557
+ start=Path.cwd(),
558
+ env=os.environ,
559
+ text=args.text,
560
+ type_name=args.type,
561
+ limit=args.limit,
562
+ map_path=args.map,
563
+ )
564
+ sys.stdout.write(FIND_RENDERERS[args.format](found))
565
+ return found.exit_code
566
+
567
+ if args.command == "show":
568
+ from periplus.navigate.show import RENDERERS as SHOW_RENDERERS
569
+ from periplus.navigate.show import run as run_show
570
+
571
+ shown = run_show(
572
+ start=Path.cwd(),
573
+ env=os.environ,
574
+ node_id=args.id,
575
+ limit=args.limit,
576
+ map_path=args.map,
577
+ )
578
+ sys.stdout.write(SHOW_RENDERERS[args.format](shown))
579
+ return shown.exit_code
580
+
581
+ if args.command in ("neighbors", "affected"):
582
+ from periplus.navigate.walk import RENDERERS as WALK_RENDERERS
583
+ from periplus.navigate.walk import run as run_walk
584
+
585
+ if args.depth < 1:
586
+ parser.error("--depth must be 1 or more")
587
+ walked = run_walk(
588
+ start=Path.cwd(),
589
+ env=os.environ,
590
+ node_id=args.id,
591
+ direction="out" if args.command == "neighbors" else "in",
592
+ depth=args.depth,
593
+ kinds=args.kind,
594
+ limit=args.limit,
595
+ map_path=args.map,
596
+ )
597
+ sys.stdout.write(WALK_RENDERERS[args.format](walked))
598
+ return walked.exit_code
599
+
600
+ if args.command == "path":
601
+ from periplus.navigate.path import RENDERERS as PATH_RENDERERS
602
+ from periplus.navigate.path import run as run_path
603
+
604
+ chained = run_path(
605
+ start=Path.cwd(),
606
+ env=os.environ,
607
+ from_id=args.source,
608
+ to_id=args.target,
609
+ kinds=args.kind,
610
+ limit=args.limit,
611
+ map_path=args.map,
612
+ )
613
+ sys.stdout.write(PATH_RENDERERS[args.format](chained))
614
+ return chained.exit_code
615
+
616
+ # Startup step 3. Imported here and never at module level: everything below this line reaches
617
+ # `ruamel.yaml` and `platformdirs`, and the check above is what makes reaching them safe.
618
+ from periplus.report import RENDERERS
619
+ from periplus.resolution import resolve
620
+
621
+ # Step 2's tuple is passed forward rather than recomputed. It is the only call site, and a
622
+ # second call from inside `resolve` would run the check after the failure it exists to prevent.
623
+ report = resolve(
624
+ start=Path.cwd(),
625
+ env=os.environ,
626
+ dependencies=dependencies,
627
+ settings_path=args.settings,
628
+ pack_checks=True,
629
+ )
630
+
631
+ # Startup step 4. The report is the data, so it goes to stdout in whichever format, including
632
+ # the problems it found, and the status carries the verdict.
633
+ sys.stdout.write(RENDERERS[args.format](report))
634
+ return report.exit_code
635
+
636
+
637
+ def _pin_the_output_stream() -> None:
638
+ """Write UTF-8 with LF line endings, whatever the console would otherwise have done.
639
+
640
+ Two different failures, one call. A non-UTF-8 default console encoding mangles or refuses a
641
+ directory name; Windows text mode translates every LF into CRLF, which breaks the report's "LF
642
+ line endings" rule outright and would make two runs on two platforms differ in bytes.
643
+
644
+ **Neither half is reproducible on this machine.** This is Linux, where the default is already
645
+ UTF-8 with LF, so this is reasoned from documented CPython behaviour rather than observed. It
646
+ is also belt and braces for the encoding half: ``render_text`` emits ASCII only, and
647
+ ``render_json`` is called with ``ensure_ascii=True``, so there is nothing in either document
648
+ that a console encoding could refuse.
649
+
650
+ Guarded by the type rather than by ``hasattr``, because the guard is a real case and not a
651
+ workaround. The case is **not** pytest's capture, as instrumenting it
652
+ shows: ``EncodedFile`` and ``CaptureIO`` both subclass ``io.TextIOWrapper``, and
653
+ across the whole suite the guard excluded capture zero times. What it does exclude is a plain
654
+ ``io.StringIO`` — the shape a caller gets from ``contextlib.redirect_stdout``, which one test
655
+ here uses — and that has no stream to reconfigure.
656
+ """
657
+ if isinstance(sys.stdout, io.TextIOWrapper):
658
+ sys.stdout.reconfigure(encoding="utf-8", newline="\n")