Skip to content

Commit c8e0d2d

Browse files
feat(ops): workflow_concern module — derive 7-bucket concern from workflow name (A1)
A1 of the Workflows page refinement (docs/specs/ops-workflows-page-refinement/). Mirrors the spec_lifecycle.py pattern (PR #533 / shipped v7.3.0). Pure-logic module that maps a workflow's canonical name to one of seven concern buckets: - review: code-review, deep-review, security-audit, bug-predict - test: test-gen, test-audit - docs: doc-gen, doc-audit, doc-orchestrator - refactor: simplify-code, refactor-plan - audit: discovery-sweep, perf-audit, dependency-check - meta: release-prep, secure-release, orchestrated-health-check, health-check - other: rag-code-gen, research-synthesis Authoritative assignment per docs/specs/ops-workflows-page-refinement/decisions.md (PR #550 — merged 2026-06-01). Module API mirrors spec_lifecycle: - derive_concern(name) → Concern (with "other" fallback) - group_by_concern(names) → dict[Concern, list[str]] (sorted) - concern_counts(names) → dict[Concern, int] (all 7 buckets populated) - ALL_CONCERNS: ordered tuple for stable UI rendering Tests (42): - Per-bucket assignment tests for all 20 workflows (one parameterized test class per bucket) - Boundary tests for the 6 trade-off cases ratified in decisions.md (security-audit/bug-predict in review not audit, test-audit in test not audit, doc-audit in docs not audit, doc-orchestrator in docs not meta, perf-audit confirmed in audit) - Unknown/empty/uppercase name fallback to "other" - group_by_concern() alphabetical-within-bucket sorting - concern_counts() always-populates-all-7-buckets contract - ALL_CONCERNS order stability (chip toolbar depends on this) - Drift-guard: every workflow returned by list_workflows() has an explicit (not-fallback) concern mapping; catches the "add-a-workflow-without-mapping" pattern at CI level Next in the Workflows page refinement spec: - A2: wire derive_concern into the route serializer - A3a/b/c: chip filter + URL params + kebab menu Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c89d8f7 commit c8e0d2d

2 files changed

Lines changed: 482 additions & 0 deletions

File tree

src/attune/ops/workflow_concern.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Concern bucket derivation for workflow names.
2+
3+
Pure logic — takes a workflow name (string slug like ``"code-review"``)
4+
and returns one of seven concern bucket labels:
5+
6+
- ``review`` — code-review, deep-review, security-audit, bug-predict
7+
- ``test`` — test-gen, test-audit
8+
- ``docs`` — doc-gen, doc-audit, doc-orchestrator
9+
- ``refactor``— simplify-code, refactor-plan
10+
- ``audit`` — discovery-sweep, perf-audit, dependency-check
11+
- ``meta`` — release-prep, secure-release, orchestrated-health-check, health-check
12+
- ``other`` — rag-code-gen, research-synthesis
13+
14+
The authoritative assignment table lives at
15+
[docs/specs/ops-workflows-page-refinement/decisions.md](../../../docs/specs/ops-workflows-page-refinement/decisions.md)
16+
§ "Concern bucket assignment". Pre-committed there before this module
17+
landed, per the existing "Pre-committed decision matrices survive
18+
contact with data" lesson.
19+
20+
Unlike ``spec_lifecycle.py`` (which derives a bucket from runtime
21+
state), concern is a **canonical static property** of the workflow
22+
itself. Each workflow lives in exactly one bucket.
23+
24+
Module is pure — no I/O, no project-root awareness. Mirrors the
25+
``spec_lifecycle.derive_lifecycle()`` shape so the ``/workflows``
26+
route can call it the same way.
27+
"""
28+
29+
from __future__ import annotations
30+
31+
from typing import Literal
32+
33+
# Type alias for the 7-bucket concern enum. Mirrors how
34+
# ``spec_lifecycle`` types the lifecycle bucket — string literals for
35+
# easy JSON serialization without needing dataclasses.
36+
Concern = Literal[
37+
"review",
38+
"test",
39+
"docs",
40+
"refactor",
41+
"audit",
42+
"meta",
43+
"other",
44+
]
45+
46+
# All valid bucket names, ordered for stable UI rendering. Don't reorder
47+
# without updating the wireframe + template chip order.
48+
ALL_CONCERNS: tuple[Concern, ...] = (
49+
"review",
50+
"test",
51+
"docs",
52+
"refactor",
53+
"audit",
54+
"meta",
55+
"other",
56+
)
57+
58+
# Authoritative assignment. Pre-committed in decisions.md.
59+
#
60+
# Trade-offs ratified there:
61+
# - bug-predict + security-audit live in `review` (not `audit`) because
62+
# their UX intent matches review-class workflows: "tell me what's
63+
# wrong with this code." `audit` is for codebase-wide health surveys.
64+
# - doc-orchestrator lives in `docs` even though it's a meta-workflow.
65+
# `meta` is reserved for release/health ops; doc-orchestrator is a
66+
# domain-specific composer.
67+
# - Naming consistency: `audit` is BOTH a bucket name AND a workflow-
68+
# name suffix (`test-audit`, `doc-audit`, `perf-audit`). These are
69+
# routed by *purpose*, not name pattern.
70+
_WORKFLOW_CONCERNS: dict[str, Concern] = {
71+
# review
72+
"code-review": "review",
73+
"deep-review": "review",
74+
"security-audit": "review",
75+
"bug-predict": "review",
76+
# test
77+
"test-gen": "test",
78+
"test-audit": "test",
79+
# docs
80+
"doc-gen": "docs",
81+
"doc-audit": "docs",
82+
"doc-orchestrator": "docs",
83+
# refactor
84+
"simplify-code": "refactor",
85+
"refactor-plan": "refactor",
86+
# audit
87+
"discovery-sweep": "audit",
88+
"perf-audit": "audit",
89+
"dependency-check": "audit",
90+
# meta
91+
"release-prep": "meta",
92+
"secure-release": "meta",
93+
"orchestrated-health-check": "meta",
94+
"health-check": "meta",
95+
# other
96+
"rag-code-gen": "other",
97+
"research-synthesis": "other",
98+
}
99+
100+
101+
def derive_concern(workflow_name: str) -> Concern:
102+
"""Return the concern bucket for one workflow.
103+
104+
Args:
105+
workflow_name: The workflow's canonical slug (e.g. ``"code-review"``).
106+
107+
Returns:
108+
One of the 7 ``Concern`` values. Unknown workflow names fall
109+
through to ``"other"`` — the catch-all bucket also used for
110+
rag-code-gen and research-synthesis. This is intentional: new
111+
workflows added without an explicit mapping show up in `other`
112+
rather than crashing the route. The drift-guard test
113+
(``test_all_registered_workflows_have_concern``) catches the
114+
missing assignment in CI.
115+
116+
Never raises.
117+
"""
118+
return _WORKFLOW_CONCERNS.get(workflow_name, "other")
119+
120+
121+
def group_by_concern(workflow_names: list[str]) -> dict[Concern, list[str]]:
122+
"""Group a list of workflow names by their concern bucket.
123+
124+
Returns a dict keyed by concern with workflow-name lists as values.
125+
Buckets are populated in ``ALL_CONCERNS`` order; empty buckets are
126+
omitted (callers that want stable ordering should iterate
127+
``ALL_CONCERNS`` and use ``.get(concern, [])``).
128+
129+
Within each bucket the names are sorted alphabetically — matches
130+
D7's sort decision and keeps output deterministic for testing.
131+
"""
132+
by_concern: dict[Concern, list[str]] = {}
133+
for name in workflow_names:
134+
c = derive_concern(name)
135+
by_concern.setdefault(c, []).append(name)
136+
for c in by_concern:
137+
by_concern[c].sort()
138+
return by_concern
139+
140+
141+
def concern_counts(workflow_names: list[str]) -> dict[Concern, int]:
142+
"""Return per-concern counts for a list of workflow names.
143+
144+
All 7 buckets are populated (zero for empty buckets) so the chip
145+
toolbar can render counts without conditional logic. Mirrors the
146+
``/specs`` route's ``bucket_counts`` shape.
147+
"""
148+
counts: dict[Concern, int] = dict.fromkeys(ALL_CONCERNS, 0)
149+
for name in workflow_names:
150+
counts[derive_concern(name)] += 1
151+
return counts

0 commit comments

Comments
 (0)