its-magic 0.1.3-3 → 0.1.3-4

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.
@@ -0,0 +1,318 @@
1
+ """
2
+ Work-kind routing resolver (US-0118 / DEC-0118).
3
+
4
+ Resolves ``(delivery_mode, phase_plan, reason_code)`` from the merged
5
+ scratchpad + story signals using the **L8 precedence chain** (LOCKED):
6
+
7
+ 1. **``start-from=<phase>``** — always wins (intersects with whatever
8
+ plan is active per DEC-0052 §2.5; handled by the caller).
9
+ 2. **Explicit ``DELIVERY_MODE``** (US-0096 / DEC-0082) — argv
10
+ ``delivery-mode=`` > backlog row ``delivery_mode`` (when
11
+ ``AUTO_DELIVERY_ROUTING=backlog_then_scratchpad``) > scratchpad
12
+ ``DELIVERY_MODE``.
13
+ 3. **Explicit ``AUTO_PHASE_*``** (US-0070 / DEC-0052) —
14
+ ``AUTO_PHASE_PLAN`` / ``AUTO_PHASE_EXCLUDE`` / ``AUTO_PHASE_INCLUDE``
15
+ / ``AUTO_PHASE_PROFILE``.
16
+ 4. **``WORK_KIND_ROUTING``-derived ``recommended_delivery_mode``** — only
17
+ when ``WORK_KIND_ROUTING=1`` AND the backlog row carries ``work_kind``
18
+ AND higher-precedence keys are unset.
19
+ 5. **Current default lifecycle** — full DEC-0052 chain; ``standard``
20
+ delivery mode.
21
+
22
+ Zero-overhead-when-off (Q8 LOCKED): when ``WORK_KIND_ROUTING != "1"`` the
23
+ resolver early-returns ``(standard, full_plan, "WORK_KIND_ROUTING_OFF")``
24
+ without invoking :func:`classify_work_kind`. Existing backlog rows
25
+ without ``work_kind``/``recommended_delivery_mode`` route via current
26
+ ``DELIVERY_MODE``/``AUTO_PHASE_*`` precedence (no forced reclassification,
27
+ no schema-migration).
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import os
33
+ import sys
34
+ from typing import List, Optional, Tuple
35
+
36
+ # Path bootstrap so the bare ``import work_kind_classify_lib`` resolves
37
+ # whether the script is run from the repo root or as a file path.
38
+ _SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
39
+ if _SCRIPT_DIR not in sys.path:
40
+ sys.path.insert(0, _SCRIPT_DIR)
41
+
42
+ import work_kind_classify_lib # noqa: E402
43
+ from work_kind_classify_lib import ( # noqa: E402
44
+ CODE_STANDARD_PHASE_PLAN,
45
+ WorkKind,
46
+ WorkKindClassification,
47
+ WORK_KIND_DELIVERY_MODE_CONFLICT,
48
+ WORK_KIND_ROUTING_OFF,
49
+ classify_work_kind,
50
+ )
51
+
52
+
53
+ # Reason code emitted by this resolver when both ``WORK_KIND_ROUTING=1``
54
+ # and explicit ``DELIVERY_MODE`` are set (the explicit value wins, but the
55
+ # conflict is surfaced for diagnostics).
56
+ WORK_KIND_ROUTING_CONFLICT_INFO = "WORK_KIND_DELIVERY_MODE_CONFLICT"
57
+
58
+ # Reason code emitted when the classifier-derived delivery mode is
59
+ # actually applied (info-only).
60
+ WORK_KIND_ROUTING_APPLIED = "WORK_KIND_ROUTING_APPLIED"
61
+
62
+ FULL_STANDARD_PLAN: List[str] = list(CODE_STANDARD_PHASE_PLAN)
63
+
64
+
65
+ def _is_set(value: Optional[str]) -> bool:
66
+ """Return True when ``value`` is a non-empty string after strip."""
67
+ return value is not None and bool(str(value).strip())
68
+
69
+
70
+ def _has_auto_phase_keys(scratchpad: dict) -> bool:
71
+ """Return True when any ``AUTO_PHASE_*`` key is non-empty."""
72
+ for key in (
73
+ "AUTO_PHASE_PLAN",
74
+ "AUTO_PHASE_EXCLUDE",
75
+ "AUTO_PHASE_INCLUDE",
76
+ "AUTO_PHASE_PROFILE",
77
+ ):
78
+ if _is_set(scratchpad.get(key)):
79
+ return True
80
+ return False
81
+
82
+
83
+ def resolve_delivery_mode_with_work_kind(
84
+ scratchpad: dict,
85
+ story_prose: str,
86
+ ac_set: List[str],
87
+ touched_file_hints: List[str],
88
+ component_scope: Optional[str] = None,
89
+ *,
90
+ backlog_work_kind: Optional[str] = None,
91
+ backlog_recommended_delivery_mode: Optional[str] = None,
92
+ has_companion_dec: bool = False,
93
+ ) -> Tuple[str, List[str], Optional[str]]:
94
+ """
95
+ Resolve ``(delivery_mode, phase_plan, reason_code)`` from scratchpad +
96
+ story signals per the L8 precedence chain.
97
+
98
+ Parameters
99
+ ----------
100
+ scratchpad:
101
+ Merged scratchpad dict (Model B: local > baseline > example).
102
+ story_prose, ac_set, touched_file_hints, component_scope:
103
+ Forwarded to :func:`classify_work_kind` when routing is enabled.
104
+ backlog_work_kind, backlog_recommended_delivery_mode:
105
+ Optional backlog-row fields set at intake (AC-4). When present
106
+ and ``WORK_KIND_ROUTING=1`` the classifier is *not* re-run — the
107
+ backlog value is trusted as the operator-accepted recommendation.
108
+ has_companion_dec:
109
+ Forwarded to the classifier (affects ``mini`` → ``mega_quick``
110
+ eligibility).
111
+
112
+ Returns
113
+ -------
114
+ Tuple ``(delivery_mode, phase_plan, reason_code)`` where
115
+ ``reason_code`` is ``None`` on a clean explicit resolution, an
116
+ info-code (``WORK_KIND_ROUTING_OFF`` / ``WORK_KIND_ROUTING_APPLIED``)
117
+ on a derived resolution, or a fail-closed ``WORK_KIND_*`` code on
118
+ conflict.
119
+ """
120
+ routing_flag = (scratchpad.get("WORK_KIND_ROUTING") or "0").strip()
121
+ if routing_flag != "1":
122
+ # Q8 LOCKED zero-overhead-when-off — early return WITHOUT
123
+ # invoking the classifier. Byte-identical to pre-US-0118 behavior.
124
+ return ("standard", list(FULL_STANDARD_PLAN), WORK_KIND_ROUTING_OFF)
125
+
126
+ explicit_delivery_mode = (scratchpad.get("DELIVERY_MODE") or "").strip()
127
+ explicit_auto_phase = _has_auto_phase_keys(scratchpad)
128
+
129
+ # L8 precedence: explicit DELIVERY_MODE wins (2) > AUTO_PHASE_* (3) >
130
+ # WORK_KIND_ROUTING-derived (4) > default (5).
131
+ if _is_set(explicit_delivery_mode):
132
+ # If the classifier would have recommended a different mode, emit
133
+ # the conflict reason code (info for diagnostics; explicit still
134
+ # wins per L8). The classifier is NOT run when the backlog row
135
+ # already carries a recommendation — we compare against that.
136
+ if (
137
+ _is_set(backlog_recommended_delivery_mode)
138
+ and backlog_recommended_delivery_mode != explicit_delivery_mode
139
+ ):
140
+ return (
141
+ explicit_delivery_mode,
142
+ _phase_plan_for_delivery_mode(explicit_delivery_mode),
143
+ WORK_KIND_ROUTING_CONFLICT_INFO,
144
+ )
145
+ return (
146
+ explicit_delivery_mode,
147
+ _phase_plan_for_delivery_mode(explicit_delivery_mode),
148
+ None,
149
+ )
150
+
151
+ if explicit_auto_phase:
152
+ # Explicit AUTO_PHASE_* wins over work-kind-derived (L8 #3 > #4).
153
+ # The caller materializes the actual plan via DEC-0052; we return
154
+ # the standard delivery mode + full plan as the base.
155
+ return ("standard", list(FULL_STANDARD_PLAN), None)
156
+
157
+ # WORK_KIND_ROUTING-derived path (L8 #4). Use the backlog row's
158
+ # accepted recommendation when present; otherwise classify now.
159
+ if _is_set(backlog_recommended_delivery_mode) and _is_set(backlog_work_kind):
160
+ delivery_mode = backlog_recommended_delivery_mode
161
+ phase_plan = _phase_plan_for_delivery_mode(delivery_mode)
162
+ return (delivery_mode, phase_plan, WORK_KIND_ROUTING_APPLIED)
163
+
164
+ try:
165
+ classification: WorkKindClassification = classify_work_kind(
166
+ story_prose=story_prose,
167
+ acceptance_criteria=ac_set,
168
+ touched_file_hints=touched_file_hints,
169
+ component_scope=component_scope,
170
+ has_companion_dec=has_companion_dec,
171
+ )
172
+ except Exception:
173
+ # WORK_KIND_CLASSIFY_FAILED — fail closed to standard lifecycle.
174
+ return ("standard", list(FULL_STANDARD_PLAN), "WORK_KIND_CLASSIFY_FAILED")
175
+
176
+ if not classification.recommended_phase_plan:
177
+ return (
178
+ "standard",
179
+ list(FULL_STANDARD_PLAN),
180
+ "WORK_KIND_PLAN_COVERAGE_MISSING",
181
+ )
182
+
183
+ return (
184
+ classification.recommended_delivery_mode,
185
+ list(classification.recommended_phase_plan),
186
+ WORK_KIND_ROUTING_APPLIED,
187
+ )
188
+
189
+
190
+ def _phase_plan_for_delivery_mode(delivery_mode: str) -> List[str]:
191
+ """Map a delivery mode to its canonical phase plan (DEC-0082 table)."""
192
+ if delivery_mode == "ultra_lean":
193
+ return ["spec", "plan", "build+verify", "ship"]
194
+ if delivery_mode == "mega_quick":
195
+ return ["quick"]
196
+ # standard (or unknown — fall back to standard full lifecycle).
197
+ return list(FULL_STANDARD_PLAN)
198
+
199
+
200
+ # ---------------------------------------------------------------------------
201
+ # Self-test (AC-12)
202
+ # ---------------------------------------------------------------------------
203
+
204
+
205
+ def self_test() -> int:
206
+ """Built-in self-test. Exits 0 on success, 1 on failure."""
207
+ failures: List[str] = []
208
+
209
+ # WORK_KIND_ROUTING=0 → zero overhead, no classifier call.
210
+ mode, plan, reason = resolve_delivery_mode_with_work_kind(
211
+ scratchpad={"WORK_KIND_ROUTING": "0"},
212
+ story_prose="anything",
213
+ ac_set=["AC-1"],
214
+ touched_file_hints=["docs/foo.md"],
215
+ )
216
+ if mode != "standard":
217
+ failures.append(f"routing-off mode: expected standard, got {mode}")
218
+ if reason != WORK_KIND_ROUTING_OFF:
219
+ failures.append(f"routing-off reason: expected {WORK_KIND_ROUTING_OFF}, got {reason}")
220
+ if plan != FULL_STANDARD_PLAN:
221
+ failures.append("routing-off plan: expected full standard plan")
222
+
223
+ # Explicit DELIVERY_MODE wins over WORK_KIND_ROUTING (L8 #2 > #4).
224
+ mode, plan, reason = resolve_delivery_mode_with_work_kind(
225
+ scratchpad={"WORK_KIND_ROUTING": "1", "DELIVERY_MODE": "ultra_lean"},
226
+ story_prose="code feature",
227
+ ac_set=["AC-1", "AC-2", "AC-3", "AC-4"],
228
+ touched_file_hints=["package.json", "src/index.ts"],
229
+ )
230
+ if mode != "ultra_lean":
231
+ failures.append(f"explicit-delivery mode: expected ultra_lean, got {mode}")
232
+ if reason is not None:
233
+ failures.append(f"explicit-delivery reason: expected None, got {reason}")
234
+
235
+ # Conflict — backlog recommends standard, explicit DELIVERY_MODE=ultra_lean.
236
+ mode, plan, reason = resolve_delivery_mode_with_work_kind(
237
+ scratchpad={"WORK_KIND_ROUTING": "1", "DELIVERY_MODE": "ultra_lean"},
238
+ story_prose="code feature",
239
+ ac_set=["AC-1"],
240
+ touched_file_hints=["package.json"],
241
+ backlog_work_kind="code",
242
+ backlog_recommended_delivery_mode="standard",
243
+ )
244
+ if mode != "ultra_lean":
245
+ failures.append(f"conflict mode: expected ultra_lean (explicit wins), got {mode}")
246
+ if reason != WORK_KIND_ROUTING_CONFLICT_INFO:
247
+ failures.append(
248
+ f"conflict reason: expected {WORK_KIND_ROUTING_CONFLICT_INFO}, got {reason}"
249
+ )
250
+
251
+ # AUTO_PHASE_* wins over WORK_KIND_ROUTING (L8 #3 > #4).
252
+ mode, plan, reason = resolve_delivery_mode_with_work_kind(
253
+ scratchpad={"WORK_KIND_ROUTING": "1", "AUTO_PHASE_PLAN": '["spec","plan"]'},
254
+ story_prose="code feature",
255
+ ac_set=["AC-1"],
256
+ touched_file_hints=["package.json"],
257
+ )
258
+ if mode != "standard":
259
+ failures.append(f"auto-phase mode: expected standard, got {mode}")
260
+ if reason is not None:
261
+ failures.append(f"auto-phase reason: expected None, got {reason}")
262
+
263
+ # WORK_KIND_ROUTING=1 + doc story → DOC route (ultra_lean + lean plan).
264
+ mode, plan, reason = resolve_delivery_mode_with_work_kind(
265
+ scratchpad={"WORK_KIND_ROUTING": "1"},
266
+ story_prose="README update",
267
+ ac_set=["AC-1"],
268
+ touched_file_hints=["docs/engineering/runbook.md"],
269
+ )
270
+ if mode != "ultra_lean":
271
+ failures.append(f"doc-derived mode: expected ultra_lean, got {mode}")
272
+ if plan != ["intake", "execute", "release"]:
273
+ failures.append(f"doc-derived plan: expected lean plan, got {plan}")
274
+ if reason != WORK_KIND_ROUTING_APPLIED:
275
+ failures.append(
276
+ f"doc-derived reason: expected {WORK_KIND_ROUTING_APPLIED}, got {reason}"
277
+ )
278
+
279
+ # Backlog-row accepted recommendation is trusted (no re-classify).
280
+ mode, plan, reason = resolve_delivery_mode_with_work_kind(
281
+ scratchpad={"WORK_KIND_ROUTING": "1"},
282
+ story_prose="ignored",
283
+ ac_set=["AC-1"],
284
+ touched_file_hints=["package.json"],
285
+ backlog_work_kind="doc",
286
+ backlog_recommended_delivery_mode="ultra_lean",
287
+ )
288
+ if mode != "ultra_lean":
289
+ failures.append(f"backlog-accept mode: expected ultra_lean, got {mode}")
290
+ if reason != WORK_KIND_ROUTING_APPLIED:
291
+ failures.append(
292
+ f"backlog-accept reason: expected {WORK_KIND_ROUTING_APPLIED}, got {reason}"
293
+ )
294
+
295
+ if failures:
296
+ for f in failures:
297
+ print(f"[WORK_KIND_ROUTING_SELF_TEST_FAIL] {f}", file=sys.stderr)
298
+ return 1
299
+ print("[WORK_KIND_ROUTING_SELF_TEST_OK]")
300
+ return 0
301
+
302
+
303
+ def main(argv: Optional[List[str]] = None) -> int:
304
+ import argparse
305
+
306
+ p = argparse.ArgumentParser(
307
+ description="Work-kind routing resolver (US-0118 / DEC-0118)."
308
+ )
309
+ p.add_argument("--self-test", action="store_true", help="Run built-in self-test.")
310
+ args = p.parse_args(argv)
311
+ if args.self_test:
312
+ return self_test()
313
+ p.print_help()
314
+ return 0
315
+
316
+
317
+ if __name__ == "__main__":
318
+ sys.exit(main())
@@ -0,0 +1,330 @@
1
+ """
2
+ US-0118 contract tests (R-0106 Q4 LOCKED — 12 `test_us0118_*` markers).
3
+
4
+ Covers:
5
+ - Each work-kind classification (DOC / MINI / CODE).
6
+ - Each recommended phase plan.
7
+ - Default-off zero-overhead behavior.
8
+ - L8 precedence vs explicit DELIVERY_MODE / AUTO_PHASE_*.
9
+ - Operator override path (backlog-row accepted recommendation).
10
+ - Each fail-closed reason code in the WORK_KIND_* family.
11
+ - --explain rule_trace emission.
12
+ - classify_touched_files reuse boundary (Q9 LOCKED import contract).
13
+
14
+ Pure stdlib pytest — no external deps. Active + `template/` parity.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ import pytest
24
+
25
+ _REPO_ROOT = Path(__file__).resolve().parent.parent
26
+ _SCRIPTS_DIR = _REPO_ROOT / "scripts"
27
+ if str(_SCRIPTS_DIR) not in sys.path:
28
+ sys.path.insert(0, str(_SCRIPTS_DIR))
29
+
30
+ import work_kind_classify_lib as wkc # noqa: E402
31
+ import work_kind_routing_lib as wkr # noqa: E402
32
+ from work_kind_classify_lib import ( # noqa: E402
33
+ WorkKind,
34
+ classify_work_kind,
35
+ )
36
+ from work_kind_routing_lib import resolve_delivery_mode_with_work_kind # noqa: E402
37
+
38
+ # Also import dev_environment_lib to verify the reuse boundary.
39
+ import dev_environment_lib # noqa: E402
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # AC-1, AC-2 — work-kind classification + phase plans
44
+ # ---------------------------------------------------------------------------
45
+
46
+
47
+ def test_us0118_doc_kind_routes_to_lean_plan():
48
+ """DOC kind → `[intake, execute, release]` (skip discovery/research/
49
+ architecture/sprint-plan/plan-verify/qa/verify-work)."""
50
+ result = classify_work_kind(
51
+ story_prose="Update README",
52
+ acceptance_criteria=["AC-1 README updated"],
53
+ touched_file_hints=["docs/engineering/runbook.md", "docs/product/backlog.md"],
54
+ )
55
+ assert result.work_kind is WorkKind.DOC
56
+ assert result.recommended_phase_plan == ["intake", "execute", "release"]
57
+ assert result.recommended_delivery_mode == "ultra_lean"
58
+
59
+
60
+ def test_us0118_mini_kind_routes_to_ultra_lean():
61
+ """MINI kind → ultra_lean when mega_quick ineligible (AC count > 3)."""
62
+ result = classify_work_kind(
63
+ story_prose="Tweak nginx config",
64
+ acceptance_criteria=["AC-1", "AC-2", "AC-3", "AC-4"], # 4 > 3
65
+ touched_file_hints=[".env.example"],
66
+ component_scope="web",
67
+ )
68
+ assert result.work_kind is WorkKind.MINI
69
+ assert result.recommended_delivery_mode == "ultra_lean"
70
+ assert result.recommended_phase_plan == ["spec", "plan", "build+verify", "ship"]
71
+
72
+
73
+ def test_us0118_mini_kind_routes_to_mega_quick_when_eligible():
74
+ """MINI kind + US-0096 eligibility (≤3 ACs, single component, no DEC)
75
+ → mega_quick."""
76
+ result = classify_work_kind(
77
+ story_prose="Tiny fix",
78
+ acceptance_criteria=["AC-1", "AC-2"], # 2 ≤ 3
79
+ touched_file_hints=[".env.example"],
80
+ component_scope="web",
81
+ has_companion_dec=False,
82
+ )
83
+ assert result.work_kind is WorkKind.MINI
84
+ assert result.recommended_delivery_mode == "mega_quick"
85
+ assert result.recommended_phase_plan == ["quick"]
86
+
87
+
88
+ def test_us0118_code_kind_routes_to_standard():
89
+ """CODE kind (tier A — package.json, or any non-skip source path) →
90
+ standard full lifecycle."""
91
+ result = classify_work_kind(
92
+ story_prose="New feature",
93
+ acceptance_criteria=["AC-1", "AC-2", "AC-3", "AC-4"],
94
+ touched_file_hints=["package.json", "src/index.ts"],
95
+ )
96
+ assert result.work_kind is WorkKind.CODE
97
+ assert result.recommended_delivery_mode == "standard"
98
+ assert result.recommended_phase_plan == [
99
+ "intake",
100
+ "discovery",
101
+ "research",
102
+ "architecture",
103
+ "sprint-plan",
104
+ "plan-verify",
105
+ "execute",
106
+ "qa",
107
+ "verify-work",
108
+ "release",
109
+ "refresh-context",
110
+ ]
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # AC-6 — L8 precedence chain
115
+ # ---------------------------------------------------------------------------
116
+
117
+
118
+ def test_us0118_explicit_delivery_mode_wins_over_work_kind():
119
+ """L8 precedence: explicit DELIVERY_MODE wins over WORK_KIND_ROUTING-
120
+ derived recommendation even when the story is CODE."""
121
+ mode, plan, reason = resolve_delivery_mode_with_work_kind(
122
+ scratchpad={"WORK_KIND_ROUTING": "1", "DELIVERY_MODE": "ultra_lean"},
123
+ story_prose="code feature",
124
+ ac_set=["AC-1", "AC-2", "AC-3", "AC-4"],
125
+ touched_file_hints=["package.json", "src/index.ts"],
126
+ )
127
+ assert mode == "ultra_lean"
128
+ # Explicit resolution → reason is None (no conflict because no
129
+ # backlog recommendation was supplied to compare against).
130
+ assert reason is None
131
+ assert plan == ["spec", "plan", "build+verify", "ship"]
132
+
133
+
134
+ def test_us0118_auto_phase_wins_over_work_kind():
135
+ """L8 precedence: explicit AUTO_PHASE_* wins over WORK_KIND_ROUTING-
136
+ derived recommendation."""
137
+ mode, plan, reason = resolve_delivery_mode_with_work_kind(
138
+ scratchpad={
139
+ "WORK_KIND_ROUTING": "1",
140
+ "AUTO_PHASE_PLAN": '["spec","plan"]',
141
+ },
142
+ story_prose="code feature",
143
+ ac_set=["AC-1"],
144
+ touched_file_hints=["package.json"],
145
+ )
146
+ assert mode == "standard"
147
+ # Explicit AUTO_PHASE_* resolution → reason is None.
148
+ assert reason is None
149
+
150
+
151
+ # ---------------------------------------------------------------------------
152
+ # AC-3, AC-8 — default-off zero-overhead + backward compat
153
+ # ---------------------------------------------------------------------------
154
+
155
+
156
+ def test_us0118_routing_off_is_noop():
157
+ """WORK_KIND_ROUTING=0 → zero overhead: returns standard + full plan +
158
+ WORK_KIND_ROUTING_OFF; classifier NOT invoked."""
159
+ mode, plan, reason = resolve_delivery_mode_with_work_kind(
160
+ scratchpad={"WORK_KIND_ROUTING": "0"},
161
+ story_prose="anything",
162
+ ac_set=["AC-1"],
163
+ touched_file_hints=["docs/foo.md"],
164
+ )
165
+ assert mode == "standard"
166
+ assert reason == wkc.WORK_KIND_ROUTING_OFF
167
+ assert plan == wkr.FULL_STANDARD_PLAN
168
+
169
+
170
+ def test_us0118_default_off_zero_overhead():
171
+ """WORK_KIND_ROUTING=0 behavior byte-identical to pre-US-0118
172
+ (standard delivery mode + full canonical phase plan)."""
173
+ # Absent key → treated as "0" (default-off).
174
+ mode1, plan1, reason1 = resolve_delivery_mode_with_work_kind(
175
+ scratchpad={},
176
+ story_prose="anything",
177
+ ac_set=["AC-1"],
178
+ touched_file_hints=["docs/foo.md"],
179
+ )
180
+ mode2, plan2, reason2 = resolve_delivery_mode_with_work_kind(
181
+ scratchpad={"WORK_KIND_ROUTING": "0"},
182
+ story_prose="different",
183
+ ac_set=["AC-99"],
184
+ touched_file_hints=["src/x.ts"],
185
+ )
186
+ assert (mode1, plan1, reason1) == (mode2, plan2, reason2)
187
+ assert mode1 == "standard"
188
+ assert reason1 == wkc.WORK_KIND_ROUTING_OFF
189
+
190
+
191
+ # ---------------------------------------------------------------------------
192
+ # AC-8 — reuse boundary (Q9 LOCKED import contract)
193
+ # ---------------------------------------------------------------------------
194
+
195
+
196
+ def test_us0118_classify_touched_files_reuse():
197
+ """classify_touched_files imported from dev_environment_lib — NOT
198
+ reimplemented in work_kind_classify_lib (Q9 LOCKED)."""
199
+ # The classifier module must reference the same function object.
200
+ assert wkc.classify_touched_files is dev_environment_lib.classify_touched_files
201
+ assert wkc.TIER_C_SKIP_PREFIXES is dev_environment_lib.TIER_C_SKIP_PREFIXES
202
+ # And the classifier produces tier-consistent results.
203
+ assert wkc.classify_touched_files(["package.json"]) == "A"
204
+ assert wkc.classify_touched_files([".env.example"]) == "B"
205
+ assert wkc.classify_touched_files(["docs/foo.md"]) is None
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # AC-5, AC-9 — intake evidence schema extension
210
+ # ---------------------------------------------------------------------------
211
+
212
+
213
+ def test_us0118_intake_evidence_records_work_kind():
214
+ """The classifier output carries the intake-evidence schema extension
215
+ fields (work_kind, recommended_delivery_mode, work_kind_operator_decision)
216
+ so /intake step 5 can persist them directly."""
217
+ result = classify_work_kind(
218
+ story_prose="README update",
219
+ acceptance_criteria=["AC-1"],
220
+ touched_file_hints=["docs/foo.md"],
221
+ )
222
+ out = result.as_dict()
223
+ # work_kind is a string in the serialized form.
224
+ assert out["work_kind"] == "doc"
225
+ assert out["recommended_delivery_mode"] in ("standard", "ultra_lean", "mega_quick")
226
+ # work_kind_operator_decision defaults to None until the operator
227
+ # accept/override gate records a value.
228
+ assert "work_kind_operator_decision" in out
229
+
230
+
231
+ # ---------------------------------------------------------------------------
232
+ # AC-7 — fail-closed reason codes (R-0106 Q2 LOCKED)
233
+ # ---------------------------------------------------------------------------
234
+
235
+
236
+ def test_us0118_reason_codes_preserved():
237
+ """All WORK_KIND_* reason codes are emitted by the appropriate failure
238
+ modes (R-0106 Q2 LOCKED)."""
239
+ # WORK_KIND_ROUTING_OFF — info-only when flag off.
240
+ _, _, reason = resolve_delivery_mode_with_work_kind(
241
+ scratchpad={"WORK_KIND_ROUTING": "0"},
242
+ story_prose="x",
243
+ ac_set=["AC-1"],
244
+ touched_file_hints=["docs/foo.md"],
245
+ )
246
+ assert reason == wkc.WORK_KIND_ROUTING_OFF
247
+
248
+ # WORK_KIND_DELIVERY_MODE_CONFLICT — explicit DELIVERY_MODE conflicts
249
+ # with backlog-row recommendation.
250
+ _, _, reason = resolve_delivery_mode_with_work_kind(
251
+ scratchpad={"WORK_KIND_ROUTING": "1", "DELIVERY_MODE": "ultra_lean"},
252
+ story_prose="x",
253
+ ac_set=["AC-1"],
254
+ touched_file_hints=["package.json"],
255
+ backlog_work_kind="code",
256
+ backlog_recommended_delivery_mode="standard",
257
+ )
258
+ assert reason == wkc.WORK_KIND_DELIVERY_MODE_CONFLICT
259
+
260
+ # WORK_KIND_CLASSIFY_FAILED — classifier raises → fail-closed standard.
261
+ _, _, reason = resolve_delivery_mode_with_work_kind(
262
+ scratchpad={"WORK_KIND_ROUTING": "1"},
263
+ story_prose="x",
264
+ ac_set=["AC-1"],
265
+ touched_file_hints=None, # type: ignore[arg-type] # provokes TypeError
266
+ )
267
+ assert reason == "WORK_KIND_CLASSIFY_FAILED"
268
+
269
+ # All reason codes are present in the WORK_KIND_REASON_CODES tuple.
270
+ expected = {
271
+ "WORK_KIND_ROUTING_OFF",
272
+ "WORK_KIND_DELIVERY_MODE_CONFLICT",
273
+ "WORK_KIND_CLASSIFY_FAILED",
274
+ "WORK_KIND_UNKNOWN_ROUTE",
275
+ "WORK_KIND_PLAN_COVERAGE_MISSING",
276
+ "WORK_KIND_TIE_BREAK_APPLIED",
277
+ }
278
+ assert set(wkc.WORK_KIND_REASON_CODES) == expected
279
+ # Every reason code has remediation guidance.
280
+ for code in wkc.WORK_KIND_REASON_CODES:
281
+ assert code in wkc.REASON_CODE_REMEDIATION
282
+ assert wkc.REASON_CODE_REMEDIATION[code]
283
+
284
+
285
+ # ---------------------------------------------------------------------------
286
+ # AC-1 — --explain flag emits rule_trace (Q3 LOCKED)
287
+ # ---------------------------------------------------------------------------
288
+
289
+
290
+ def test_us0118_explain_emits_rule_trace():
291
+ """--explain flag (explain=True) emits a non-empty rule_trace list of
292
+ (rule_id, matched_signal, contribution) tuples."""
293
+ result = classify_work_kind(
294
+ story_prose="code feature",
295
+ acceptance_criteria=["AC-1"],
296
+ touched_file_hints=["package.json"],
297
+ explain=True,
298
+ )
299
+ assert result.rule_trace
300
+ for entry in result.rule_trace:
301
+ assert isinstance(entry, tuple)
302
+ assert len(entry) == 3
303
+ rule_id, matched_signal, contribution = entry
304
+ assert isinstance(rule_id, str) and rule_id
305
+ assert isinstance(matched_signal, str) and matched_signal
306
+ assert isinstance(contribution, str) and contribution
307
+
308
+ # Without explain, rule_trace is empty.
309
+ no_explain = classify_work_kind(
310
+ story_prose="code feature",
311
+ acceptance_criteria=["AC-1"],
312
+ touched_file_hints=["package.json"],
313
+ )
314
+ assert no_explain.rule_trace == []
315
+
316
+
317
+ # ---------------------------------------------------------------------------
318
+ # Q1 LOCKED — tie-break (highest tier wins)
319
+ # ---------------------------------------------------------------------------
320
+
321
+
322
+ def test_us0118_tie_break_code_wins():
323
+ """Story touching both docs/ and src/ (mixed tier) → CODE (highest
324
+ tier wins per Q1 LOCKED)."""
325
+ result = classify_work_kind(
326
+ story_prose="Feature + docs",
327
+ acceptance_criteria=["AC-1"],
328
+ touched_file_hints=["docs/foo.md", "package.json"],
329
+ )
330
+ assert result.work_kind is WorkKind.CODE