@windyroad/itil 0.28.0 → 0.28.1-preview.306
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.
package/package.json
CHANGED
|
@@ -23,7 +23,7 @@ This skill is the foreground-lightweight-capture variant of `/wr-itil:manage-pro
|
|
|
23
23
|
|
|
24
24
|
## Rule 6 audit (per ADR-032 + ADR-013)
|
|
25
25
|
|
|
26
|
-
This skill has **one classification-only AskUserQuestion (type-tag,
|
|
26
|
+
This skill has **at most one classification-only AskUserQuestion (type-tag, ambiguous-signal fallback only) and zero control-flow branches keyed on the answer**. Each potentially-interactive decision is framework-mediated per ADR-044:
|
|
27
27
|
|
|
28
28
|
| Decision | Resolution |
|
|
29
29
|
|----------|-----------|
|
|
@@ -32,9 +32,9 @@ This skill has **one classification-only AskUserQuestion (type-tag, taste author
|
|
|
32
32
|
| Effort default | Framework-policy: `M` flagged "deferred — re-rate at next /wr-itil:review-problems". |
|
|
33
33
|
| Multi-concern split | Out of scope: capture-problem creates one ticket per invocation. Multi-concern observations route to `/wr-itil:manage-problem` (its Step 4b owns the split). |
|
|
34
34
|
| Empty `$ARGUMENTS` | Halt-with-stderr-directive: print "capture-problem requires a description in $ARGUMENTS — invoke /wr-itil:manage-problem instead for the full intake flow" and exit. AFK orchestrators MUST NOT invoke capture-problem with empty arguments — caller-side contract. |
|
|
35
|
-
| Type classification (P170 / ADR-060 item 8c) |
|
|
35
|
+
| Type classification (P170 / ADR-060 item 8c; P185 derive-first refactor) | **Derive-first; silent-framework per ADR-044 category 4 on unambiguous-signal descriptions; taste fallback per category 5 on ambiguous descriptions only.** Step 1.5 runs a lexical-signal classifier against the description text. Unambiguous one-sided signal → classify silently + emit stderr advisory. Mixed or zero signals → AskUserQuestion. `--type=<value>` pre-resolves silently (highest priority). `--no-prompt` defaults to `technical` silently (AFK contract). Maintainer-side ONLY: this dispatch is paired with JTBD-301 protection — `.github/ISSUE_TEMPLATE/problem-report.yml` (plugin-user-side intake) MUST NOT carry an equivalent type selector; triage assigns the type during `/wr-itil:manage-problem` ingestion of user-reported issues. **The classifier is also NOT invoked from `/wr-itil:manage-problem`'s ingestion-of-plugin-user-reports path** — plugin-user descriptions do not carry the same authorial intent as maintainer-internal captures, so triage stays user-judgement per JTBD-301. **I2 invariant** (ADR-060 line 98): the prompt is a classification facet, not a workflow split — Steps 0-7 control-flow is identical regardless of the chosen `type_value`; only the substituted value in the Step 4 skeleton template differs. The stderr advisory text shape is also I2-isomorphic: identical sentence structure across `technical` vs `user-business` classifications beyond the substituted values + signal names. |
|
|
36
36
|
|
|
37
|
-
Per ADR-013 Rule 6 fail-safe: every decision above resolves without interactive user input in non-interactive contexts (the type-tag carve-out resolves to `technical` via `--no-prompt` or `--type=` caller-side pre-resolution). AFK orchestrators MUST pass `--no-prompt` or `--type=<value>` per JTBD-006 § Persona Constraints; AFK callers that omit both flags violate the caller-side contract. Interactive and pre-resolved AFK paths produce identical observable outputs except for the `**Type**:` field value, satisfying the I2 invariant by construction.
|
|
37
|
+
Per ADR-013 Rule 6 fail-safe: every decision above resolves without interactive user input in non-interactive contexts (the type-tag carve-out resolves to `technical` via `--no-prompt` or `--type=` caller-side pre-resolution, or via the derive-first classifier on unambiguous descriptions). AFK orchestrators MUST pass `--no-prompt` or `--type=<value>` per JTBD-006 § Persona Constraints; AFK callers that omit both flags violate the caller-side contract. Interactive (pre-resolved, derive-classified, or ambiguous-fallback) and pre-resolved AFK paths produce identical observable outputs except for the `**Type**:` field value, satisfying the I2 invariant by construction.
|
|
38
38
|
|
|
39
39
|
## Steps
|
|
40
40
|
|
|
@@ -61,9 +61,9 @@ fi
|
|
|
61
61
|
|
|
62
62
|
| Flag | Effect on Step 1.5 |
|
|
63
63
|
|------|-------------------|
|
|
64
|
-
| `--type=technical` | Pre-resolves type to `technical`; Step 1.5 skips the AskUserQuestion. |
|
|
65
|
-
| `--type=user-business` | Pre-resolves type to `user-business`; Step 1.5 skips the AskUserQuestion. |
|
|
66
|
-
| `--no-prompt` | Pre-resolves type to `technical` (default); Step 1.5 skips the AskUserQuestion. |
|
|
64
|
+
| `--type=technical` | Pre-resolves type to `technical`; Step 1.5 skips the classifier and the AskUserQuestion. |
|
|
65
|
+
| `--type=user-business` | Pre-resolves type to `user-business`; Step 1.5 skips the classifier and the AskUserQuestion. |
|
|
66
|
+
| `--no-prompt` | Pre-resolves type to `technical` (default); Step 1.5 skips the classifier and the AskUserQuestion. |
|
|
67
67
|
|
|
68
68
|
Strip recognised leading flags from `$ARGUMENTS`; the remainder (after flags) is the free-text description. If both `--type=<value>` and `--no-prompt` are present, `--type=<value>` wins (more specific). Unknown leading flags halt-with-stderr-directive: print "capture-problem: unknown flag '<flag>' — recognised flags: --type=technical, --type=user-business, --no-prompt" and exit.
|
|
69
69
|
|
|
@@ -71,19 +71,45 @@ Empty description (post-flag-strip) halts per the Rule 6 audit above.
|
|
|
71
71
|
|
|
72
72
|
Derive a kebab-case title slug from the first 8-10 non-stopword tokens of the description (matching the existing `manage-problem` slug derivation pattern).
|
|
73
73
|
|
|
74
|
-
### 1.5 Type classification (
|
|
74
|
+
### 1.5 Type classification (derive-first; silent-framework per ADR-044 category 4; taste fallback per category 5 on ambiguity)
|
|
75
75
|
|
|
76
|
-
Resolve `type_value` ∈ {`technical`, `user-business`} per the following framework-mediated dispatch
|
|
76
|
+
Resolve `type_value` ∈ {`technical`, `user-business`} per the following framework-mediated dispatch. **The dispatch order is load-bearing** — pre-resolution flags short-circuit BEFORE the classifier runs, and the AskUserQuestion fires ONLY on genuinely-ambiguous descriptions.
|
|
77
77
|
|
|
78
|
-
1. **If `--type=<value>` was set in Step 1**: use that value; do NOT fire AskUserQuestion (silent-proceed per ADR-013 Rule 5).
|
|
79
|
-
2. **Else if `--no-prompt` was set in Step 1**: default `type_value = technical`; do NOT fire AskUserQuestion. JTBD-006 protection: AFK orchestrators MUST pass this flag (or `--type=<value>`).
|
|
80
|
-
3. **Else** (interactive context, no caller-side pre-resolution):
|
|
81
|
-
- `technical` — *"Bug, defect, broken behaviour, framework drift — root cause sits in code or process."*
|
|
82
|
-
- `user-business` — *"Missing capability, UX gap, adopter friction, JTBD-shaped need — root cause sits in unmet user need."*
|
|
78
|
+
1. **If `--type=<value>` was set in Step 1**: use that value; do NOT run the classifier; do NOT fire AskUserQuestion (silent-proceed per ADR-013 Rule 5).
|
|
79
|
+
2. **Else if `--no-prompt` was set in Step 1**: default `type_value = technical`; do NOT run the classifier; do NOT fire AskUserQuestion. JTBD-006 protection: AFK orchestrators MUST pass this flag (or `--type=<value>`).
|
|
80
|
+
3. **Else** (interactive context, no caller-side pre-resolution): run the **lexical-signal classifier** against the description text:
|
|
83
81
|
|
|
84
|
-
**
|
|
82
|
+
**Technical signals** (regex; matched against the post-flag-strip description, case-insensitive unless noted):
|
|
83
|
+
- Code identifiers: `[a-z]+[A-Z][a-zA-Z]+` (camelCase), `[a-z]+-[a-z][a-z-]+` (kebab-case identifiers with ≥2 hyphens), `[a-z]+_[a-z][a-z_]+` (snake_case).
|
|
84
|
+
- File paths: `\.(md|sh|bats|ts|js|json|yaml|yml|py|rb|go|css|html)\b`, `packages/[a-z-]+/`, `docs/[a-z-]+/`, `\.github/`, `\.claude/`, `/tmp/`.
|
|
85
|
+
- Command-name patterns: `/wr-[a-z-]+:[a-z-]+\b`, `\bgit (commit|push|mv|add|rebase|merge)\b`, `\bnpm (run|install|publish)\b`, `\bbash\b`, `\bbats\b`, `\bgrep\b`, `\bsed\b`, `\bjq\b`.
|
|
86
|
+
- Mechanism words: `\b(drift|regression|hook|marker|gate|refresh|idempotent|exit code|stderr|stdout|regex|formula|dispatch|frontmatter|substring|escape|sentinel|bypass|TTL|cache|invalidate|deduplicate|race|deadlock|timeout|preflight)\b`.
|
|
87
|
+
- Error-message patterns: `\b(error|failure|exception|panic|stack trace|segfault|null pointer|undefined|not found|permission denied|EACCES|ENOENT|exit \d+)\b`.
|
|
85
88
|
|
|
86
|
-
**
|
|
89
|
+
**User-business signals** (regex; same casing):
|
|
90
|
+
- Persona names: `\b(adopter|adopters|plugin-user|plugin-users|solo[-_ ]?developer|maintainer-persona|end[-_ ]?user|customer|stakeholder)\b`.
|
|
91
|
+
- Journey words: `\b(workflow|journey|onboarding|friction|UX|experience|usability|discoverability|cognitive load|attention|interrupt|context-switch)\b`.
|
|
92
|
+
- JTBD-shaped need words: `\bJTBD-\d+\b`, `\bjob-to-be-done\b`, `\b(want|need|can't|cannot|blocked from|unable to|hard to|painful to)\b\s+(use|access|find|discover|complete)`, `\bdesired outcome\b`, `\bunmet need\b`.
|
|
93
|
+
|
|
94
|
+
**Decision rule**:
|
|
95
|
+
|
|
96
|
+
- **Unambiguous technical** (≥1 technical signal AND 0 user-business signals): set `type_value = technical`; emit stderr advisory and proceed. Do NOT fire AskUserQuestion.
|
|
97
|
+
- **Unambiguous user-business** (0 technical signals AND ≥1 user-business signal): set `type_value = user-business`; emit stderr advisory and proceed. Do NOT fire AskUserQuestion.
|
|
98
|
+
- **Ambiguous** (≥1 signal each side, OR 0 signals on both sides): fire AskUserQuestion with options `technical` (default) and `user-business`. Question text: *"What type of problem is this?"* Per-option descriptions:
|
|
99
|
+
- `technical` — *"Bug, defect, broken behaviour, framework drift — root cause sits in code or process."*
|
|
100
|
+
- `user-business` — *"Missing capability, UX gap, adopter friction, JTBD-shaped need — root cause sits in unmet user need."*
|
|
101
|
+
|
|
102
|
+
**Stderr advisory contract** (silent-classification path only): emit a SINGLE line to stderr (NOT stdout, NOT in the ticket body) of the form:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
capture-problem: classified type=<value> from description signals: <signal1>, <signal2>[, ...]; re-invoke with --type=<other-value> to override
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The advisory text shape is I2-isomorphic — the sentence structure (`classified type=<value> from description signals: ...; re-invoke with --type=<other-value> to override`) is identical regardless of which type was classified; only the substituted `<value>` / `<other-value>` / `<signal*>` tokens differ. Embedding the advisory in stdout would risk machine-readers parsing it as a ticket-body line; embedding it in the ticket body would violate ADR-060's frontmatter / body-bullet schema. Stderr is the correct channel — visible to interactive maintainers in the terminal; invisible to ticket consumers; loggable by AFK orchestrators that capture subprocess stderr.
|
|
109
|
+
|
|
110
|
+
**I2 invariant guard (ADR-060 line 98)**: the resolved `type_value` is used at Step 4 ONLY as a substituted string in the skeleton template's `**Type**:` body field. Steps 2, 3, 4 (other than the `**Type**:` substitution), 5, 6, 7 execute identically regardless of `type_value`. The skill carries NO control-flow branch keyed on `type` — that would convert classification into a workflow split and violate I2. The lexical-signal classifier is UPSTREAM of the value's substitution (it resolves WHICH value to substitute, not WHICH workflow to execute); the substitution and all downstream steps remain uniform. Pure-bash supporting-script enforcement of this invariant lives in `packages/itil/scripts/test/i2-no-type-branching.bats`; the SKILL.md surface coverage gap is named at P176 (descendant of P012 master harness).
|
|
111
|
+
|
|
112
|
+
**JTBD-301 scope guard**: this dispatch fires on the maintainer-side `/wr-itil:capture-problem` skill only. The plugin-user-side intake (`.github/ISSUE_TEMPLATE/problem-report.yml`) MUST NOT carry an equivalent type selector — plugin-user persona constraint is "no pre-classification". Triage assigns `type` during `/wr-itil:manage-problem` ingestion of user-reported issues, not at user-report time. **The lexical-signal classifier is ALSO NOT invoked from `/wr-itil:manage-problem`'s ingestion-of-plugin-user-reports path** — plugin-user descriptions do not carry the same authorial intent as maintainer-internal captures (a plugin-user describing their friction in maintainer-vocabulary terms would mis-classify); triage stays user-judgement, not lexical-classifier inference.
|
|
87
113
|
|
|
88
114
|
### 2. Minimal-grep duplicate check (3-keyword title-only)
|
|
89
115
|
|
|
@@ -226,9 +252,9 @@ The trailing pointer is **not optional** — it is the user-visible signal that
|
|
|
226
252
|
|---------|----------------|-----------------|
|
|
227
253
|
| Duplicate-check | Wide-net grep + AskUserQuestion branch on matches | 3-keyword title-only grep, list-only (no branch) |
|
|
228
254
|
| Multi-concern split | Step 4b AskUserQuestion | Out of scope (one ticket per invocation) |
|
|
229
|
-
| Skeleton-fill | Full-intake; AskUserQuestion for missing fields | Deferred-placeholder pattern +
|
|
230
|
-
| Type-tag prompt | Step 4-equivalent AskUserQuestion fires alongside other intake fields | Step 1.5 classification-only AskUserQuestion
|
|
231
|
-
| AskUserQuestion authority | Multiple branches (deviation-approval / direction-setting / taste / mechanical) |
|
|
255
|
+
| Skeleton-fill | Full-intake; AskUserQuestion for missing fields | Deferred-placeholder pattern + derive-first type classification (AskUserQuestion fires only on ambiguous descriptions) |
|
|
256
|
+
| Type-tag prompt | Step 4-equivalent AskUserQuestion fires alongside other intake fields | Step 1.5 derive-first dispatch — lexical-signal classifier silently resolves unambiguous descriptions (with stderr advisory); ambiguous descriptions fall back to classification-only AskUserQuestion. `--type=` and `--no-prompt` flags pre-resolve for non-interactive callers. I2 invariant: no control-flow branch keyed on type |
|
|
257
|
+
| AskUserQuestion authority | Multiple branches (deviation-approval / direction-setting / taste / mechanical) | Zero unconditional AskUserQuestion fires; ambiguous-signal fallback only (silent-framework per ADR-044 cat. 4 on unambiguous; taste per cat. 5 on ambiguous); zero control-flow branches |
|
|
232
258
|
| README refresh | P094 inline (regenerate + stage in same commit) | Deferred to next `/wr-itil:review-problems` |
|
|
233
259
|
| Status transitions | Step 7 owns Open → Known Error → Verifying → Closed | Out of scope (creation only) |
|
|
234
260
|
| Commit grain | One commit per intake (or per split-concern set) | One commit per capture |
|
|
@@ -246,7 +272,8 @@ The two skills share the `/tmp/manage-problem-grep-${SESSION_ID}` create-gate ma
|
|
|
246
272
|
- **P176** — agent-side I2 (no type-branching) coverage gap on the SKILL.md surface (this file's surface); descendant of P012 master harness ticket. The Step 1.5 I2 invariant guard is enforced by audit-trailed prose here per ADR-052 § Surface 2 escape-hatch contract; behavioural enforcement awaits the master harness.
|
|
247
273
|
- **ADR-032** (`docs/decisions/032-governance-skill-invocation-patterns.proposed.md`) — foreground-lightweight-capture variant amendment.
|
|
248
274
|
- **ADR-038** — progressive-disclosure pattern (SKILL.md + REFERENCE.md split).
|
|
249
|
-
- **ADR-044** — decision-delegation contract; type classification is
|
|
275
|
+
- **ADR-044** — decision-delegation contract; type classification is **derive-first**: silent-framework per category 4 on unambiguous-signal descriptions (the classifier IS the framework resolving the answer from observable evidence per ADR-026 grounding); taste per category 5 fallback on genuinely-ambiguous descriptions only. `--no-prompt` / `--type=<value>` are policy-authorised silent-proceed shapes per category 4 (caller-side pre-resolution). P185 re-classified Step 1.5's taxonomy position from "cat 5 unconditional ask" to "cat 4 derive-first with cat 5 fallback".
|
|
276
|
+
- **P185** — `/wr-itil:capture-problem` asks a classification question it can answer itself from the description's observable evidence — inverse-P078 / P132 trap at a SKILL contract surface. The Step 1.5 derive-first refactor (lexical-signal classifier + stderr advisory) ships this fix.
|
|
250
277
|
- **ADR-049** — bin/ on PATH; capture-problem reuses the existing `wr-itil-reconcile-readme` shim.
|
|
251
278
|
- **ADR-052** — behavioural-tests-default for skill testing; SKILL.md I2 surface coverage gap is named, not silent (P176 + ADR-052 § Surface 2).
|
|
252
279
|
- **ADR-060** (`docs/decisions/060-...accepted.md`) — Phase 1 item 8c authored Step 1.5 here; I2 invariant (line 98) governs the no-control-flow-branch contract; line 132 names the maintainer-side-only / JTBD-301-protection scope; line 160 (Confirmation criterion 4) gates the type-prompt placement.
|
|
@@ -317,3 +317,262 @@ EOF
|
|
|
317
317
|
run grep -F '/wr-itil:review-problems' "$SKILL_FILE"
|
|
318
318
|
[ "$status" -eq 0 ]
|
|
319
319
|
}
|
|
320
|
+
|
|
321
|
+
# ---------------------------------------------------------------------------
|
|
322
|
+
# P185 — Step 1.5 derive-first classifier behavioural tests.
|
|
323
|
+
#
|
|
324
|
+
# The classifier is an agent-driven SKILL.md instruction (not a pure-bash
|
|
325
|
+
# script), so these tests mirror the lexical-signal regex sets from
|
|
326
|
+
# SKILL.md and assert classification outcomes on fixture descriptions —
|
|
327
|
+
# same shape as the existing next-ID-formula and duplicate-grep tests
|
|
328
|
+
# that mirror manage-problem Step 3 / capture-problem Step 2 formulas.
|
|
329
|
+
#
|
|
330
|
+
# Per ADR-052 (behavioural-tests-default): observable input → output
|
|
331
|
+
# assertions on the classifier's resolution function. Per P081: NOT
|
|
332
|
+
# grepping SKILL.md prose for signal-token references — that would be
|
|
333
|
+
# the structural-test-disguised-as-behavioural anti-pattern.
|
|
334
|
+
#
|
|
335
|
+
# I2 protection: each test exercises both `technical` and `user-business`
|
|
336
|
+
# classification paths and asserts the stderr-advisory shape is
|
|
337
|
+
# isomorphic beyond the substituted type-value + signal names.
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
# Mirror of the SKILL.md Step 1.5 classifier — emits "technical",
|
|
341
|
+
# "user-business", or "ambiguous" on stdout. Always returns 0 so the
|
|
342
|
+
# `result=$(classify_description ...)` substitution in tests below
|
|
343
|
+
# never trips bats' default-fail-on-nonzero-substitution behaviour;
|
|
344
|
+
# tests assert on the string verdict, not the exit code.
|
|
345
|
+
#
|
|
346
|
+
# This helper mirrors the SKILL.md regex set so the test exercises the
|
|
347
|
+
# classifier's load-bearing pattern set; drift between this helper and
|
|
348
|
+
# SKILL.md is the failure mode this test corpus catches.
|
|
349
|
+
classify_description() {
|
|
350
|
+
local desc="$1"
|
|
351
|
+
local tech_signals=()
|
|
352
|
+
local ub_signals=()
|
|
353
|
+
|
|
354
|
+
# Technical signals — case-insensitive matching via grep -E -o -i.
|
|
355
|
+
# Each match contributes the matched token to the signal list.
|
|
356
|
+
local tech_patterns=(
|
|
357
|
+
'[a-z]+[A-Z][a-zA-Z]+' # camelCase (case-SENSITIVE - the case shape is the signal)
|
|
358
|
+
'[a-z]+-[a-z][a-z-]+-[a-z][a-z-]+' # kebab-case with >=2 hyphens
|
|
359
|
+
'[a-z]+_[a-z][a-z_]+' # snake_case
|
|
360
|
+
'\.(md|sh|bats|ts|js|json|yaml|yml|py|rb|go|css|html)\b'
|
|
361
|
+
'packages/[a-z-]+/'
|
|
362
|
+
'docs/[a-z-]+/'
|
|
363
|
+
'\.github/'
|
|
364
|
+
'/tmp/'
|
|
365
|
+
'/wr-[a-z-]+:[a-z-]+'
|
|
366
|
+
'\bgit (commit|push|mv|add|rebase|merge)\b'
|
|
367
|
+
'\bnpm (run|install|publish)\b'
|
|
368
|
+
'\b(bash|bats|grep|sed|jq)\b'
|
|
369
|
+
'\b(drift|regression|hook|marker|gate|refresh|idempotent|stderr|stdout|regex|formula|dispatch|frontmatter|substring|escape|sentinel|bypass|TTL|cache|invalidate|deduplicate|race|deadlock|timeout|preflight)\b'
|
|
370
|
+
'\b(error|failure|exception|panic|segfault|undefined|EACCES|ENOENT)\b'
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# User-business signals — case-insensitive.
|
|
374
|
+
local ub_patterns=(
|
|
375
|
+
'\b(adopter|adopters|plugin-user|plugin-users|solo[-_ ]?developer|maintainer-persona|end[-_ ]?user|customer|stakeholder)\b'
|
|
376
|
+
'\b(workflow|journey|onboarding|friction|UX|experience|usability|discoverability)\b'
|
|
377
|
+
'\bJTBD-[0-9]+\b'
|
|
378
|
+
'\bjob-to-be-done\b'
|
|
379
|
+
'\b(want|need|cannot|unable to)\b[[:space:]]+(use|access|find|discover|complete)'
|
|
380
|
+
'\bdesired outcome\b'
|
|
381
|
+
'\bunmet need\b'
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
for p in "${tech_patterns[@]}"; do
|
|
385
|
+
if echo "$desc" | grep -qE "$p" 2>/dev/null; then
|
|
386
|
+
tech_signals+=("$p")
|
|
387
|
+
fi
|
|
388
|
+
done
|
|
389
|
+
|
|
390
|
+
for p in "${ub_patterns[@]}"; do
|
|
391
|
+
if echo "$desc" | grep -qiE "$p" 2>/dev/null; then
|
|
392
|
+
ub_signals+=("$p")
|
|
393
|
+
fi
|
|
394
|
+
done
|
|
395
|
+
|
|
396
|
+
local tech_n="${#tech_signals[@]}"
|
|
397
|
+
local ub_n="${#ub_signals[@]}"
|
|
398
|
+
|
|
399
|
+
if [ "$tech_n" -ge 1 ] && [ "$ub_n" -eq 0 ]; then
|
|
400
|
+
echo "technical"
|
|
401
|
+
elif [ "$tech_n" -eq 0 ] && [ "$ub_n" -ge 1 ]; then
|
|
402
|
+
echo "user-business"
|
|
403
|
+
else
|
|
404
|
+
echo "ambiguous"
|
|
405
|
+
fi
|
|
406
|
+
return 0
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
@test "P185: classifier resolves pure-technical description as technical" {
|
|
410
|
+
# camelCase identifier + file path + mechanism word — all technical signals.
|
|
411
|
+
result=$(classify_description "The captureProblem hook in packages/itil/hooks/lib/detectors.sh has a regex drift")
|
|
412
|
+
[ "$result" = "technical" ]
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
@test "P185: classifier resolves pure-technical description (error-message variant)" {
|
|
416
|
+
result=$(classify_description "The bats test exits with exception ENOENT on /tmp/manage-problem-grep")
|
|
417
|
+
[ "$result" = "technical" ]
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@test "P185: classifier resolves pure-technical description (command-name variant)" {
|
|
421
|
+
result=$(classify_description "git mv to docs/problems/<NNN>.verifying.md fails on the rebase merge")
|
|
422
|
+
[ "$result" = "technical" ]
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@test "P185: classifier resolves pure-user-business description (persona + journey)" {
|
|
426
|
+
result=$(classify_description "Adopters cannot complete the onboarding workflow without UX friction")
|
|
427
|
+
[ "$result" = "user-business" ]
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
@test "P185: classifier resolves pure-user-business description (JTBD-shaped need)" {
|
|
431
|
+
result=$(classify_description "JTBD-101 names a desired outcome the plugin-user cannot achieve")
|
|
432
|
+
[ "$result" = "user-business" ]
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
@test "P185: classifier resolves pure-user-business description (unmet-need variant)" {
|
|
436
|
+
result=$(classify_description "Solo-developers want to discover scaffold templates but cannot find them")
|
|
437
|
+
[ "$result" = "user-business" ]
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
@test "P185: classifier resolves mixed-signal description as ambiguous" {
|
|
441
|
+
# Mentions both technical mechanism (hook drift) and user-business (adopter friction)
|
|
442
|
+
# — exactly the case the AskUserQuestion fallback is designed for.
|
|
443
|
+
result=$(classify_description "The hook drift affects adopters in the onboarding workflow")
|
|
444
|
+
[ "$result" = "ambiguous" ]
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
@test "P185: classifier resolves no-signal description as ambiguous" {
|
|
448
|
+
# Plain prose with no technical or user-business signals — fallback to ask.
|
|
449
|
+
result=$(classify_description "Something is off but I cannot describe it well")
|
|
450
|
+
[ "$result" = "ambiguous" ]
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
# ---------------------------------------------------------------------------
|
|
454
|
+
# Stderr-advisory shape — I2 isomorphism guard.
|
|
455
|
+
# Per architect-review rider: the advisory text MUST be identical in
|
|
456
|
+
# sentence structure across `technical` vs `user-business` classifications
|
|
457
|
+
# beyond the substituted type-value tokens. Otherwise the advisory itself
|
|
458
|
+
# becomes a control-flow asymmetry keyed on `type` and re-introduces an
|
|
459
|
+
# I2 leak through the back door.
|
|
460
|
+
# ---------------------------------------------------------------------------
|
|
461
|
+
|
|
462
|
+
# Mirror of the SKILL.md advisory template.
|
|
463
|
+
format_stderr_advisory() {
|
|
464
|
+
local resolved_type="$1"
|
|
465
|
+
local other_type="$2"
|
|
466
|
+
local signals="$3"
|
|
467
|
+
printf 'capture-problem: classified type=%s from description signals: %s; re-invoke with --type=%s to override\n' \
|
|
468
|
+
"$resolved_type" "$signals" "$other_type"
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
# Strip the type-value tokens + signal-name list so we can compare the
|
|
472
|
+
# sentence skeleton in isolation.
|
|
473
|
+
strip_substituted_tokens() {
|
|
474
|
+
sed -E 's/type=[a-z-]+/type=<X>/g; s/signals: [^;]+;/signals: <S>;/g'
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
@test "P185: stderr advisory shape is isomorphic across technical vs user-business classifications" {
|
|
478
|
+
tech_msg=$(format_stderr_advisory technical user-business "camelCase-id, packages/path")
|
|
479
|
+
ub_msg=$(format_stderr_advisory user-business technical "adopter, onboarding")
|
|
480
|
+
tech_shape=$(echo "$tech_msg" | strip_substituted_tokens)
|
|
481
|
+
ub_shape=$(echo "$ub_msg" | strip_substituted_tokens)
|
|
482
|
+
[ "$tech_shape" = "$ub_shape" ]
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
@test "P185: stderr advisory names the override flag with the OTHER type value" {
|
|
486
|
+
tech_msg=$(format_stderr_advisory technical user-business "sig")
|
|
487
|
+
ub_msg=$(format_stderr_advisory user-business technical "sig")
|
|
488
|
+
# The technical-classified advisory must offer the user-business override.
|
|
489
|
+
echo "$tech_msg" | grep -q -- '--type=user-business'
|
|
490
|
+
# The user-business-classified advisory must offer the technical override.
|
|
491
|
+
echo "$ub_msg" | grep -q -- '--type=technical'
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
@test "P185: stderr advisory does NOT prefix with type-value when describing the contract" {
|
|
495
|
+
# The shape `classified type=<value> from description signals: <list>;
|
|
496
|
+
# re-invoke with --type=<other> to override` — the leading prose
|
|
497
|
+
# "capture-problem: classified type=" must be identical regardless of
|
|
498
|
+
# type value (substitution happens AFTER the equals sign).
|
|
499
|
+
tech_msg=$(format_stderr_advisory technical user-business "sig")
|
|
500
|
+
ub_msg=$(format_stderr_advisory user-business technical "sig")
|
|
501
|
+
echo "$tech_msg" | grep -q '^capture-problem: classified type='
|
|
502
|
+
echo "$ub_msg" | grep -q '^capture-problem: classified type='
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# ---------------------------------------------------------------------------
|
|
506
|
+
# Pre-resolution flag precedence — caller-side flags MUST short-circuit
|
|
507
|
+
# before the classifier runs. Order: --type=<value> > --no-prompt >
|
|
508
|
+
# classifier > AskUserQuestion fallback.
|
|
509
|
+
# ---------------------------------------------------------------------------
|
|
510
|
+
|
|
511
|
+
# Mirror of SKILL.md Step 1.5 dispatch order.
|
|
512
|
+
resolve_type_dispatch() {
|
|
513
|
+
local type_flag="$1" # value from --type=<X>, empty if not passed
|
|
514
|
+
local no_prompt="$2" # "1" if --no-prompt passed, empty otherwise
|
|
515
|
+
local desc="$3"
|
|
516
|
+
if [ -n "$type_flag" ]; then
|
|
517
|
+
echo "$type_flag:pre-resolved-flag"
|
|
518
|
+
return 0
|
|
519
|
+
fi
|
|
520
|
+
if [ "$no_prompt" = "1" ]; then
|
|
521
|
+
echo "technical:no-prompt-default"
|
|
522
|
+
return 0
|
|
523
|
+
fi
|
|
524
|
+
local classified
|
|
525
|
+
classified=$(classify_description "$desc")
|
|
526
|
+
if [ "$classified" = "ambiguous" ]; then
|
|
527
|
+
echo "ambiguous:fallback-to-ask"
|
|
528
|
+
else
|
|
529
|
+
echo "$classified:derived-from-signals"
|
|
530
|
+
fi
|
|
531
|
+
return 0
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
@test "P185: --type=user-business pre-resolves even on pure-technical description" {
|
|
535
|
+
# Caller-side flag MUST win over the classifier — explicit override.
|
|
536
|
+
result=$(resolve_type_dispatch user-business "" "The hook in packages/itil/hooks/lib drifts")
|
|
537
|
+
[ "$result" = "user-business:pre-resolved-flag" ]
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
@test "P185: --no-prompt pre-resolves to technical even on pure-user-business description" {
|
|
541
|
+
# AFK contract: --no-prompt always lands `technical`, regardless of description signals.
|
|
542
|
+
result=$(resolve_type_dispatch "" "1" "Adopters cannot complete the onboarding workflow")
|
|
543
|
+
[ "$result" = "technical:no-prompt-default" ]
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
@test "P185: no flags + pure-technical description → derived-from-signals technical" {
|
|
547
|
+
result=$(resolve_type_dispatch "" "" "The captureProblem.bats test fails with exit 1")
|
|
548
|
+
[ "$result" = "technical:derived-from-signals" ]
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
@test "P185: no flags + pure-user-business description → derived-from-signals user-business" {
|
|
552
|
+
result=$(resolve_type_dispatch "" "" "JTBD-301 plugin-user persona constraint")
|
|
553
|
+
[ "$result" = "user-business:derived-from-signals" ]
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
@test "P185: no flags + ambiguous description → fallback to AskUserQuestion" {
|
|
557
|
+
result=$(resolve_type_dispatch "" "" "Something feels wrong but I cannot say what")
|
|
558
|
+
[ "$result" = "ambiguous:fallback-to-ask" ]
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
# ---------------------------------------------------------------------------
|
|
562
|
+
# Meta-recursive corpus validation — exercise the classifier against
|
|
563
|
+
# real problem-ticket descriptions and assert the classifier's verdict
|
|
564
|
+
# matches the existing `**Type**:` field on each ticket. Limited to the
|
|
565
|
+
# tickets whose descriptions are short enough to be deterministic; the
|
|
566
|
+
# point is to catch obvious classifier regressions on the canonical
|
|
567
|
+
# corpus, not to validate every edge case.
|
|
568
|
+
# ---------------------------------------------------------------------------
|
|
569
|
+
|
|
570
|
+
@test "P185: classifier matches the P185 ticket's own self-classification (meta-recursive)" {
|
|
571
|
+
# P185's body opens with: "/wr-itil:capture-problem Step 1.5 currently
|
|
572
|
+
# fires an AskUserQuestion for type ..." — command-name pattern +
|
|
573
|
+
# camelCase + mechanism words ("dispatch", "regex"). The ticket's
|
|
574
|
+
# **Type**: field is `technical`. Classifier must agree.
|
|
575
|
+
desc="/wr-itil:capture-problem Step 1.5 fires an AskUserQuestion for type via a regex dispatch the SKILL.md frontmatter resolves"
|
|
576
|
+
result=$(classify_description "$desc")
|
|
577
|
+
[ "$result" = "technical" ]
|
|
578
|
+
}
|