pycforge 0.15.2__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.
- pycforge/__init__.py +8 -0
- pycforge/__main__.py +2 -0
- pycforge/_version.py +1 -0
- pycforge/converter/__init__.py +0 -0
- pycforge/converter/analysis/__init__.py +5 -0
- pycforge/converter/analysis/functions.py +723 -0
- pycforge/converter/analysis/model.py +158 -0
- pycforge/converter/analysis/planning.py +464 -0
- pycforge/converter/analysis/stage.py +1976 -0
- pycforge/converter/analysis/symbols.py +237 -0
- pycforge/converter/analysis/validation.py +2166 -0
- pycforge/converter/c_output/__init__.py +2 -0
- pycforge/converter/c_output/conformance.py +297 -0
- pycforge/converter/c_output/renderer.py +220 -0
- pycforge/converter/conditional_regions/__init__.py +71 -0
- pycforge/converter/conditional_regions/analysis.py +481 -0
- pycforge/converter/conditional_regions/lowering.py +579 -0
- pycforge/converter/conditional_regions/model.py +243 -0
- pycforge/converter/conditional_regions/validation.py +690 -0
- pycforge/converter/containers/__init__.py +19 -0
- pycforge/converter/containers/analysis.py +542 -0
- pycforge/converter/containers/lowering.py +290 -0
- pycforge/converter/containers/model.py +146 -0
- pycforge/converter/contracts/__init__.py +0 -0
- pycforge/converter/contracts/configuration.py +178 -0
- pycforge/converter/contracts/identifiers.py +68 -0
- pycforge/converter/contracts/versions.py +71 -0
- pycforge/converter/core/__init__.py +0 -0
- pycforge/converter/core/artifact_io.py +87 -0
- pycforge/converter/core/cancellation.py +7 -0
- pycforge/converter/core/canonicalization.py +254 -0
- pycforge/converter/core/context.py +13 -0
- pycforge/converter/core/diagnostics.py +95 -0
- pycforge/converter/core/enums.py +23 -0
- pycforge/converter/core/fingerprint.py +22 -0
- pycforge/converter/core/pipeline.py +8 -0
- pycforge/converter/core/progress.py +25 -0
- pycforge/converter/core/request.py +50 -0
- pycforge/converter/core/resource_policy.py +31 -0
- pycforge/converter/core/result.py +23 -0
- pycforge/converter/core/serialization.py +36 -0
- pycforge/converter/core/stage.py +10 -0
- pycforge/converter/core/stage_artifact.py +80 -0
- pycforge/converter/core/stage_outcome.py +15 -0
- pycforge/converter/decision_trace/__init__.py +0 -0
- pycforge/converter/decision_trace/recorder.py +52 -0
- pycforge/converter/facade.py +459 -0
- pycforge/converter/frontend/__init__.py +4 -0
- pycforge/converter/frontend/normalizer.py +59 -0
- pycforge/converter/frontend/parser.py +20 -0
- pycforge/converter/frontend/source_document.py +93 -0
- pycforge/converter/frontend/stages.py +518 -0
- pycforge/converter/frontend/validation.py +76 -0
- pycforge/converter/io/__init__.py +0 -0
- pycforge/converter/io/atomic_writer.py +33 -0
- pycforge/converter/ir/__init__.py +1 -0
- pycforge/converter/ir/c_ir/__init__.py +4 -0
- pycforge/converter/ir/c_ir/builder.py +26 -0
- pycforge/converter/ir/c_ir/model.py +318 -0
- pycforge/converter/ir/c_ir/serialization.py +88 -0
- pycforge/converter/ir/c_ir/validation.py +834 -0
- pycforge/converter/ir/python_ir/__init__.py +17 -0
- pycforge/converter/ir/python_ir/nodes.py +86 -0
- pycforge/converter/keyword_calls/__init__.py +68 -0
- pycforge/converter/keyword_calls/analysis.py +458 -0
- pycforge/converter/keyword_calls/lowering.py +271 -0
- pycforge/converter/keyword_calls/model.py +304 -0
- pycforge/converter/keyword_calls/validation.py +802 -0
- pycforge/converter/keyword_only_calls/__init__.py +68 -0
- pycforge/converter/keyword_only_calls/analysis.py +640 -0
- pycforge/converter/keyword_only_calls/lowering.py +376 -0
- pycforge/converter/keyword_only_calls/model.py +328 -0
- pycforge/converter/keyword_only_calls/validation.py +1357 -0
- pycforge/converter/lowering.py +1000 -0
- pycforge/converter/modules/__init__.py +13 -0
- pycforge/converter/modules/analysis.py +1250 -0
- pycforge/converter/modules/lowering.py +200 -0
- pycforge/converter/modules/model.py +73 -0
- pycforge/converter/modules/stage.py +192 -0
- pycforge/converter/numeric_semantics/__init__.py +23 -0
- pycforge/converter/numeric_semantics/analysis.py +420 -0
- pycforge/converter/numeric_semantics/lowering.py +153 -0
- pycforge/converter/numeric_semantics/model.py +123 -0
- pycforge/converter/records/__init__.py +34 -0
- pycforge/converter/records/analysis.py +1394 -0
- pycforge/converter/records/lowering.py +393 -0
- pycforge/converter/records/model.py +319 -0
- pycforge/converter/rules/__init__.py +3 -0
- pycforge/converter/support_templates/__init__.py +49 -0
- pycforge/converter/support_templates/assembly.py +106 -0
- pycforge/converter/support_templates/factories.py +348 -0
- pycforge/converter/support_templates/model.py +159 -0
- pycforge/converter/support_templates/registry.py +388 -0
- pycforge/converter/telemetry/__init__.py +0 -0
- pycforge/converter/telemetry/sink.py +39 -0
- pycforge/ide/__init__.py +19 -0
- pycforge/ide/__main__.py +5 -0
- pycforge/ide/_worker_event_codec.py +276 -0
- pycforge/ide/_worker_protocol_json.py +197 -0
- pycforge/ide/_worker_protocol_types.py +90 -0
- pycforge/ide/_worker_request_codec.py +200 -0
- pycforge/ide/_worker_request_data.py +260 -0
- pycforge/ide/_worker_result_data.py +343 -0
- pycforge/ide/action_contract.py +469 -0
- pycforge/ide/action_validation.py +229 -0
- pycforge/ide/command_palette.py +223 -0
- pycforge/ide/controller.py +904 -0
- pycforge/ide/controller_conversion.py +546 -0
- pycforge/ide/controller_io.py +205 -0
- pycforge/ide/editor.py +951 -0
- pycforge/ide/editor_commands_qt.py +274 -0
- pycforge/ide/editor_lexical.py +43 -0
- pycforge/ide/editor_sidebars.py +236 -0
- pycforge/ide/editor_syntax.py +243 -0
- pycforge/ide/find_replace.py +535 -0
- pycforge/ide/icons.py +91 -0
- pycforge/ide/io_service.py +196 -0
- pycforge/ide/model.py +164 -0
- pycforge/ide/panels.py +903 -0
- pycforge/ide/positions.py +220 -0
- pycforge/ide/process_worker.py +130 -0
- pycforge/ide/qt.py +230 -0
- pycforge/ide/qt_actions.py +418 -0
- pycforge/ide/qt_close.py +169 -0
- pycforge/ide/qt_command_palette.py +297 -0
- pycforge/ide/qt_contract.py +220 -0
- pycforge/ide/qt_documents.py +590 -0
- pycforge/ide/qt_editor_buffers.py +113 -0
- pycforge/ide/qt_editor_surfaces.py +112 -0
- pycforge/ide/qt_menus.py +324 -0
- pycforge/ide/qt_projection.py +500 -0
- pycforge/ide/qt_shell.py +348 -0
- pycforge/ide/qt_shell_interactions.py +320 -0
- pycforge/ide/qt_state.py +393 -0
- pycforge/ide/qt_workspace_features.py +398 -0
- pycforge/ide/qt_workspace_navigation.py +208 -0
- pycforge/ide/qt_workspace_observers.py +173 -0
- pycforge/ide/qt_workspace_panels.py +545 -0
- pycforge/ide/qt_workspace_widgets.py +228 -0
- pycforge/ide/resources/icons/about.svg +1 -0
- pycforge/ide/resources/icons/add-document.svg +1 -0
- pycforge/ide/resources/icons/brand-mark.svg +1 -0
- pycforge/ide/resources/icons/bundle-search.svg +1 -0
- pycforge/ide/resources/icons/cancel.svg +1 -0
- pycforge/ide/resources/icons/check.svg +1 -0
- pycforge/ide/resources/icons/chevron-right.svg +1 -0
- pycforge/ide/resources/icons/close.svg +1 -0
- pycforge/ide/resources/icons/collapse-all.svg +1 -0
- pycforge/ide/resources/icons/command-palette.svg +1 -0
- pycforge/ide/resources/icons/convert.svg +1 -0
- pycforge/ide/resources/icons/copy.svg +1 -0
- pycforge/ide/resources/icons/cut.svg +1 -0
- pycforge/ide/resources/icons/decision-trace.svg +1 -0
- pycforge/ide/resources/icons/details.svg +1 -0
- pycforge/ide/resources/icons/diagnostics.svg +1 -0
- pycforge/ide/resources/icons/duplicate-line.svg +1 -0
- pycforge/ide/resources/icons/exit.svg +1 -0
- pycforge/ide/resources/icons/expand-all.svg +1 -0
- pycforge/ide/resources/icons/export.svg +1 -0
- pycforge/ide/resources/icons/find.svg +1 -0
- pycforge/ide/resources/icons/go-to-line.svg +1 -0
- pycforge/ide/resources/icons/go-to-output.svg +1 -0
- pycforge/ide/resources/icons/go-to-source.svg +1 -0
- pycforge/ide/resources/icons/history.svg +1 -0
- pycforge/ide/resources/icons/indent.svg +1 -0
- pycforge/ide/resources/icons/link-c.svg +1 -0
- pycforge/ide/resources/icons/mappings.svg +1 -0
- pycforge/ide/resources/icons/module.svg +1 -0
- pycforge/ide/resources/icons/move-down.svg +1 -0
- pycforge/ide/resources/icons/move-line-down.svg +1 -0
- pycforge/ide/resources/icons/move-line-up.svg +1 -0
- pycforge/ide/resources/icons/move-up.svg +1 -0
- pycforge/ide/resources/icons/next.svg +1 -0
- pycforge/ide/resources/icons/open.svg +1 -0
- pycforge/ide/resources/icons/outdent.svg +1 -0
- pycforge/ide/resources/icons/outline.svg +1 -0
- pycforge/ide/resources/icons/paste.svg +1 -0
- pycforge/ide/resources/icons/previous.svg +1 -0
- pycforge/ide/resources/icons/primary-module.svg +1 -0
- pycforge/ide/resources/icons/redo.svg +1 -0
- pycforge/ide/resources/icons/remove-document.svg +1 -0
- pycforge/ide/resources/icons/replace.svg +1 -0
- pycforge/ide/resources/icons/save-as.svg +1 -0
- pycforge/ide/resources/icons/save.svg +1 -0
- pycforge/ide/resources/icons/select-all.svg +1 -0
- pycforge/ide/resources/icons/settings.svg +1 -0
- pycforge/ide/resources/icons/show-c.svg +1 -0
- pycforge/ide/resources/icons/split-view.svg +1 -0
- pycforge/ide/resources/icons/summary.svg +1 -0
- pycforge/ide/resources/icons/telemetry.svg +1 -0
- pycforge/ide/resources/icons/toggle-comment.svg +1 -0
- pycforge/ide/resources/icons/toggle-fold.svg +1 -0
- pycforge/ide/resources/icons/undo.svg +1 -0
- pycforge/ide/resources/icons/whitespace.svg +1 -0
- pycforge/ide/revisions.py +400 -0
- pycforge/ide/search_service.py +280 -0
- pycforge/ide/session_history.py +150 -0
- pycforge/ide/source_editing.py +417 -0
- pycforge/ide/source_structure.py +558 -0
- pycforge/ide/source_structure_async.py +191 -0
- pycforge/ide/supervisor.py +503 -0
- pycforge/ide/theme.py +119 -0
- pycforge/ide/theme_stylesheet.py +598 -0
- pycforge/ide/theme_workspace_stylesheet.py +155 -0
- pycforge/ide/visual_tokens.py +142 -0
- pycforge/ide/worker_protocol.py +80 -0
- pycforge/ide/workspace_search.py +462 -0
- pycforge/ide/workspace_session.py +256 -0
- pycforge/laboratory/__init__.py +6 -0
- pycforge/laboratory/audits.py +2244 -0
- pycforge/laboratory/checkpoint_e.py +2685 -0
- pycforge/laboratory/cli.py +119 -0
- pycforge/laboratory/exit_codes.py +11 -0
- pycforge/laboratory/keyword_audit.py +661 -0
- pycforge/laboratory/keyword_only_audit.py +481 -0
- pycforge-0.15.2.dist-info/METADATA +209 -0
- pycforge-0.15.2.dist-info/RECORD +221 -0
- pycforge-0.15.2.dist-info/WHEEL +5 -0
- pycforge-0.15.2.dist-info/entry_points.txt +5 -0
- pycforge-0.15.2.dist-info/licenses/LICENSE +674 -0
- pycforge-0.15.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Deterministic validation for the import-safe PyCForge action contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Mapping
|
|
7
|
+
|
|
8
|
+
from .action_contract import (
|
|
9
|
+
ActionSpec,
|
|
10
|
+
PlacementKind,
|
|
11
|
+
SurfaceKind,
|
|
12
|
+
SurfaceSpec,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_action_contract(
|
|
17
|
+
*,
|
|
18
|
+
action_specs: Mapping[str, ActionSpec],
|
|
19
|
+
surface_specs: Mapping[str, SurfaceSpec],
|
|
20
|
+
dynamic_groups: Mapping[str, str],
|
|
21
|
+
) -> tuple[str, ...]:
|
|
22
|
+
"""Return stable contract errors without importing optional Qt."""
|
|
23
|
+
|
|
24
|
+
errors: list[str] = []
|
|
25
|
+
action_pattern = re.compile(
|
|
26
|
+
r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$"
|
|
27
|
+
)
|
|
28
|
+
surface_pattern = re.compile(
|
|
29
|
+
r"^(?:menu|toolbar|context)\.[a-z][a-z0-9_]*$"
|
|
30
|
+
)
|
|
31
|
+
shortcuts: dict[tuple[str, str, str], str] = {}
|
|
32
|
+
for key, spec in sorted(action_specs.items()):
|
|
33
|
+
if key != spec.action_id:
|
|
34
|
+
errors.append(f"action mapping key mismatch: {key}")
|
|
35
|
+
if not action_pattern.fullmatch(spec.action_id):
|
|
36
|
+
errors.append(f"invalid action id: {spec.action_id}")
|
|
37
|
+
if not spec.menu_text or not spec.tooltip or not spec.accessible_name:
|
|
38
|
+
errors.append(f"incomplete action metadata: {spec.action_id}")
|
|
39
|
+
mnemonics = _mnemonics(spec.menu_text)
|
|
40
|
+
if not spec.dynamic and len(mnemonics) != 1:
|
|
41
|
+
errors.append(
|
|
42
|
+
f"action must declare one mnemonic: {spec.action_id}"
|
|
43
|
+
)
|
|
44
|
+
if "&" in spec.accessible_name or "&" in spec.toolbar_text:
|
|
45
|
+
errors.append(f"presentation mnemonic leaked: {spec.action_id}")
|
|
46
|
+
if spec.standard_shortcut and spec.shortcut:
|
|
47
|
+
errors.append(
|
|
48
|
+
f"ambiguous shortcut declaration: {spec.action_id}"
|
|
49
|
+
)
|
|
50
|
+
if spec.shortcut_context not in {"window", "widget"}:
|
|
51
|
+
errors.append(f"invalid shortcut context: {spec.action_id}")
|
|
52
|
+
if spec.tone not in {"normal", "primary", "danger"}:
|
|
53
|
+
errors.append(f"invalid action tone: {spec.action_id}")
|
|
54
|
+
sequence = spec.standard_shortcut or spec.shortcut
|
|
55
|
+
if sequence:
|
|
56
|
+
shortcut_key = (
|
|
57
|
+
spec.shortcut_context,
|
|
58
|
+
"standard" if spec.standard_shortcut else "literal",
|
|
59
|
+
sequence.casefold(),
|
|
60
|
+
)
|
|
61
|
+
previous = shortcuts.get(shortcut_key)
|
|
62
|
+
if previous:
|
|
63
|
+
errors.append(
|
|
64
|
+
f"duplicate shortcut: {previous}, {spec.action_id}"
|
|
65
|
+
)
|
|
66
|
+
shortcuts[shortcut_key] = spec.action_id
|
|
67
|
+
|
|
68
|
+
for group_id, action_id in sorted(dynamic_groups.items()):
|
|
69
|
+
spec = action_specs.get(action_id)
|
|
70
|
+
if not group_id or spec is None:
|
|
71
|
+
errors.append(f"invalid dynamic group: {group_id}")
|
|
72
|
+
elif not spec.dynamic:
|
|
73
|
+
errors.append(
|
|
74
|
+
f"dynamic template is not marked dynamic: {action_id}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
for key, surface in sorted(surface_specs.items()):
|
|
78
|
+
_validate_surface(
|
|
79
|
+
key,
|
|
80
|
+
surface,
|
|
81
|
+
action_specs,
|
|
82
|
+
surface_specs,
|
|
83
|
+
dynamic_groups,
|
|
84
|
+
surface_pattern,
|
|
85
|
+
errors,
|
|
86
|
+
)
|
|
87
|
+
return tuple(errors)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _validate_surface(
|
|
91
|
+
key: str,
|
|
92
|
+
surface: SurfaceSpec,
|
|
93
|
+
action_specs: Mapping[str, ActionSpec],
|
|
94
|
+
surface_specs: Mapping[str, SurfaceSpec],
|
|
95
|
+
dynamic_groups: Mapping[str, str],
|
|
96
|
+
surface_pattern: re.Pattern[str],
|
|
97
|
+
errors: list[str],
|
|
98
|
+
) -> None:
|
|
99
|
+
if key != surface.surface_id:
|
|
100
|
+
errors.append(f"surface mapping key mismatch: {key}")
|
|
101
|
+
if not surface_pattern.fullmatch(surface.surface_id):
|
|
102
|
+
errors.append(f"invalid surface id: {surface.surface_id}")
|
|
103
|
+
if not surface.accessible_name:
|
|
104
|
+
errors.append(
|
|
105
|
+
f"missing surface accessible name: {surface.surface_id}"
|
|
106
|
+
)
|
|
107
|
+
if (
|
|
108
|
+
surface.kind is SurfaceKind.MENU
|
|
109
|
+
and len(_mnemonics(surface.title)) != 1
|
|
110
|
+
):
|
|
111
|
+
errors.append(f"menu must declare one mnemonic: {surface.surface_id}")
|
|
112
|
+
|
|
113
|
+
placements = surface.placements
|
|
114
|
+
targets: set[tuple[PlacementKind, str]] = set()
|
|
115
|
+
mnemonics: dict[str, str] = {}
|
|
116
|
+
for index, placement in enumerate(placements):
|
|
117
|
+
if placement.target:
|
|
118
|
+
placement_key = (placement.kind, placement.target)
|
|
119
|
+
if placement_key in targets:
|
|
120
|
+
errors.append(
|
|
121
|
+
f"duplicate placement {placement.target} "
|
|
122
|
+
f"in {surface.surface_id}"
|
|
123
|
+
)
|
|
124
|
+
targets.add(placement_key)
|
|
125
|
+
_validate_placement(
|
|
126
|
+
placement,
|
|
127
|
+
surface,
|
|
128
|
+
action_specs,
|
|
129
|
+
surface_specs,
|
|
130
|
+
dynamic_groups,
|
|
131
|
+
mnemonics,
|
|
132
|
+
errors,
|
|
133
|
+
)
|
|
134
|
+
if placement.kind is PlacementKind.SEPARATOR and (
|
|
135
|
+
index == 0
|
|
136
|
+
or index == len(placements) - 1
|
|
137
|
+
or placements[index - 1].kind is PlacementKind.SEPARATOR
|
|
138
|
+
):
|
|
139
|
+
errors.append(f"misplaced separator in {surface.surface_id}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _validate_placement(
|
|
143
|
+
placement,
|
|
144
|
+
surface: SurfaceSpec,
|
|
145
|
+
action_specs: Mapping[str, ActionSpec],
|
|
146
|
+
surface_specs: Mapping[str, SurfaceSpec],
|
|
147
|
+
dynamic_groups: Mapping[str, str],
|
|
148
|
+
mnemonics: dict[str, str],
|
|
149
|
+
errors: list[str],
|
|
150
|
+
) -> None:
|
|
151
|
+
if placement.kind is PlacementKind.ACTION:
|
|
152
|
+
spec = action_specs.get(placement.target)
|
|
153
|
+
if spec is None:
|
|
154
|
+
errors.append(
|
|
155
|
+
f"unknown action {placement.target} in {surface.surface_id}"
|
|
156
|
+
)
|
|
157
|
+
elif spec.dynamic:
|
|
158
|
+
errors.append(
|
|
159
|
+
f"dynamic template placed directly: {placement.target}"
|
|
160
|
+
)
|
|
161
|
+
elif surface.kind is not SurfaceKind.TOOLBAR:
|
|
162
|
+
_record_mnemonic(
|
|
163
|
+
mnemonics,
|
|
164
|
+
_mnemonics(spec.menu_text),
|
|
165
|
+
placement.target,
|
|
166
|
+
surface.surface_id,
|
|
167
|
+
errors,
|
|
168
|
+
)
|
|
169
|
+
elif placement.kind is PlacementKind.SUBMENU:
|
|
170
|
+
submenu = surface_specs.get(placement.target)
|
|
171
|
+
if submenu is None or submenu.kind is not SurfaceKind.MENU:
|
|
172
|
+
errors.append(
|
|
173
|
+
f"unknown submenu {placement.target} in {surface.surface_id}"
|
|
174
|
+
)
|
|
175
|
+
else:
|
|
176
|
+
_record_mnemonic(
|
|
177
|
+
mnemonics,
|
|
178
|
+
_mnemonics(submenu.title),
|
|
179
|
+
placement.target,
|
|
180
|
+
surface.surface_id,
|
|
181
|
+
errors,
|
|
182
|
+
)
|
|
183
|
+
elif placement.kind is PlacementKind.DYNAMIC:
|
|
184
|
+
if placement.target not in dynamic_groups:
|
|
185
|
+
errors.append(
|
|
186
|
+
f"unknown dynamic group {placement.target} "
|
|
187
|
+
f"in {surface.surface_id}"
|
|
188
|
+
)
|
|
189
|
+
elif placement.target:
|
|
190
|
+
errors.append(f"separator has a target in {surface.surface_id}")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _mnemonics(text: str) -> tuple[str, ...]:
|
|
194
|
+
markers: list[str] = []
|
|
195
|
+
index = 0
|
|
196
|
+
while index < len(text):
|
|
197
|
+
if text[index] != "&":
|
|
198
|
+
index += 1
|
|
199
|
+
continue
|
|
200
|
+
if index + 1 < len(text) and text[index + 1] == "&":
|
|
201
|
+
index += 2
|
|
202
|
+
continue
|
|
203
|
+
if index + 1 < len(text):
|
|
204
|
+
markers.append(text[index + 1].casefold())
|
|
205
|
+
index += 1
|
|
206
|
+
return tuple(markers)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _record_mnemonic(
|
|
210
|
+
owners: dict[str, str],
|
|
211
|
+
mnemonics: tuple[str, ...],
|
|
212
|
+
target: str,
|
|
213
|
+
surface_id: str,
|
|
214
|
+
errors: list[str],
|
|
215
|
+
) -> None:
|
|
216
|
+
if len(mnemonics) != 1:
|
|
217
|
+
return
|
|
218
|
+
mnemonic = mnemonics[0]
|
|
219
|
+
previous = owners.get(mnemonic)
|
|
220
|
+
if previous is not None:
|
|
221
|
+
errors.append(
|
|
222
|
+
f"duplicate mnemonic {mnemonic!r} in {surface_id}: "
|
|
223
|
+
f"{previous}, {target}"
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
owners[mnemonic] = target
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
__all__ = ["validate_action_contract"]
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Deterministic, bounded command-palette projection over declared actions.
|
|
2
|
+
|
|
3
|
+
The palette returns inert metadata and stable action IDs only. It does not
|
|
4
|
+
accept handlers, evaluate command strings, or provide an execution mechanism;
|
|
5
|
+
the optional Qt adapter must resolve a selected ID through the sole declared
|
|
6
|
+
action registry.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from .action_contract import ACTION_SPECS, ActionSpec, ActionState
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
MAX_COMMAND_PALETTE_RESULTS = 50
|
|
18
|
+
MAX_COMMAND_PALETTE_QUERY_CHARS = 256
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class CommandPaletteItem:
|
|
23
|
+
"""One inert projection of a visible static action."""
|
|
24
|
+
|
|
25
|
+
action_id: str
|
|
26
|
+
label: str
|
|
27
|
+
tooltip: str
|
|
28
|
+
accessible_name: str
|
|
29
|
+
icon_name: str | None
|
|
30
|
+
shortcut: str | None
|
|
31
|
+
shortcut_kind: str | None
|
|
32
|
+
enabled: bool
|
|
33
|
+
checked: bool | None
|
|
34
|
+
tone: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class CommandPaletteProjection:
|
|
39
|
+
"""A bounded result set plus its untruncated matching count."""
|
|
40
|
+
|
|
41
|
+
query: str
|
|
42
|
+
items: tuple[CommandPaletteItem, ...]
|
|
43
|
+
total_count: int
|
|
44
|
+
limit: int
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def truncated(self) -> bool:
|
|
48
|
+
return self.total_count > len(self.items)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def strip_mnemonics(text: str) -> str:
|
|
52
|
+
"""Remove Qt mnemonic markers while preserving escaped ampersands."""
|
|
53
|
+
|
|
54
|
+
if not isinstance(text, str):
|
|
55
|
+
raise TypeError("action label must be text")
|
|
56
|
+
rendered: list[str] = []
|
|
57
|
+
index = 0
|
|
58
|
+
while index < len(text):
|
|
59
|
+
character = text[index]
|
|
60
|
+
if character != "&":
|
|
61
|
+
rendered.append(character)
|
|
62
|
+
index += 1
|
|
63
|
+
continue
|
|
64
|
+
if index + 1 < len(text) and text[index + 1] == "&":
|
|
65
|
+
rendered.append("&")
|
|
66
|
+
index += 2
|
|
67
|
+
continue
|
|
68
|
+
index += 1
|
|
69
|
+
return "".join(rendered)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _shortcut(spec: ActionSpec) -> tuple[str | None, str | None]:
|
|
73
|
+
if spec.standard_shortcut:
|
|
74
|
+
return spec.standard_shortcut, "standard"
|
|
75
|
+
if spec.shortcut:
|
|
76
|
+
return spec.shortcut, "literal"
|
|
77
|
+
return None, None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _rank(
|
|
81
|
+
query_terms: tuple[str, ...],
|
|
82
|
+
label: str,
|
|
83
|
+
spec: ActionSpec,
|
|
84
|
+
shortcut: str | None,
|
|
85
|
+
declaration_index: int,
|
|
86
|
+
) -> tuple[int, int, str, str] | None:
|
|
87
|
+
if not query_terms:
|
|
88
|
+
return 0, declaration_index, "", spec.action_id
|
|
89
|
+
normalized_label = label.casefold()
|
|
90
|
+
normalized_id = spec.action_id.replace(".", " ").replace("_", " ").casefold()
|
|
91
|
+
searchable = " ".join(
|
|
92
|
+
(
|
|
93
|
+
normalized_label,
|
|
94
|
+
normalized_id,
|
|
95
|
+
spec.tooltip.casefold(),
|
|
96
|
+
spec.accessible_name.casefold(),
|
|
97
|
+
(shortcut or "").casefold(),
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
if any(term not in searchable for term in query_terms):
|
|
101
|
+
return None
|
|
102
|
+
joined = " ".join(query_terms)
|
|
103
|
+
if normalized_label == joined:
|
|
104
|
+
quality = 0
|
|
105
|
+
elif normalized_label.startswith(joined):
|
|
106
|
+
quality = 1
|
|
107
|
+
elif all(
|
|
108
|
+
any(word.startswith(term) for word in normalized_label.split())
|
|
109
|
+
for term in query_terms
|
|
110
|
+
):
|
|
111
|
+
quality = 2
|
|
112
|
+
elif joined in normalized_label:
|
|
113
|
+
quality = 3
|
|
114
|
+
elif joined in normalized_id:
|
|
115
|
+
quality = 4
|
|
116
|
+
else:
|
|
117
|
+
quality = 5
|
|
118
|
+
return quality, declaration_index, normalized_label, spec.action_id
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def project_command_palette(
|
|
122
|
+
query: str = "",
|
|
123
|
+
*,
|
|
124
|
+
states: Mapping[str, ActionState] | None = None,
|
|
125
|
+
action_specs: Mapping[str, ActionSpec] = ACTION_SPECS,
|
|
126
|
+
limit: int = MAX_COMMAND_PALETTE_RESULTS,
|
|
127
|
+
) -> CommandPaletteProjection:
|
|
128
|
+
"""Project visible static actions under a deterministic absolute limit."""
|
|
129
|
+
|
|
130
|
+
if not isinstance(query, str):
|
|
131
|
+
raise TypeError("palette query must be text")
|
|
132
|
+
if len(query) > MAX_COMMAND_PALETTE_QUERY_CHARS:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
"palette query exceeds "
|
|
135
|
+
f"{MAX_COMMAND_PALETTE_QUERY_CHARS} characters"
|
|
136
|
+
)
|
|
137
|
+
if (
|
|
138
|
+
isinstance(limit, bool)
|
|
139
|
+
or not isinstance(limit, int)
|
|
140
|
+
or not 1 <= limit <= MAX_COMMAND_PALETTE_RESULTS
|
|
141
|
+
):
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"palette result limit must be between 1 and "
|
|
144
|
+
f"{MAX_COMMAND_PALETTE_RESULTS}"
|
|
145
|
+
)
|
|
146
|
+
if not isinstance(action_specs, Mapping):
|
|
147
|
+
raise TypeError("action specs must be a mapping")
|
|
148
|
+
projected_states = {} if states is None else dict(states)
|
|
149
|
+
unknown = set(projected_states).difference(action_specs)
|
|
150
|
+
if unknown:
|
|
151
|
+
raise KeyError(f"state supplied for unknown actions: {sorted(unknown)}")
|
|
152
|
+
if any(
|
|
153
|
+
not isinstance(state, ActionState)
|
|
154
|
+
for state in projected_states.values()
|
|
155
|
+
):
|
|
156
|
+
raise TypeError("palette states must be ActionState values")
|
|
157
|
+
|
|
158
|
+
normalized_query = " ".join(query.split())
|
|
159
|
+
query_terms = tuple(normalized_query.casefold().split())
|
|
160
|
+
ranked: list[
|
|
161
|
+
tuple[tuple[int, int, str, str], CommandPaletteItem]
|
|
162
|
+
] = []
|
|
163
|
+
for declaration_index, (action_id, spec) in enumerate(
|
|
164
|
+
action_specs.items()
|
|
165
|
+
):
|
|
166
|
+
if not isinstance(spec, ActionSpec):
|
|
167
|
+
raise TypeError("palette action specs must be ActionSpec values")
|
|
168
|
+
if action_id != spec.action_id:
|
|
169
|
+
raise ValueError(f"action mapping key mismatch: {action_id}")
|
|
170
|
+
if spec.dynamic:
|
|
171
|
+
continue
|
|
172
|
+
state = projected_states.get(action_id, ActionState())
|
|
173
|
+
if not state.visible:
|
|
174
|
+
continue
|
|
175
|
+
label = strip_mnemonics(spec.menu_text)
|
|
176
|
+
shortcut, shortcut_kind = _shortcut(spec)
|
|
177
|
+
rank = _rank(
|
|
178
|
+
query_terms,
|
|
179
|
+
label,
|
|
180
|
+
spec,
|
|
181
|
+
shortcut,
|
|
182
|
+
declaration_index,
|
|
183
|
+
)
|
|
184
|
+
if rank is None:
|
|
185
|
+
continue
|
|
186
|
+
ranked.append(
|
|
187
|
+
(
|
|
188
|
+
rank,
|
|
189
|
+
CommandPaletteItem(
|
|
190
|
+
action_id=action_id,
|
|
191
|
+
label=label,
|
|
192
|
+
tooltip=spec.tooltip,
|
|
193
|
+
accessible_name=spec.accessible_name,
|
|
194
|
+
icon_name=spec.icon_name,
|
|
195
|
+
shortcut=shortcut,
|
|
196
|
+
shortcut_kind=shortcut_kind,
|
|
197
|
+
enabled=state.enabled,
|
|
198
|
+
checked=state.checked,
|
|
199
|
+
tone=spec.tone,
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
ranked.sort(key=lambda record: record[0])
|
|
204
|
+
return CommandPaletteProjection(
|
|
205
|
+
normalized_query,
|
|
206
|
+
tuple(item for _rank_value, item in ranked[:limit]),
|
|
207
|
+
len(ranked),
|
|
208
|
+
limit,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
filter_command_palette = project_command_palette
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
__all__ = [
|
|
216
|
+
"MAX_COMMAND_PALETTE_QUERY_CHARS",
|
|
217
|
+
"MAX_COMMAND_PALETTE_RESULTS",
|
|
218
|
+
"CommandPaletteItem",
|
|
219
|
+
"CommandPaletteProjection",
|
|
220
|
+
"filter_command_palette",
|
|
221
|
+
"project_command_palette",
|
|
222
|
+
"strip_mnemonics",
|
|
223
|
+
]
|