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,352 @@
1
+ # The rules
2
+
3
+ Sixteen rules. Every one came from something breaking, and the evidence is beside it.
4
+
5
+ They are here because the format was proved across five packs — `php`, `yaml`, `go`,
6
+ `drupal`, `twig` — and what made that work is not the schema's field list. It is these.
7
+
8
+ ---
9
+
10
+ ## 1. A rule fills seven slots, and none of them names a language
11
+
12
+ ```
13
+ rule its own name, recorded as provenance on everything it produces
14
+ reads file, or map
15
+ in named folders — only when it reads files
16
+ match one kind, plus a where list
17
+ id where the id comes from, and how it is reshaped
18
+ emits node, edge, attribute, type, or role
19
+ confidence declared or inferred
20
+ ```
21
+
22
+ 102 rules across five packs use exactly these seven keys and no others. A rule that finds a
23
+ PHP class, a YAML mapping entry, a Go interface and a Drupal block plugin has the same shape
24
+ in all four cases.
25
+
26
+ **The test applied to every proposed key:** can it be stated without naming a language or a
27
+ language construct? `deriver_argument`, `unwrap: constructor_arg_0` and
28
+ `alter_hook_definition_key` all failed it and were removed. Those belong in the values a pack
29
+ writes.
30
+
31
+ ---
32
+
33
+ ## 2. Two relationships, and `reads` is the whole of it
34
+
35
+ ```
36
+ supplementary independent. each rule finds the thing on its own.
37
+ complementary dependent. the second reads what the first produced.
38
+ ```
39
+
40
+ `reads: file` and `reads: map` make that mechanical, and it is the only ordering the engine
41
+ ever needs. No weights, no numbers — Drupal's own weight systems show where numbers end up,
42
+ with everything at `-49` beside a comment explaining why it is not `-50`.
43
+
44
+ **It tracks how explicit a language is, not where a pack sits.** PHP writes `extends` and
45
+ `implements` at the declaration site, so 21 of its 21 rules read files and none reads the
46
+ map. Go states no interface relationship anywhere, so it needs map rules — and Go is a base
47
+ pack with nothing beneath it.
48
+
49
+ ```
50
+ php 21 file rules 0 map rules
51
+ yaml 1 0
52
+ go 6 2
53
+ drupal 32 16
54
+ ```
55
+
56
+ ---
57
+
58
+ ## 3. Merging is an outcome of matching ids, not a property of rules
59
+
60
+ Two rules land on one node **only if both extract the same id**. If they do not, the map
61
+ grows two nodes for one thing, looks fully populated, and nothing errors.
62
+
63
+ > The PHP pack's identity was `parts: [namespace, declared_name]` — right for a class, wrong
64
+ > for anything inside one. Run against a real module it merged four different `build()`
65
+ > methods on four different classes into one node. No error, no warning.
66
+
67
+ The failure modes differ, which is why the two names in rule 2 are worth having. Lose a
68
+ supplementary rule and the node still arrives by the other route — what is lost is the link.
69
+ Lose a complementary rule and the thing never appears.
70
+
71
+ ---
72
+
73
+ ## 4. Identity is a named source, optionally reshaped
74
+
75
+ ```
76
+ from a named source: a file stem, a matched argument, a captured value
77
+ pattern optional. searched in that source; its named captures become parts
78
+ template optional. assembles the parts into the id
79
+ normalize optional. declared substitutions
80
+ ```
81
+
82
+ The steps run in that order: from, pattern, template, normalize. On a tree declaration rule,
83
+ `pattern` reads one source and is searched in its text, not matched whole. A source it does not
84
+ fit makes nothing: no node, no edge, no skipped row, and no fire. A `{name}` in the pattern is a
85
+ path value or a settings value, filled in as literal text.
86
+
87
+ This closed the sharpest question the design had open — whether id transformations are a
88
+ closed set of engine functions, in which case a pack needing a new one needs a release. They
89
+ are not a set. Regex is a general string language written as data.
90
+
91
+ > `field.field.block_content.type_way_code.body` carries three facts in one name — entity
92
+ > type, bundle, field. One pattern with named captures reads it, and reads
93
+ > `field.field.node.article.body` unchanged.
94
+
95
+ **The id comes from the thing, never from the rule.** Renaming a rule must not churn ids
96
+ across a committed map.
97
+
98
+ ---
99
+
100
+ ## 5. A rule emits nodes or edges, never both
101
+
102
+ The part of a file that makes a node is a different part from the part that makes an edge,
103
+ and they sit on different lines. One rule doing both stamps one location on all of it.
104
+
105
+ ```
106
+ block.block.block_ways_1_attribute.yml
107
+ line 1 the file exists -> the node
108
+ line 5 dependencies: block_ways -> an edge
109
+ line 13 plugin: 'block_ways_attribute' -> another edge
110
+ ```
111
+
112
+ The schema refuses a rule that emits both. Before the split, all three said line 1, and
113
+ "where is this block placed" answered with the top of the file.
114
+
115
+ An edge rule still computes an id — it needs one to name its endpoint. It just does not emit
116
+ a node with it.
117
+
118
+ ---
119
+
120
+ ## 6. A rule never writes a path
121
+
122
+ The pack names folders; the project settings override them.
123
+
124
+ ```
125
+ pack config = ./config/sync
126
+ rule in: [config, module_config]
127
+ settings config = ./drupal-app/config/default
128
+ ```
129
+
130
+ > Three real Drupal sites keep exported config in three different places, and one of them is
131
+ > computed in PHP at runtime: `dirname($app_root) . '/config/default'`. With the path in the
132
+ > rule, that is a pack edit. With a name, it is one line of settings and no rule changes.
133
+
134
+ ---
135
+
136
+ ## 7. A rule never writes a file extension
137
+
138
+ ```
139
+ match:
140
+ file: "block_content.type.*"
141
+ filetype: yaml
142
+ ```
143
+
144
+ The rule names the file and declares the type. The pack that owns the language claims the
145
+ extensions — `[yml, yaml]` — so a site keeping config as `.yaml` needs no rule change, and
146
+ a glob can never accidentally match a file of another type sharing a name.
147
+
148
+ `match.ending` is the one place a rule names an ending, and only one a pack already claims: it
149
+ keeps the files of that ending, or of any in a list, where the glob, which reads the stem, cannot
150
+ tell `.inc` from `.theme`. An ending that no loaded pack claims is refused, and the refusal lists
151
+ the claimed endings.
152
+
153
+ ---
154
+
155
+ ## 8. Confidence is declared on the rule, never computed
156
+
157
+ ```
158
+ Go formatter.go ^[A-Z] on a name declared capitalisation IS visibility
159
+ React Footer.tsx ^[A-Z] on a name inferred a convention people can ignore
160
+ ```
161
+
162
+ Same primitive, same regex, same character. Only the pack author knows which.
163
+
164
+ The case that proves it is not the regex: the alter-hook rule reads a string **literal**, so
165
+ a runtime check would call it `declared`. What is uncertain is not the literal — it is that
166
+ assigning into `$definitions[...]` inside a `block_alter` implementation *constitutes
167
+ declaring a plugin*. That is a claim about what the hook means.
168
+
169
+ ---
170
+
171
+ ## 9. A type is declared once; rules for it arrive from anywhere
172
+
173
+ ```
174
+ block/block_type.yaml declares drupal.block_type, finds nothing
175
+ block/block_type_config.yaml one rule, the config file
176
+ block/block_type_code.yaml one rule, BlockContentType::create
177
+ ```
178
+
179
+ Each mechanism file names what it contributes to, so a typo fails the type-table build rather
180
+ than minting a type nothing ever finds.
181
+
182
+ **What this opens is the point.** A rule file written locally for one repository is the same
183
+ kind of thing as a rule file in the shipped pack. The test, and it is mechanically checkable:
184
+ *a local rule file, copied unchanged into the official pack, does exactly the same thing.*
185
+
186
+ ---
187
+
188
+ ## 10. An edge target is an id, not a second vocabulary
189
+
190
+ Edge targets grew eight keys — `compose`, `namespace`, `key`, `argument_key`,
191
+ `class_from_argument`, `matching`, `each`, `template`. Every one of them collapsed into the
192
+ id block that already existed. What is left is `from`, `template`, `where` for filtering, and
193
+ `each` for cardinality — three of which are the id block and one of which is not identity at
194
+ all.
195
+
196
+ A tree declaration rule's end may also be `{declared: <type>}`: the id of the node of that
197
+ type that a rule of the same pack, or of a pack it depends on, declares at the same match; the
198
+ first such rule that fires there and mints an id gives it. The type must be declared by one of
199
+ those packs, or the map stops with exit 23, and must meet the kind's types at that end, or it
200
+ stops with exit 22. When no such rule mints an id there, the edge is not made and one skipped row
201
+ names the end; when no rule maps that id, the end is a referenced node of the named type alone.
202
+ The end does not test the other rule's folders. It, `enclosing_class` and `enclosing_method` take
203
+ another rule's whole id, so an end filled only by whole ids may list types with different id
204
+ namespaces.
205
+
206
+ An end may also find its name in a table the pack carries. `tables`, a top-level key of
207
+ `pack.yaml`, maps a table's name to a map of written name to node name; the resolution step
208
+ `lookup_last_segment_in: <table>` gives the value of the name's last segment, and a rule may name
209
+ the tables of its pack and of the packs it depends on, the nearer pack's winning. A step naming a
210
+ table none of them declares is refused, naming the `tables` key. The table is data, so the engine
211
+ still holds no language knowledge. An end read `{argument: N}` also takes a class constant
212
+ declared as text, when the grammar block declares `constant_access` and `constant_declaration`,
213
+ the node types of a constant read and of a declaration; one without the other is refused.
214
+
215
+ ---
216
+
217
+ ## 11. A match is one kind plus a `where` list
218
+
219
+ `match` reached 24 keys, most used once. The top eight name what is being matched:
220
+
221
+ ```
222
+ declaration attribute annotation static_call method_call array_entry
223
+ subscript_assignment node_type text
224
+ ```
225
+
226
+ Everything else was a condition wearing the wrong shape — `in_method_with_attribute`,
227
+ `argument_present`, `on_service`, `has_child`, `ancestor_reaches` — and moved under `where`.
228
+
229
+ Two conditions force `reads: map`, because they cannot be answered from one file:
230
+ `ancestor_reaches` and `contained_by_type`.
231
+
232
+ `name_matches` tests a child's whole text. Its string form tests the first `name_child`. Its
233
+ mapping form, `{child: scope, pattern: Config}`, names the child: a field of the match, or else
234
+ its first child of that node type. So one rule can test a call's class and its method. A match
235
+ without that child, or whose child does not match, makes nothing and is not a fire.
236
+
237
+ ---
238
+
239
+ ## 12. Code is text; the tree is an optimisation
240
+
241
+ A file is text and a pattern can read it. The parse tree makes containment and disambiguation
242
+ cheap and correct, and it is not the foundation.
243
+
244
+ > Signature normalisation was called "language knowledge the format cannot express". It is a
245
+ > pattern and three substitutions:
246
+ >
247
+ > ```yaml
248
+ > pattern: '(?s)interface\s*\{(?<body>.*?)\}'
249
+ > normalize:
250
+ > - {replace: 'interface\{\}', with: 'any'}
251
+ > - {replace: '\n', with: ';'}
252
+ > - {replace: '[ \t]+', with: ' '}
253
+ > ```
254
+ >
255
+ > That produced `MarshalYAML() (any, error)` and four correct `satisfies` edges, from an
256
+ > engine containing no Go.
257
+
258
+ **The consequence is the floor.** `packs/twig` has no grammar block, two files, and it maps
259
+ templates. Adding a stack needs patterns, not a tree-sitter grammar.
260
+
261
+ ---
262
+
263
+ ## 13. Claim only what the method can see
264
+
265
+ A pattern cannot tell these apart:
266
+
267
+ ```
268
+ {{ way }} a variable
269
+ {{ way|upper }} a variable with a filter
270
+ {{ path('front') }} a function call
271
+ {{ user.name }} a property on an object
272
+ ```
273
+
274
+ So the twig pack emits `twig.reference`, not `twig.variable`, and marks it `inferred`. The
275
+ alternative is calling `path` a variable, which is a wrong entry in a committed artifact.
276
+
277
+ Nothing is lost by being honest. The names are still there, still connected, still findable —
278
+ and a reader knows not to trust them as variables.
279
+
280
+ ---
281
+
282
+ ## 14. A pack may only name types it declares or a dependency declares
283
+
284
+ Checkable before a single source file is opened, which is what makes building the type table
285
+ an ordering commitment rather than an implementation detail. Checked mechanically across five
286
+ packs; it found one violation.
287
+
288
+ There are no kinds of pack. A root is a pack with nothing beneath it and need not be a
289
+ language. If any level were special, a third party could not occupy it.
290
+
291
+ ---
292
+
293
+ ## 15. Something is a node when the two sides can exist without each other — and always when something names it
294
+
295
+ ```
296
+ a block plugin and its class two nodes views_block:… is a plugin with no first-party class
297
+ a field storage and its use one node, one `body` storage, three bundles,
298
+ three edges the attachment is the edge
299
+ a class extending another one edge "the extending" is not a thing anyone asks about
300
+ ```
301
+
302
+ **And the harder half:** if something else names it by id, it must be a node, because an edge
303
+ cannot be the target of an edge. Two displays name a field instance, and that was the
304
+ argument for keeping it a node — until the displays turned out to declare their own bundle,
305
+ so pointing at the storage loses nothing.
306
+
307
+ **A boundary is named, never read.** A pack's `boundary` groups list the framework names the
308
+ project builds on. Each group gives a `type`, its `ancestry` edge kinds and its `names`. A listed
309
+ name that an edge reaches is a node with state `declared`: the group's type, no location, the
310
+ drawing pack's provenance. A name's value classifies a mapped class whose ancestry reaches the
311
+ name. A class whose type lies above the value takes the value's type. A class already of that
312
+ type or below it keeps its type. Either way it gains an `inferred` row. When its type and the
313
+ value lie on no one chain, it keeps its type and the report holds a skipped row naming both.
314
+
315
+ ---
316
+
317
+ ## 16. Silence is the enemy
318
+
319
+ Every failure worth naming from building this was silent. A pack that reads fine and does
320
+ nothing:
321
+
322
+ ```
323
+ on: class_declaration parsed as the boolean key `true`, in five rules
324
+ two `id:` keys in a mapping YAML kept the last, discarding 48 rule names
325
+ a duplicate `pack:` key discarded an entire file of React rules
326
+ find: {file: "*.yml"} matched nothing on three real sites, silently
327
+ name_child a field name in one pack, a node type in another
328
+ ```
329
+
330
+ So: the meta-schema rejects unknown keys, the engine reports every rule that fired zero times,
331
+ and a rule shape it cannot execute is named rather than skipped.
332
+
333
+ **And the map does the same.** A rule that looked and failed emits a node recording what it
334
+ searched for and what it found — not a warning, and never an edge to a name it could not
335
+ resolve.
336
+
337
+ > The spike emitted 221 call edges to bare words. `Draft -> t`, fifty-eight times, where `t`
338
+ > is a translation method.
339
+
340
+ ---
341
+
342
+ ## Still open
343
+
344
+ - **Cardinality.** Nothing says a class extends one class and implements many. A second
345
+ `inherits` edge would sit in a map looking fine. It belongs on the edge kind, per pack —
346
+ Go has no `inherits` at all and a Go struct embeds many.
347
+ - **Descriptions.** Each node carries `source_hash`; the artifact keyed by node id has no
348
+ schema.
349
+ - **Addressing inside a data file.** `each: "services.*"` is one underspecified line, and two
350
+ packs need it.
351
+ - **Resolution.** The PHP pack declares a whole section — `scope`, `match`,
352
+ `never_match_by`, `on_zero_definitions` — and nothing specifies how an engine uses it.
periplus/detect.py ADDED
@@ -0,0 +1,228 @@
1
+ """Framework detection: which packs a repository looks like it needs, read off the packs themselves.
2
+
3
+ The signatures and the records here were written before the bodies were, as the specification the
4
+ tests in ``tests/test_detect.py`` were written against. Eleven of those tests expect ``()`` and so
5
+ passed against the skeleton's unconditional empty answer; their sensitivity is not something being
6
+ green demonstrates, and it was checked by planting a spurious detection and watching each one fire.
7
+
8
+ The one rule this module exists to keep, and the reason it is not a table of frameworks:
9
+
10
+ A detector is declared in a pack manifest, never in engine code. Recognising a new stack is a
11
+ ``detect:`` block in somebody's ``pack.yaml`` and no edit here. A third-party pack is detected
12
+ exactly as a bundled one is, because this module cannot tell them apart — it reads manifests
13
+ off the pack search path and evaluates what they declare.
14
+
15
+ That property fails silently, so ``tests/test_detect.py`` asserts it
16
+ with a fixture pack this package has never heard of rather than with a bundled one.
17
+
18
+ **Where it sits.** It reads ``pack.yaml``, so it is on ``manifest.py``'s side of the line
19
+ ``packs.py`` draws: ``packs.py`` opens no file inside a pack directory and this module opens one per
20
+ candidate. It is a peer of ``manifest.py`` rather than part of it because it also probes the
21
+ *repository* being initialised, which is a second tree and a second question, and ``manifest.py``
22
+ is about pack files only.
23
+
24
+ **It never raises for anything a person's input can cause, and it returns no ``Problem``.** A pack
25
+ whose manifest cannot be read contributes no detection and no report — the same posture
26
+ ``manifest.py`` takes towards a mistyped ``depends``, and for the same reason: the validator
27
+ reports what a pack file gets wrong, and ``periplus status`` reports what the pack search path gets
28
+ wrong. A detector is a suggestion written into a file a person then edits, so the cost of a missed
29
+ detection is a line they type and the cost of a spurious one is a wrong pin in a committed
30
+ artifact. Every ambiguity therefore resolves towards detecting nothing.
31
+
32
+ **Determinism**, which is a stated property of this tool and not a nicety. Detections come back
33
+ sorted by pin, never in the order the filesystem listed the pack directories, and a signal is
34
+ evaluated against a path joined under the repository root rather than against whatever the pack
35
+ asked for.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ from collections.abc import Mapping, Sequence
41
+ from dataclasses import dataclass
42
+ from pathlib import Path
43
+
44
+ from periplus.manifest import ManifestUnreadable, load_manifest
45
+ from periplus.packs import PackCandidate
46
+ from periplus.preflight import DependencyStatus
47
+ from periplus.resolution import resolve
48
+
49
+ __all__ = ["DETECT_KEY", "Detection", "detect", "detect_packs"]
50
+
51
+ #: The manifest key a pack declares its detector under. Named here rather than spelled at each use,
52
+ #: and read out of ``PackManifest.document`` rather than off a field: ``manifest.py`` carries the
53
+ #: whole parsed mapping precisely so a consumer like this one can read a block it does not model,
54
+ #: and its docstring names framework detection as one of the two consumers that exist for.
55
+ DETECT_KEY = "detect"
56
+
57
+ #: The key a detector names its alternatives under, and the two a single alternative may carry.
58
+ #: They are the same names ``pack-manifest.schema.json`` declares, and the sets exist so that this
59
+ #: evaluator closes both levels the way the schema closes them: a block or a signal carrying a key
60
+ #: neither set holds is read as no detector at all. The schema is what tells an author they wrote
61
+ #: ``contain:``; this is what stops that manifest detecting on existence alone in the meantime,
62
+ #: because the validator that enforces the schema is a later task and ``init`` does not run it.
63
+ ANY_OF_KEY = "any_of"
64
+ PATH_KEY = "path"
65
+ CONTAINS_KEY = "contains"
66
+ _DETECT_KEYS = frozenset({ANY_OF_KEY})
67
+ _SIGNAL_KEYS = frozenset({PATH_KEY, CONTAINS_KEY})
68
+
69
+
70
+ @dataclass(frozen=True, slots=True)
71
+ class Detection:
72
+ """One pack whose detector fired, and the evidence that fired it.
73
+
74
+ ``pack`` is the pack half of the directory name and ``pin`` is the whole ``<pack>@<version>``,
75
+ which is what a ``packs:`` list holds. Both, because they answer two questions: which pack this
76
+ is, and what to write into a settings file. Deriving one from the other at a call site is how
77
+ two callers come to disagree about where the version came from.
78
+
79
+ ``path`` is the repository-relative path of the signal that matched, carried so a person can be
80
+ told *why* a pack was suggested. A detection with no evidence behind it is a guess presented as
81
+ a finding.
82
+ """
83
+
84
+ pack: str
85
+ pin: str
86
+ path: str
87
+
88
+
89
+ def detect_packs(root: Path, candidates: Sequence[PackCandidate]) -> tuple[Detection, ...]:
90
+ """Every pack on the search path whose declared detector matches the repository at ``root``.
91
+
92
+ ``root`` is the repository being examined and ``candidates`` are the pack directories to read,
93
+ both arguments rather than discovered here, for the reason ``describe_contract`` takes its
94
+ candidates: a test drives this against a temporary tree with nothing monkeypatched, and the
95
+ question of where packs live stays in ``packs.py``.
96
+
97
+ Only ``named`` candidates are considered — an entry that is not a ``<pack>@<version>``
98
+ directory is not a pack, whatever is inside it.
99
+
100
+ Sorted by pin. Never raises.
101
+
102
+ One detection per pin, and the first candidate carrying that pin is the one that answers. Two
103
+ roots holding the same ``<pack>@<version>`` directory are the state ``match_pins`` reports as
104
+ ``PIN_DUPLICATED``, and this is not the place it is reported: what a settings file needs is the
105
+ pin written once, and writing it twice would hand a person a duplicate to delete on top of the
106
+ problem the resolver will name for them anyway.
107
+ """
108
+ found: dict[str, Detection] = {}
109
+ for candidate in candidates:
110
+ if candidate.status != "named" or candidate.name is None or candidate.entry in found:
111
+ continue
112
+ try:
113
+ manifest = load_manifest(candidate.root.traversable / candidate.entry)
114
+ except ManifestUnreadable:
115
+ # One unreadable `pack.yaml` on the search path contributes nothing and stops nothing.
116
+ # See the module docstring: reporting on the search path is `periplus status`'s, and a
117
+ # third-party pack with a truncated manifest must not turn creating a project into a
118
+ # traceback.
119
+ continue
120
+ evidence = _first_match(root, manifest.document.get(DETECT_KEY))
121
+ if evidence is not None:
122
+ found[candidate.entry] = Detection(
123
+ pack=candidate.name.pack, pin=candidate.entry, path=evidence
124
+ )
125
+ return tuple(found[pin] for pin in sorted(found))
126
+
127
+
128
+ def _first_match(root: Path, block: object) -> str | None:
129
+ """The path of the first signal in ``block`` that matches ``root``, or nothing.
130
+
131
+ ``block`` is whatever the manifest had under ``detect:`` — a mapping if the author wrote one,
132
+ and any YAML value at all otherwise. Every shape that is not a detector answers ``None``, which
133
+ is the same answer a pack with no ``detect:`` key gives: an unreadable detector recognises
134
+ nothing, never everything.
135
+
136
+ The alternatives are tried in the order they were written, so the evidence a person is shown is
137
+ the first thing the author said would count rather than whichever probe happened to run first.
138
+ """
139
+ if not isinstance(block, Mapping) or not set(block) <= _DETECT_KEYS:
140
+ return None
141
+ alternatives = block.get(ANY_OF_KEY)
142
+ if not isinstance(alternatives, list | tuple) or not alternatives:
143
+ return None
144
+ for signal in alternatives:
145
+ matched = _match(root, signal)
146
+ if matched is not None:
147
+ return matched
148
+ return None
149
+
150
+
151
+ def _match(root: Path, signal: object) -> str | None:
152
+ """One alternative against the repository: its declared path if it matches, else nothing.
153
+
154
+ A signal with no ``path`` matches nothing. That is the case the direction of every decision
155
+ here exists for: read as "no constraint", a pathless signal fires on every repository there is,
156
+ and the pin it writes lands in a committed file with nothing in the tree to explain it.
157
+
158
+ ``contains`` is a **literal substring** of the file's text, never a pattern. A pack is
159
+ third-party data read with no time limit around it, and a backtracking regular expression over
160
+ an attacker-chosen subject is a cost this has no way to bound. A file that will not decode as
161
+ UTF-8 or will not open matches nothing, rather than matching on its bytes.
162
+
163
+ What comes back is the path **as the pack declared it**, not the absolute path that was
164
+ probed: it is evidence a person reads beside a repository-relative settings file, and the
165
+ machine's own directory layout is not part of why a pack was suggested.
166
+ """
167
+ if not isinstance(signal, Mapping) or not set(signal) <= _SIGNAL_KEYS:
168
+ return None
169
+ declared = signal.get(PATH_KEY)
170
+ if not isinstance(declared, str):
171
+ return None
172
+ target = _under(root, declared)
173
+ if target is None or not target.is_file():
174
+ return None
175
+ if CONTAINS_KEY not in signal:
176
+ return declared
177
+ wanted = signal[CONTAINS_KEY]
178
+ if not isinstance(wanted, str):
179
+ return None
180
+ try:
181
+ text = target.read_text(encoding="utf-8")
182
+ except (OSError, UnicodeDecodeError, ValueError):
183
+ return None
184
+ return declared if wanted in text else None
185
+
186
+
187
+ def _under(root: Path, declared: str) -> Path | None:
188
+ """``declared`` joined under ``root``, or ``None`` if it does not stay there.
189
+
190
+ A signal path is third-party data and ``pathlib`` obliges whatever it is handed: ``root /
191
+ "../outside.txt"`` reaches upwards and joining an absolute path discards the root entirely. So
192
+ the join is checked rather than trusted, and both sides are resolved before the check, which is
193
+ also what stops a symlink inside the repository from pointing the probe out of it.
194
+
195
+ What that closes is a probe. A detector alternative per candidate path turns a committed
196
+ settings file into a readout of which files exist on the machine that ran ``init``, and the
197
+ person reviewing that diff has no way to see where the answer came from.
198
+
199
+ The root itself is not a match. A path that resolves to the repository is not a file inside it,
200
+ and an empty ``path:`` is the way to write one by accident.
201
+ """
202
+ try:
203
+ base = root.resolve()
204
+ full = (base / declared).resolve()
205
+ except (OSError, ValueError):
206
+ return None
207
+ if full == base or not full.is_relative_to(base):
208
+ return None
209
+ return full
210
+
211
+
212
+ def detect(
213
+ start: Path,
214
+ env: Mapping[str, str],
215
+ dependencies: tuple[DependencyStatus, ...],
216
+ ) -> tuple[Detection, ...]:
217
+ """The command's entry point: resolve the pack search path, then detect against ``start``.
218
+
219
+ The search path is resolved **exactly as ``status`` resolves it**, by calling ``resolve()`` —
220
+ the same choice ``spec()`` makes and for the same reason. A second definition of where packs
221
+ live would drift, and would miss a ``pack_paths`` root a user-level settings file added.
222
+
223
+ Problems ``resolve()` found are deliberately dropped rather than returned. This runs as part of
224
+ ``periplus init``, where a directory matching no pack still exits 0, and a
225
+ settings fault surfacing through the create-a-project command would report on a state that
226
+ command is not about. ``periplus status`` is where the search path is reported on.
227
+ """
228
+ return detect_packs(start, resolve(start=start, env=env, dependencies=dependencies).candidates)
@@ -0,0 +1,5 @@
1
+ """The extraction engine, one module per run step of ``contract/spec/engine.md``.
2
+
3
+ The engine names no stack. Every folder, type, file extension and rule it acts on comes from a
4
+ loaded pack.
5
+ """