ww-agentic-workflows 1.0.0.dev3__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.
- ww/__init__.py +18 -0
- ww/_bundled_extensions/ww/git/extension.py +1728 -0
- ww/action_execution.py +887 -0
- ww/actions/__init__.py +94 -0
- ww/actions/command.py +444 -0
- ww/actions/contracts.py +699 -0
- ww/actions/extension.py +197 -0
- ww/actions/mcp.py +84 -0
- ww/actions/prompt.py +74 -0
- ww/actions/skill.py +62 -0
- ww/actions/slash_command.py +63 -0
- ww/agents.py +151 -0
- ww/amendments.py +54 -0
- ww/artifacts.py +93 -0
- ww/assessments.py +181 -0
- ww/assets/__init__.py +2 -0
- ww/assets/agent_instructions.md +49 -0
- ww/assets/docs/examples.md +879 -0
- ww/assets/docs/features.md +4639 -0
- ww/assets/docs/specification.md +1876 -0
- ww/assets/noww_skill.md +11 -0
- ww/assets/workflows/catchall.yaml +26 -0
- ww/assets/workflows/onboarding.yaml +586 -0
- ww/assets/workflows/scriptize.yaml +130 -0
- ww/assets/ww-automate_skill.md +23 -0
- ww/assets/ww-deduce-feedback_skill.md +38 -0
- ww/assets/ww-feedback-rules_skill.md +48 -0
- ww/assets/ww-learn-project_skill.md +22 -0
- ww/assets/ww-refresh_skill.md +26 -0
- ww/assets/ww-rule_skill.md +83 -0
- ww/assets/ww-rules-from-artifacts_skill.md +22 -0
- ww/assets/ww-scriptize_skill.md +33 -0
- ww/assets/ww-setup_skill.md +94 -0
- ww/assets/ww-solve_skill.md +23 -0
- ww/assets/ww-suggest_skill.md +32 -0
- ww/assets/ww-wizard_skill.md +105 -0
- ww/assets/ww_skill.md +59 -0
- ww/assignments.py +283 -0
- ww/bootstrap.py +405 -0
- ww/builtin_workflows.py +215 -0
- ww/changes.py +225 -0
- ww/child_coordination.py +482 -0
- ww/children.py +106 -0
- ww/claude_permissions.py +115 -0
- ww/cli/__init__.py +7 -0
- ww/cli/__main__.py +6 -0
- ww/cli/audit.py +129 -0
- ww/cli/catalogs.py +131 -0
- ww/cli/discover.py +607 -0
- ww/cli/initialization.py +898 -0
- ww/cli/lookup.py +287 -0
- ww/cli/main.py +1768 -0
- ww/cli/parser.py +1200 -0
- ww/cli/prompts.py +217 -0
- ww/cli/updates.py +117 -0
- ww/completion_artifacts.py +156 -0
- ww/completion_inputs.py +39 -0
- ww/config/__init__.py +582 -0
- ww/config/actions.py +591 -0
- ww/config/composition.py +571 -0
- ww/config/rules.py +511 -0
- ww/config/steps.py +1220 -0
- ww/config/values.py +223 -0
- ww/config_files.py +191 -0
- ww/config_writes.py +264 -0
- ww/contracts.py +155 -0
- ww/control.py +41 -0
- ww/defaults.py +130 -0
- ww/design_docs.py +32 -0
- ww/discovery.py +104 -0
- ww/documents.py +217 -0
- ww/errors.py +18 -0
- ww/executable.py +43 -0
- ww/execution_models/__init__.py +64 -0
- ww/execution_models/construction.py +148 -0
- ww/execution_models/decoding.py +38 -0
- ww/execution_models/plan_codec.py +565 -0
- ww/execution_models/records.py +1206 -0
- ww/execution_models/runs.py +266 -0
- ww/extensions/__init__.py +40 -0
- ww/extensions/api.py +559 -0
- ww/extensions/registry.py +864 -0
- ww/extensions/store.py +78 -0
- ww/feedback.py +342 -0
- ww/handler_repairs.py +57 -0
- ww/hooks/__init__.py +40 -0
- ww/hooks/agents.py +380 -0
- ww/hooks/install.py +168 -0
- ww/hooks/notices.py +206 -0
- ww/hooks/records.py +209 -0
- ww/hooks/runtime.py +266 -0
- ww/hooks/transcripts.py +183 -0
- ww/inspect.py +896 -0
- ww/instructions/__init__.py +17 -0
- ww/instructions/builder.py +1682 -0
- ww/instructions/commands.py +335 -0
- ww/instructions/handoff.py +149 -0
- ww/instructions/models.py +686 -0
- ww/instructions/policy.py +219 -0
- ww/instructions/text.py +168 -0
- ww/interactions.py +187 -0
- ww/interpolation.py +37 -0
- ww/item_passes.py +167 -0
- ww/items.py +99 -0
- ww/locking.py +207 -0
- ww/metadata_publication.py +230 -0
- ww/onboarding.py +229 -0
- ww/open_work.py +236 -0
- ww/operations.py +193 -0
- ww/operator_ui/__init__.py +16 -0
- ww/operator_ui/page.html +351 -0
- ww/operator_ui/server.py +215 -0
- ww/operator_ui/session.py +389 -0
- ww/operator_ui/sheet.py +104 -0
- ww/operator_ui/view.py +109 -0
- ww/output.py +339 -0
- ww/output_adapters/__init__.py +12 -0
- ww/output_adapters/base.py +25 -0
- ww/output_adapters/json_adapter.py +37 -0
- ww/output_adapters/markdown.py +2293 -0
- ww/output_adapters/rule_pages.py +337 -0
- ww/output_adapters/terminal.py +21 -0
- ww/package_updates.py +167 -0
- ww/plan/__init__.py +38 -0
- ww/plan/actions.py +207 -0
- ww/plan/compiler.py +1492 -0
- ww/plan/constructs.py +456 -0
- ww/plan/models.py +665 -0
- ww/project_config.py +752 -0
- ww/recovery.py +401 -0
- ww/replanning.py +367 -0
- ww/results.py +77 -0
- ww/rule_checks.py +230 -0
- ww/rule_conversion.py +331 -0
- ww/rule_disputes.py +148 -0
- ww/rule_store.py +456 -0
- ww/rule_verification.py +714 -0
- ww/rule_views.py +447 -0
- ww/rule_writes.py +920 -0
- ww/run_coordination.py +158 -0
- ww/runtimes.py +105 -0
- ww/service.py +4405 -0
- ww/setup_apply.py +428 -0
- ww/step_values.py +20 -0
- ww/storage.py +447 -0
- ww/storage_adapters/__init__.py +36 -0
- ww/storage_adapters/base.py +540 -0
- ww/storage_adapters/filesystem.py +370 -0
- ww/storage_adapters/memory.py +195 -0
- ww/storage_adapters/project_metadata.py +69 -0
- ww/storage_adapters/task_document.py +484 -0
- ww/task_ids.py +114 -0
- ww/task_references.py +124 -0
- ww/transitions.py +1619 -0
- ww/updates.py +399 -0
- ww/upgrade.py +95 -0
- ww/validation.py +168 -0
- ww/variables.py +275 -0
- ww/workflow_config.py +854 -0
- ww/workflow_update.py +239 -0
- ww/workflow_validation.py +1260 -0
- ww/workspace.py +50 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/METADATA +690 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/RECORD +167 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/WHEEL +4 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/entry_points.txt +2 -0
- ww_agentic_workflows-1.0.0.dev3.dist-info/licenses/LICENSE +674 -0
ww/rule_views.py
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
"""Read-only views of rules for the ``check``, ``rule`` and ``rules`` commands.
|
|
3
|
+
|
|
4
|
+
A task's rules are read from its frozen plan, never from the configuration as
|
|
5
|
+
it reads now, so ``ww rule`` shows the wording the task was given. The
|
|
6
|
+
project's rules as declared, for ``ww rules``, come from the composed
|
|
7
|
+
configuration; the rule-automation store adds what ww learned about them.
|
|
8
|
+
Nothing here changes a task, the store, or a file; ``prune`` returns the
|
|
9
|
+
store with its orphans removed for the caller to save.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, replace
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from ww.config.rules import rule_source
|
|
18
|
+
from ww.errors import StateError
|
|
19
|
+
from ww.execution_models import CheckReport, ExecutionState, PlanItemExecution
|
|
20
|
+
from ww.instructions.builder import fix_failures
|
|
21
|
+
from ww.instructions.models import CheckPreview
|
|
22
|
+
from ww.plan import PlanItem, PlannedCheck, PlannedRule, WorkflowPlan
|
|
23
|
+
from ww.rule_conversion import ScriptizeState, scriptize_state
|
|
24
|
+
from ww.rule_disputes import DisputeEntry
|
|
25
|
+
from ww.rule_store import RuleAutomation, describe_command
|
|
26
|
+
from ww.rule_verification import to_verify
|
|
27
|
+
from ww.workflow_config import (
|
|
28
|
+
NameFilter,
|
|
29
|
+
RuleDefinition,
|
|
30
|
+
RuleGroupRef,
|
|
31
|
+
WorkflowConfiguration,
|
|
32
|
+
every_step,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_preview(
|
|
37
|
+
state: ExecutionState, item: PlanItem, report: CheckReport
|
|
38
|
+
) -> CheckPreview:
|
|
39
|
+
"""What ``ww check`` shows for a report it ran and did not record."""
|
|
40
|
+
record = state.item_executions[state.cursor]
|
|
41
|
+
return CheckPreview(
|
|
42
|
+
task_id=state.task_id,
|
|
43
|
+
step=item.name,
|
|
44
|
+
checks=len(report.results),
|
|
45
|
+
failures=fix_failures(item, record, report.failed),
|
|
46
|
+
passed=tuple(
|
|
47
|
+
result.id for result in report.results if result.status == "passed"
|
|
48
|
+
),
|
|
49
|
+
not_applicable=tuple(
|
|
50
|
+
result.id for result in report.results if result.status == "not_applicable"
|
|
51
|
+
),
|
|
52
|
+
judged=tuple(
|
|
53
|
+
rule.id for rule in to_verify(item, record) if not _covered(rule, record)
|
|
54
|
+
),
|
|
55
|
+
waived=record.checks_waived,
|
|
56
|
+
all_files=report.all_files,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _covered(rule: PlannedRule, record: PlanItemExecution) -> bool:
|
|
61
|
+
return any(rule.id in check.covers for check in record.resolved_checks)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class RuleView:
|
|
66
|
+
"""One rule or check of a task, in full, for ``ww rule``.
|
|
67
|
+
|
|
68
|
+
``kind`` is ``rule`` (a rule with or without a command of its own),
|
|
69
|
+
``hook`` (a ``before_complete`` hook with ``on_failure: fix``) or
|
|
70
|
+
``derived`` (an approved store check the step resolved). ``steps`` are
|
|
71
|
+
the task's steps that carry it. For a rule without a command,
|
|
72
|
+
``store_status``, ``store_check`` and ``interpretation`` are what the
|
|
73
|
+
rule-automation store knows about its wording, and ``resolution`` how
|
|
74
|
+
the current step enforces it.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
id: str
|
|
78
|
+
kind: str
|
|
79
|
+
summary: str
|
|
80
|
+
text: str
|
|
81
|
+
steps: tuple[str, ...]
|
|
82
|
+
paths: tuple[str, ...] = ()
|
|
83
|
+
source: str | None = None
|
|
84
|
+
command: str | None = None
|
|
85
|
+
assertion: str | None = None
|
|
86
|
+
max_fixes: int | None = None
|
|
87
|
+
text_hash: str | None = None
|
|
88
|
+
store_status: str | None = None
|
|
89
|
+
store_check: str | None = None
|
|
90
|
+
interpretation: str | None = None
|
|
91
|
+
resolution: str | None = None
|
|
92
|
+
covers: tuple[str, ...] = ()
|
|
93
|
+
|
|
94
|
+
def to_dict(self) -> dict[str, object]:
|
|
95
|
+
return {
|
|
96
|
+
"id": self.id,
|
|
97
|
+
"kind": self.kind,
|
|
98
|
+
"summary": self.summary,
|
|
99
|
+
"text": self.text,
|
|
100
|
+
"steps": list(self.steps),
|
|
101
|
+
"paths": list(self.paths),
|
|
102
|
+
"source": self.source,
|
|
103
|
+
"command": self.command,
|
|
104
|
+
"assert": self.assertion,
|
|
105
|
+
"max_fixes": self.max_fixes,
|
|
106
|
+
"text_hash": self.text_hash,
|
|
107
|
+
"store_status": self.store_status,
|
|
108
|
+
"store_check": self.store_check,
|
|
109
|
+
"interpretation": self.interpretation,
|
|
110
|
+
"resolution": self.resolution,
|
|
111
|
+
"covers": list(self.covers),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def rule_view(
|
|
116
|
+
state: ExecutionState,
|
|
117
|
+
plan: WorkflowPlan,
|
|
118
|
+
automation: RuleAutomation,
|
|
119
|
+
rule_id: str,
|
|
120
|
+
) -> RuleView:
|
|
121
|
+
"""Find a rule or check of the task by ID, the current step first."""
|
|
122
|
+
order = sorted(range(len(plan.items)), key=lambda index: index != state.cursor)
|
|
123
|
+
steps = tuple(
|
|
124
|
+
dict.fromkeys(
|
|
125
|
+
plan.items[index].name
|
|
126
|
+
for index in order
|
|
127
|
+
if any(rule.id == rule_id for rule in plan.items[index].rules)
|
|
128
|
+
or any(check.id == rule_id for check in plan.items[index].checks)
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
for index in order:
|
|
132
|
+
item = plan.items[index]
|
|
133
|
+
record = state.item_executions[index]
|
|
134
|
+
rule = next((rule for rule in item.rules if rule.id == rule_id), None)
|
|
135
|
+
if rule is not None:
|
|
136
|
+
return _rule(
|
|
137
|
+
item, record, automation, rule, steps, current=index == state.cursor
|
|
138
|
+
)
|
|
139
|
+
check = next(
|
|
140
|
+
(
|
|
141
|
+
check
|
|
142
|
+
for check in (*item.checks, *record.resolved_checks)
|
|
143
|
+
if check.id == rule_id
|
|
144
|
+
),
|
|
145
|
+
None,
|
|
146
|
+
)
|
|
147
|
+
if check is not None:
|
|
148
|
+
return _check(check, steps or (item.name,))
|
|
149
|
+
raise StateError(
|
|
150
|
+
f"task {state.task_id!r} has no rule or check {rule_id!r}; the step "
|
|
151
|
+
"page and the fix page name them"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _rule(
|
|
156
|
+
item: PlanItem,
|
|
157
|
+
record: PlanItemExecution,
|
|
158
|
+
automation: RuleAutomation,
|
|
159
|
+
rule: PlannedRule,
|
|
160
|
+
steps: tuple[str, ...],
|
|
161
|
+
*,
|
|
162
|
+
current: bool,
|
|
163
|
+
) -> RuleView:
|
|
164
|
+
own = next((check for check in item.checks if check.id == rule.id), None)
|
|
165
|
+
entry = None if rule.has_command else automation.rules.get(rule.text_hash)
|
|
166
|
+
resolution = next(
|
|
167
|
+
(entry for entry in record.rule_resolutions if entry.id == rule.id), None
|
|
168
|
+
)
|
|
169
|
+
derived = next(
|
|
170
|
+
(check for check in record.resolved_checks if rule.id in check.covers), None
|
|
171
|
+
)
|
|
172
|
+
command = own or derived
|
|
173
|
+
return RuleView(
|
|
174
|
+
id=rule.id,
|
|
175
|
+
kind="rule",
|
|
176
|
+
summary=rule.summary,
|
|
177
|
+
text=rule.text,
|
|
178
|
+
steps=steps,
|
|
179
|
+
paths=rule.paths,
|
|
180
|
+
source=rule.source,
|
|
181
|
+
command=describe_command(command.command) if command else None,
|
|
182
|
+
assertion=(
|
|
183
|
+
command.command.assertion.describe()
|
|
184
|
+
if command and command.command.assertion
|
|
185
|
+
else None
|
|
186
|
+
),
|
|
187
|
+
max_fixes=rule.max_fixes,
|
|
188
|
+
text_hash=rule.text_hash,
|
|
189
|
+
store_status=entry.status if entry else None,
|
|
190
|
+
store_check=entry.check if entry else None,
|
|
191
|
+
interpretation=entry.interpretation if entry else None,
|
|
192
|
+
resolution=resolution.status if resolution and current else None,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _check(check: PlannedCheck, steps: tuple[str, ...]) -> RuleView:
|
|
197
|
+
return RuleView(
|
|
198
|
+
id=check.id,
|
|
199
|
+
kind=check.source,
|
|
200
|
+
summary=check.summary,
|
|
201
|
+
text=check.summary,
|
|
202
|
+
steps=steps,
|
|
203
|
+
paths=check.paths,
|
|
204
|
+
command=describe_command(check.command),
|
|
205
|
+
assertion=(
|
|
206
|
+
check.command.assertion.describe() if check.command.assertion else None
|
|
207
|
+
),
|
|
208
|
+
max_fixes=check.max_fixes,
|
|
209
|
+
covers=check.covers,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True)
|
|
214
|
+
class ListedRule:
|
|
215
|
+
"""One declared rule: its ID, first sentence, globs, and file.
|
|
216
|
+
|
|
217
|
+
``store_check`` names the approved rule-automation check that runs for a
|
|
218
|
+
rule without a command of its own, the one ``rules promote`` copies into
|
|
219
|
+
its file. ``scriptize`` says where the rule stands: ``command``,
|
|
220
|
+
``converted``, ``not_convertible``, ``rejected`` or ``unscriptized``.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
id: str
|
|
224
|
+
summary: str
|
|
225
|
+
paths: tuple[str, ...] = ()
|
|
226
|
+
has_check: bool = False
|
|
227
|
+
source: str | None = None
|
|
228
|
+
disputes: int = 0
|
|
229
|
+
store_check: str | None = None
|
|
230
|
+
scriptize: ScriptizeState = "unscriptized"
|
|
231
|
+
|
|
232
|
+
def to_dict(self) -> dict[str, object]:
|
|
233
|
+
return {
|
|
234
|
+
"id": self.id,
|
|
235
|
+
"summary": self.summary,
|
|
236
|
+
"paths": list(self.paths),
|
|
237
|
+
"has_check": self.has_check,
|
|
238
|
+
"source": self.source,
|
|
239
|
+
"disputes": self.disputes,
|
|
240
|
+
"store_check": self.store_check,
|
|
241
|
+
"scriptize": self.scriptize,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@dataclass(frozen=True)
|
|
246
|
+
class ListedGroup:
|
|
247
|
+
"""One root rule group: where it applies on its own, and its rules.
|
|
248
|
+
|
|
249
|
+
``workflows`` and ``steps`` admit every workflow or step, render as
|
|
250
|
+
``"*"``, or list names; an empty one means the group applies only where a
|
|
251
|
+
step names it.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
name: str
|
|
255
|
+
origin: str
|
|
256
|
+
workflows: NameFilter
|
|
257
|
+
steps: NameFilter
|
|
258
|
+
hints: dict[str, str]
|
|
259
|
+
rules: tuple[ListedRule, ...]
|
|
260
|
+
|
|
261
|
+
def to_dict(self) -> dict[str, object]:
|
|
262
|
+
return {
|
|
263
|
+
"name": self.name,
|
|
264
|
+
"origin": self.origin,
|
|
265
|
+
"workflows": self.workflows.to_data(),
|
|
266
|
+
"steps": self.steps.to_data(),
|
|
267
|
+
"hints": self.hints,
|
|
268
|
+
"rules": [rule.to_dict() for rule in self.rules],
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass(frozen=True)
|
|
273
|
+
class ListedStep:
|
|
274
|
+
"""One step's own ``rules`` list: its rules, and the groups it names."""
|
|
275
|
+
|
|
276
|
+
step: str
|
|
277
|
+
rules: tuple[ListedRule, ...]
|
|
278
|
+
groups: tuple[str, ...]
|
|
279
|
+
|
|
280
|
+
def to_dict(self) -> dict[str, object]:
|
|
281
|
+
return {
|
|
282
|
+
"step": self.step,
|
|
283
|
+
"rules": [rule.to_dict() for rule in self.rules],
|
|
284
|
+
"groups": list(self.groups),
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@dataclass(frozen=True)
|
|
289
|
+
class RulesListing:
|
|
290
|
+
"""The project's declared rules, for ``ww rules``.
|
|
291
|
+
|
|
292
|
+
``check_guidance`` is the project's ``rules.check_guidance``, so an agent
|
|
293
|
+
writing a check reads it without opening ww's configuration files.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
groups: tuple[ListedGroup, ...]
|
|
297
|
+
steps: tuple[ListedStep, ...]
|
|
298
|
+
check_guidance: str | None = None
|
|
299
|
+
|
|
300
|
+
def to_dict(self) -> dict[str, object]:
|
|
301
|
+
return {
|
|
302
|
+
"groups": [group.to_dict() for group in self.groups],
|
|
303
|
+
"steps": [step.to_dict() for step in self.steps],
|
|
304
|
+
"check_guidance": self.check_guidance,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def rules_listing(
|
|
309
|
+
configuration: WorkflowConfiguration,
|
|
310
|
+
root: Path,
|
|
311
|
+
disputes: tuple[DisputeEntry, ...] = (),
|
|
312
|
+
automation: RuleAutomation | None = None,
|
|
313
|
+
) -> RulesListing:
|
|
314
|
+
"""Every root group with its filters and rules, then every step's own list.
|
|
315
|
+
|
|
316
|
+
A step name repeated across workflows is listed once; ``disputes`` count
|
|
317
|
+
how often each rule was disputed; ``automation`` names the approved
|
|
318
|
+
store check of each rule without a command.
|
|
319
|
+
"""
|
|
320
|
+
automation = automation if automation is not None else RuleAutomation()
|
|
321
|
+
counts: dict[str, int] = {}
|
|
322
|
+
for dispute in disputes:
|
|
323
|
+
counts[dispute.check] = counts.get(dispute.check, 0) + 1
|
|
324
|
+
|
|
325
|
+
def listed(rule: RuleDefinition) -> ListedRule:
|
|
326
|
+
return ListedRule(
|
|
327
|
+
id=rule.id,
|
|
328
|
+
summary=rule.summary,
|
|
329
|
+
paths=rule.paths,
|
|
330
|
+
has_check=rule.check is not None,
|
|
331
|
+
source=rule_source(rule.source, root),
|
|
332
|
+
disputes=counts.get(rule.id, 0),
|
|
333
|
+
store_check=_store_check(automation, rule),
|
|
334
|
+
scriptize=scriptize_state(automation, rule),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
groups = tuple(
|
|
338
|
+
ListedGroup(
|
|
339
|
+
name=group.name,
|
|
340
|
+
origin=group.origin,
|
|
341
|
+
workflows=group.workflows,
|
|
342
|
+
steps=group.steps,
|
|
343
|
+
hints=group.hints.to_dict(),
|
|
344
|
+
rules=tuple(listed(rule) for rule in group.rules),
|
|
345
|
+
)
|
|
346
|
+
for group in configuration.rule_groups
|
|
347
|
+
)
|
|
348
|
+
steps: dict[str, ListedStep] = {}
|
|
349
|
+
for step in every_step(configuration):
|
|
350
|
+
if not step.rules or step.name in steps:
|
|
351
|
+
continue
|
|
352
|
+
steps[step.name] = ListedStep(
|
|
353
|
+
step=step.name,
|
|
354
|
+
rules=tuple(
|
|
355
|
+
listed(entry)
|
|
356
|
+
for entry in step.rules
|
|
357
|
+
if isinstance(entry, RuleDefinition)
|
|
358
|
+
),
|
|
359
|
+
groups=tuple(
|
|
360
|
+
entry.name for entry in step.rules if isinstance(entry, RuleGroupRef)
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
return RulesListing(groups, tuple(steps.values()))
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _store_check(automation: RuleAutomation, rule: RuleDefinition) -> str | None:
|
|
367
|
+
"""The approved store check that runs for a rule without its own command."""
|
|
368
|
+
if rule.check is not None:
|
|
369
|
+
return None
|
|
370
|
+
converted = automation.converted_check(rule.text_hash)
|
|
371
|
+
return converted[0] if converted is not None else None
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def declared_hashes(configuration: WorkflowConfiguration) -> frozenset[str]:
|
|
375
|
+
"""The wording hash of every rule the composed configuration declares."""
|
|
376
|
+
return frozenset(
|
|
377
|
+
{rule.text_hash for group in configuration.rule_groups for rule in group.rules}
|
|
378
|
+
| {
|
|
379
|
+
entry.text_hash
|
|
380
|
+
for step in every_step(configuration)
|
|
381
|
+
for entry in step.rules
|
|
382
|
+
if isinstance(entry, RuleDefinition)
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@dataclass(frozen=True)
|
|
388
|
+
class Orphans:
|
|
389
|
+
"""Store entries no declared rule needs any more.
|
|
390
|
+
|
|
391
|
+
``rules`` are rule hashes no rule has the wording of; ``checks`` are
|
|
392
|
+
checks that cover only such rules and that no remaining rule names.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
rules: tuple[str, ...] = ()
|
|
396
|
+
checks: tuple[str, ...] = ()
|
|
397
|
+
|
|
398
|
+
def __bool__(self) -> bool:
|
|
399
|
+
return bool(self.rules or self.checks)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def orphans(automation: RuleAutomation, declared: frozenset[str]) -> Orphans:
|
|
403
|
+
rules = tuple(key for key in automation.rules if key not in declared)
|
|
404
|
+
gone = set(rules)
|
|
405
|
+
named = {
|
|
406
|
+
entry.check
|
|
407
|
+
for key, entry in automation.rules.items()
|
|
408
|
+
if key not in gone and entry.check is not None
|
|
409
|
+
}
|
|
410
|
+
checks = tuple(
|
|
411
|
+
name
|
|
412
|
+
for name, check in automation.checks.items()
|
|
413
|
+
if name not in named
|
|
414
|
+
and all(
|
|
415
|
+
key in gone
|
|
416
|
+
for key in (
|
|
417
|
+
*check.spec.covers,
|
|
418
|
+
*(check.pending.covers if check.pending is not None else ()),
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
return Orphans(rules, checks)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def prune(
|
|
426
|
+
automation: RuleAutomation, listed: Orphans, declared: frozenset[str]
|
|
427
|
+
) -> RuleAutomation:
|
|
428
|
+
"""The store without the listed entries that are still orphans.
|
|
429
|
+
|
|
430
|
+
An entry that stopped being an orphan since it was listed, because its
|
|
431
|
+
wording was declared again meanwhile, is kept; one that became an orphan
|
|
432
|
+
since is not removed unasked.
|
|
433
|
+
"""
|
|
434
|
+
current = orphans(automation, declared)
|
|
435
|
+
rules = set(listed.rules) & set(current.rules)
|
|
436
|
+
checks = set(listed.checks) & set(current.checks)
|
|
437
|
+
return replace(
|
|
438
|
+
automation,
|
|
439
|
+
rules={
|
|
440
|
+
key: entry for key, entry in automation.rules.items() if key not in rules
|
|
441
|
+
},
|
|
442
|
+
checks={
|
|
443
|
+
name: check
|
|
444
|
+
for name, check in automation.checks.items()
|
|
445
|
+
if name not in checks
|
|
446
|
+
},
|
|
447
|
+
)
|