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.
- periplus/__init__.py +70 -0
- periplus/checkmap.py +80 -0
- periplus/cli.py +658 -0
- periplus/contract/schema/map.schema.json +138 -0
- periplus/contract/schema/navigation.schema.json +200 -0
- periplus/contract/schema/pack-file.schema.json +1405 -0
- periplus/contract/schema/pack-manifest.schema.json +245 -0
- periplus/contract/schema/project-settings.schema.json +85 -0
- periplus/contract/spec/engine.md +151 -0
- periplus/contract/spec/rules.md +352 -0
- periplus/detect.py +228 -0
- periplus/engine/__init__.py +5 -0
- periplus/engine/check.py +481 -0
- periplus/engine/emit.py +368 -0
- periplus/engine/packload.py +2321 -0
- periplus/engine/pattern.py +27 -0
- periplus/engine/rules.py +1946 -0
- periplus/engine/select.py +491 -0
- periplus/engine/source.py +172 -0
- periplus/engine/tree.py +424 -0
- periplus/errors.py +332 -0
- periplus/init.py +358 -0
- periplus/layout.py +28 -0
- periplus/manifest.py +498 -0
- periplus/map.py +364 -0
- periplus/navigate/__init__.py +1 -0
- periplus/navigate/find.py +143 -0
- periplus/navigate/ids.py +30 -0
- periplus/navigate/listing.py +42 -0
- periplus/navigate/mapfile.py +189 -0
- periplus/navigate/path.py +175 -0
- periplus/navigate/show.py +175 -0
- periplus/navigate/types.py +83 -0
- periplus/navigate/walk.py +138 -0
- periplus/packs/advancedqueue_basic@0.0.1/pack.yaml +14 -0
- periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type.yaml +9 -0
- periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type_annotation.yaml +43 -0
- periplus/packs/advancedqueue_basic@0.0.1/queue_job_type/queue_job_type_attribute.yaml +43 -0
- periplus/packs/ai_basic@0.0.1/ai_agent/ai_agent.yaml +8 -0
- periplus/packs/ai_basic@0.0.1/ai_agent/ai_agent_attribute.yaml +43 -0
- periplus/packs/ai_basic@0.0.1/ai_function_call/ai_function_call.yaml +8 -0
- periplus/packs/ai_basic@0.0.1/ai_function_call/ai_function_call_attribute.yaml +43 -0
- periplus/packs/ai_basic@0.0.1/pack.yaml +15 -0
- periplus/packs/config_pages_basic@0.0.1/bundle/config_pages_bundle_config.yaml +54 -0
- periplus/packs/config_pages_basic@0.0.1/pack.yaml +11 -0
- periplus/packs/crop_basic@0.0.1/bundle/crop_bundle_config.yaml +54 -0
- periplus/packs/crop_basic@0.0.1/pack.yaml +11 -0
- periplus/packs/drupal@0.1.0/block/block_placement.yaml +25 -0
- periplus/packs/drupal@0.1.0/block/block_placement_code.yaml +58 -0
- periplus/packs/drupal@0.1.0/block/block_placement_config.yaml +53 -0
- periplus/packs/drupal@0.1.0/block/block_plugin.yaml +52 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_altered.yaml +61 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_annotation.yaml +69 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_attribute.yaml +56 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_content.yaml +46 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_derived.yaml +110 -0
- periplus/packs/drupal@0.1.0/block/block_plugin_deriver.yaml +30 -0
- periplus/packs/drupal@0.1.0/block/block_type.yaml +14 -0
- periplus/packs/drupal@0.1.0/block/block_type_code.yaml +43 -0
- periplus/packs/drupal@0.1.0/block/block_type_config.yaml +45 -0
- periplus/packs/drupal@0.1.0/cache/cache.yaml +18 -0
- periplus/packs/drupal@0.1.0/cache/cache_backend.yaml +31 -0
- periplus/packs/drupal@0.1.0/component/sdc.yaml +19 -0
- periplus/packs/drupal@0.1.0/component/sdc_definition.yaml +25 -0
- periplus/packs/drupal@0.1.0/component/sdc_use.yaml +25 -0
- periplus/packs/drupal@0.1.0/config/config_object.yaml +65 -0
- periplus/packs/drupal@0.1.0/display/display.yaml +32 -0
- periplus/packs/drupal@0.1.0/display/display_code.yaml +53 -0
- periplus/packs/drupal@0.1.0/display/display_config.yaml +95 -0
- periplus/packs/drupal@0.1.0/entity/entity.yaml +33 -0
- periplus/packs/drupal@0.1.0/entity/entity_class.yaml +60 -0
- periplus/packs/drupal@0.1.0/entity/entity_save.yaml +27 -0
- periplus/packs/drupal@0.1.0/entity/entity_type_annotation.yaml +85 -0
- periplus/packs/drupal@0.1.0/entity/entity_type_attribute.yaml +76 -0
- periplus/packs/drupal@0.1.0/field/field_instance_code.yaml +40 -0
- periplus/packs/drupal@0.1.0/field/field_instance_config.yaml +58 -0
- periplus/packs/drupal@0.1.0/field/field_storage.yaml +37 -0
- periplus/packs/drupal@0.1.0/field/field_storage_code.yaml +39 -0
- periplus/packs/drupal@0.1.0/field/field_storage_config.yaml +24 -0
- periplus/packs/drupal@0.1.0/form/form.yaml +48 -0
- periplus/packs/drupal@0.1.0/form/form_class.yaml +50 -0
- periplus/packs/drupal@0.1.0/form/form_id.yaml +48 -0
- periplus/packs/drupal@0.1.0/form/form_methods.yaml +39 -0
- periplus/packs/drupal@0.1.0/hook/hook.yaml +8 -0
- periplus/packs/drupal@0.1.0/hook/hook_attribute.yaml +28 -0
- periplus/packs/drupal@0.1.0/module/module.yaml +8 -0
- periplus/packs/drupal@0.1.0/module/module_info.yaml +50 -0
- periplus/packs/drupal@0.1.0/pack.yaml +184 -0
- periplus/packs/drupal@0.1.0/plugin/plugin.yaml +17 -0
- periplus/packs/drupal@0.1.0/plugin/plugin_annotation.yaml +120 -0
- periplus/packs/drupal@0.1.0/recommendations/recommendations.yaml +14 -0
- periplus/packs/drupal@0.1.0/service/service.yaml +19 -0
- periplus/packs/drupal@0.1.0/service/service_autowire.yaml +28 -0
- periplus/packs/drupal@0.1.0/service/service_definition.yaml +24 -0
- periplus/packs/drupal@0.1.0/service/service_use.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/action/action.yaml +9 -0
- periplus/packs/drupal_basic@0.3.0/action/action_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/action/action_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/block/block_placement.yaml +16 -0
- periplus/packs/drupal_basic@0.3.0/block/block_placement_config.yaml +51 -0
- periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin.yaml +14 -0
- periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin_annotation.yaml +47 -0
- periplus/packs/drupal_basic@0.3.0/block_plugin/block_plugin_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/block_type/block_type.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/block_type/block_type_config.yaml +54 -0
- periplus/packs/drupal_basic@0.3.0/boundary/boundary_types.yaml +19 -0
- periplus/packs/drupal_basic@0.3.0/bundle/bundle.yaml +20 -0
- periplus/packs/drupal_basic@0.3.0/bundle/bundle_config.yaml +393 -0
- periplus/packs/drupal_basic@0.3.0/call/call.yaml +349 -0
- periplus/packs/drupal_basic@0.3.0/component/component.yaml +30 -0
- periplus/packs/drupal_basic@0.3.0/config/config_object.yaml +119 -0
- periplus/packs/drupal_basic@0.3.0/constraint/constraint.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/constraint/constraint_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/constraint/constraint_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/display/display.yaml +30 -0
- periplus/packs/drupal_basic@0.3.0/display/display_config.yaml +203 -0
- periplus/packs/drupal_basic@0.3.0/display/display_mode_config.yaml +60 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/config_entity_type_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/config_entity_type_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/content_entity_type_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/content_entity_type_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/entity_type/entity_type.yaml +16 -0
- periplus/packs/drupal_basic@0.3.0/field/field_instance_config.yaml +116 -0
- periplus/packs/drupal_basic@0.3.0/field/field_storage.yaml +21 -0
- periplus/packs/drupal_basic@0.3.0/field/field_storage_config.yaml +45 -0
- periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/field_formatter/field_formatter_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/field_type/field_type_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/field_type/field_type_attribute.yaml +50 -0
- periplus/packs/drupal_basic@0.3.0/field_widget/field_widget_annotation.yaml +46 -0
- periplus/packs/drupal_basic@0.3.0/field_widget/field_widget_attribute.yaml +50 -0
- periplus/packs/drupal_basic@0.3.0/form/form_id.yaml +80 -0
- periplus/packs/drupal_basic@0.3.0/hook/hook.yaml +100 -0
- periplus/packs/drupal_basic@0.3.0/hook/hook_attribute.yaml +64 -0
- periplus/packs/drupal_basic@0.3.0/hook/hook_theme_include.yaml +34 -0
- periplus/packs/drupal_basic@0.3.0/library/library.yaml +12 -0
- periplus/packs/drupal_basic@0.3.0/library/library_definition.yaml +58 -0
- periplus/packs/drupal_basic@0.3.0/local_action/local_action.yaml +12 -0
- periplus/packs/drupal_basic@0.3.0/local_action/local_action_definition.yaml +49 -0
- periplus/packs/drupal_basic@0.3.0/mail/mail.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/mail/mail_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/media_source/media_source.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/media_source/media_source_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/media_source/media_source_attribute.yaml +79 -0
- periplus/packs/drupal_basic@0.3.0/menu_link/menu_link.yaml +18 -0
- periplus/packs/drupal_basic@0.3.0/menu_link/menu_link_definition.yaml +48 -0
- periplus/packs/drupal_basic@0.3.0/module/module.yaml +7 -0
- periplus/packs/drupal_basic@0.3.0/module/module_info.yaml +58 -0
- periplus/packs/drupal_basic@0.3.0/pack.yaml +528 -0
- periplus/packs/drupal_basic@0.3.0/permission/permission.yaml +7 -0
- periplus/packs/drupal_basic@0.3.0/permission/permission_definition.yaml +27 -0
- periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker.yaml +9 -0
- periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/queue_worker/queue_worker_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource.yaml +8 -0
- periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource_annotation.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/rest_resource/rest_resource_attribute.yaml +43 -0
- periplus/packs/drupal_basic@0.3.0/role/role.yaml +14 -0
- periplus/packs/drupal_basic@0.3.0/role/role_config.yaml +44 -0
- periplus/packs/drupal_basic@0.3.0/route/route.yaml +42 -0
- periplus/packs/drupal_basic@0.3.0/route/route_definition.yaml +113 -0
- periplus/packs/drupal_basic@0.3.0/route/route_options.yaml +55 -0
- periplus/packs/drupal_basic@0.3.0/route/route_requirements.yaml +108 -0
- periplus/packs/drupal_basic@0.3.0/service/service.yaml +56 -0
- periplus/packs/drupal_basic@0.3.0/service/service_definition.yaml +181 -0
- periplus/packs/drupal_basic@0.3.0/template/template.yaml +86 -0
- periplus/packs/drupal_basic@0.3.0/template/template_route_link.yaml +30 -0
- periplus/packs/drupal_basic@0.3.0/theme/theme.yaml +15 -0
- periplus/packs/drupal_basic@0.3.0/theme/theme_info.yaml +61 -0
- periplus/packs/drupal_basic@0.3.0/theme_hook/template_suggestion.yaml +29 -0
- periplus/packs/drupal_basic@0.3.0/theme_hook/theme_hook.yaml +87 -0
- periplus/packs/drupal_basic@0.3.0/theme_hook/theme_hook_render.yaml +20 -0
- periplus/packs/drupal_basic@0.3.0/url/url.yaml +88 -0
- periplus/packs/drupal_basic@0.3.0/views_plugin/views_field_attribute.yaml +80 -0
- periplus/packs/drupal_basic@0.3.0/views_plugin/views_filter_attribute.yaml +80 -0
- periplus/packs/drupal_basic@0.3.0/views_plugin/views_plugin.yaml +15 -0
- periplus/packs/drupal_basic@0.3.0/views_plugin/views_style_attribute.yaml +80 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/attachment.yaml +12 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_php.yaml +15 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_php_array.yaml +52 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/attachment_in_twig.yaml +26 -0
- periplus/packs/drupal_js_basic@0.0.1/attachment/php_function_holder.yaml +13 -0
- periplus/packs/drupal_js_basic@0.0.1/behavior/behavior.yaml +198 -0
- periplus/packs/drupal_js_basic@0.0.1/pack.yaml +153 -0
- periplus/packs/drupal_js_basic@0.0.1/setting/setting.yaml +13 -0
- periplus/packs/drupal_js_basic@0.0.1/setting/setting_in_js.yaml +15 -0
- periplus/packs/drupal_js_basic@0.0.1/setting/setting_in_php.yaml +92 -0
- periplus/packs/drush_basic@0.0.1/command/command.yaml +15 -0
- periplus/packs/drush_basic@0.0.1/command/command_annotation.yaml +60 -0
- periplus/packs/drush_basic@0.0.1/command/command_attribute.yaml +88 -0
- periplus/packs/drush_basic@0.0.1/pack.yaml +29 -0
- periplus/packs/eck_basic@0.0.1/bundle/eck_bundle_config.yaml +56 -0
- periplus/packs/eck_basic@0.0.1/entity_type/eck_entity_type_config.yaml +26 -0
- periplus/packs/eck_basic@0.0.1/pack.yaml +28 -0
- periplus/packs/go@0.0.2/interfaces/satisfies.yaml +47 -0
- periplus/packs/go@0.0.2/pack.yaml +51 -0
- periplus/packs/go@0.0.2/types/find_declarations.yaml +137 -0
- periplus/packs/go@0.0.2/types/node_types.yaml +36 -0
- periplus/packs/go_basic@0.0.3/pack.yaml +104 -0
- periplus/packs/go_basic@0.0.3/rules/calls.yaml +25 -0
- periplus/packs/go_basic@0.0.3/rules/declarations.yaml +40 -0
- periplus/packs/go_basic@0.0.3/rules/imports.yaml +16 -0
- periplus/packs/go_basic@0.0.3/types/node_types.yaml +15 -0
- periplus/packs/js_basic@0.0.1/function/function.yaml +68 -0
- periplus/packs/js_basic@0.0.1/pack.yaml +50 -0
- periplus/packs/laravel_basic@0.0.5/pack.yaml +313 -0
- periplus/packs/laravel_basic@0.0.5/rules/classes.yaml +42 -0
- periplus/packs/laravel_basic@0.0.5/rules/config.yaml +229 -0
- periplus/packs/laravel_basic@0.0.5/rules/routes.yaml +210 -0
- periplus/packs/laravel_basic@0.0.5/rules/types.yaml +50 -0
- periplus/packs/laravel_basic@0.0.5/rules/views.yaml +100 -0
- periplus/packs/paragraphs_basic@0.0.1/bundle/paragraph_bundle_config.yaml +54 -0
- periplus/packs/paragraphs_basic@0.0.1/pack.yaml +12 -0
- periplus/packs/php@0.1.0/edges/edge_kinds.yaml +64 -0
- periplus/packs/php@0.1.0/edges/find_calls.yaml +34 -0
- periplus/packs/php@0.1.0/edges/find_imports.yaml +18 -0
- periplus/packs/php@0.1.0/edges/find_member_access.yaml +22 -0
- periplus/packs/php@0.1.0/edges/find_structure.yaml +38 -0
- periplus/packs/php@0.1.0/embedded/embedded_regions.yaml +19 -0
- periplus/packs/php@0.1.0/imports/imports.yaml +20 -0
- periplus/packs/php@0.1.0/pack.yaml +117 -0
- periplus/packs/php@0.1.0/resolution/excluded_tokens.yaml +23 -0
- periplus/packs/php@0.1.0/resolution/resolution.yaml +35 -0
- periplus/packs/php@0.1.0/types/find_declarations.yaml +102 -0
- periplus/packs/php@0.1.0/types/node_types.yaml +55 -0
- periplus/packs/php_basic@0.2.0/edges/edge_kinds.yaml +48 -0
- periplus/packs/php_basic@0.2.0/edges/find_structure.yaml +750 -0
- periplus/packs/php_basic@0.2.0/imports/imports.yaml +19 -0
- periplus/packs/php_basic@0.2.0/pack.yaml +94 -0
- periplus/packs/php_basic@0.2.0/types/find_declarations.yaml +267 -0
- periplus/packs/php_basic@0.2.0/types/node_types.yaml +55 -0
- periplus/packs/profile_basic@0.0.1/bundle/profile_bundle_config.yaml +54 -0
- periplus/packs/profile_basic@0.0.1/pack.yaml +12 -0
- periplus/packs/salesforce_basic@0.0.1/event/salesforce_event.yaml +36 -0
- periplus/packs/salesforce_basic@0.0.1/pack.yaml +31 -0
- periplus/packs/salesforce_basic@0.0.1/salesforce_mapping/salesforce_mapping.yaml +19 -0
- periplus/packs/salesforce_basic@0.0.1/salesforce_mapping/salesforce_mapping_config.yaml +83 -0
- periplus/packs/salesforce_basic@0.0.1/salesforce_mapping_field/salesforce_mapping_field.yaml +8 -0
- periplus/packs/salesforce_basic@0.0.1/salesforce_mapping_field/salesforce_mapping_field_annotation.yaml +43 -0
- periplus/packs/twig@0.0.1/pack.yaml +26 -0
- periplus/packs/twig@0.0.1/types/template.yaml +57 -0
- periplus/packs/twig_basic@0.1.0/pack.yaml +11 -0
- periplus/packs/twig_tweak_basic@0.0.1/pack.yaml +27 -0
- periplus/packs/twig_tweak_basic@0.0.1/rules/block.yaml +20 -0
- periplus/packs/twig_tweak_basic@0.0.1/rules/entity.yaml +35 -0
- periplus/packs/twig_tweak_basic@0.0.1/rules/field.yaml +21 -0
- periplus/packs/twig_tweak_basic@0.0.1/rules/menu.yaml +21 -0
- periplus/packs/webform_basic@0.0.1/bundle/webform_submission_bundle_config.yaml +54 -0
- periplus/packs/webform_basic@0.0.1/pack.yaml +20 -0
- periplus/packs/webform_basic@0.0.1/webform_handler/webform_handler.yaml +8 -0
- periplus/packs/webform_basic@0.0.1/webform_handler/webform_handler_annotation.yaml +43 -0
- periplus/packs/yaml@0.0.1/pack.yaml +72 -0
- periplus/packs/yaml@0.0.1/types/find_mapping_entry.yaml +21 -0
- periplus/packs/yaml@0.0.1/types/yaml_types.yaml +44 -0
- periplus/packs/yaml_basic@0.1.0/pack.yaml +10 -0
- periplus/packs.py +447 -0
- periplus/preflight.py +102 -0
- periplus/py.typed +0 -0
- periplus/report.py +596 -0
- periplus/resolution.py +355 -0
- periplus/settings.py +617 -0
- periplus/settings.stub.yml +99 -0
- periplus/spec.py +774 -0
- periplus/update.py +197 -0
- periplus/validate.py +655 -0
- periplus_map-0.2.1.dist-info/METADATA +566 -0
- periplus_map-0.2.1.dist-info/RECORD +272 -0
- periplus_map-0.2.1.dist-info/WHEEL +4 -0
- periplus_map-0.2.1.dist-info/entry_points.txt +2 -0
- periplus_map-0.2.1.dist-info/licenses/LICENSE +202 -0
- 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)
|