@windyroad/itil 0.26.0 → 0.27.0-preview.296
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/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/scripts/migrate-problems-add-type.sh +116 -0
- package/scripts/reconcile-readme.sh +30 -3
- package/scripts/test/dual-tolerant-glob-rfc-002-t2.bats +309 -0
- package/scripts/test/i2-no-type-branching.bats +320 -0
- package/scripts/test/migrate-problems-add-type.bats +280 -0
- package/scripts/test/reconcile-readme.bats +353 -0
- package/scripts/test/skill-md-dual-tolerant-coverage-rfc-002-t3.bats +374 -0
- package/skills/capture-problem/SKILL.md +53 -12
- package/skills/capture-rfc/SKILL.md +6 -3
- package/skills/close-incident/SKILL.md +2 -2
- package/skills/link-incident/SKILL.md +4 -2
- package/skills/list-problems/SKILL.md +15 -11
- package/skills/manage-problem/SKILL.md +25 -13
- package/skills/manage-rfc/SKILL.md +4 -1
- package/skills/report-upstream/SKILL.md +3 -1
- package/skills/review-problems/SKILL.md +8 -7
- package/skills/transition-problem/SKILL.md +3 -3
- package/skills/transition-problems/SKILL.md +2 -2
- package/skills/work-problem/SKILL.md +6 -1
- package/skills/work-problems/SKILL.md +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/itil/scripts/migrate-problems-add-type.sh
|
|
3
|
+
#
|
|
4
|
+
# One-shot bulk migration: ensure every `<problems-dir>/<NNN>-*.<status>.md`
|
|
5
|
+
# carries a `**Type**: technical` body field per ADR-060 Phase 1 item 8b.
|
|
6
|
+
#
|
|
7
|
+
# Default `**Type**: technical` per ADR-060 line 92 (existing tickets
|
|
8
|
+
# bulk-migrate to default; per-ticket judgement comes later via
|
|
9
|
+
# capture-problem AskUserQuestion in item 8c — out of scope here).
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# migrate-problems-add-type.sh [--apply] [<problems-dir>]
|
|
13
|
+
#
|
|
14
|
+
# Default <problems-dir> is ./docs/problems.
|
|
15
|
+
#
|
|
16
|
+
# Modes:
|
|
17
|
+
# diagnose (default): read-only. Lists each ticket needing migration on
|
|
18
|
+
# stdout (one per line, basename only). Exit 0 = clean, 1 = drift.
|
|
19
|
+
# --apply: writes `**Type**: technical` after the LAST present body
|
|
20
|
+
# field marker in {Status, Reported, Priority, Effort, WSJF}.
|
|
21
|
+
# Idempotent — re-running with Type already present is a no-op.
|
|
22
|
+
#
|
|
23
|
+
# Exit codes:
|
|
24
|
+
# 0 = clean (diagnose: no migration needed; apply: completed)
|
|
25
|
+
# 1 = drift (diagnose only; tickets needing migration listed)
|
|
26
|
+
# 2 = parse error
|
|
27
|
+
#
|
|
28
|
+
# Tickets with NO recognisable header field markers are skipped with a
|
|
29
|
+
# `SKIP <basename>` warning on stderr — these are typically malformed
|
|
30
|
+
# scaffold leftovers and should not be auto-migrated.
|
|
31
|
+
#
|
|
32
|
+
# @problem P170 (Slice 4 B7.T2 / item 8b)
|
|
33
|
+
# @adr ADR-060 (type-tag schema; default `technical`; spec line 91 amended
|
|
34
|
+
# 2026-05-06: header field block in body, NOT YAML frontmatter)
|
|
35
|
+
# @adr ADR-014 (one bounded sub-task per script)
|
|
36
|
+
|
|
37
|
+
set -uo pipefail
|
|
38
|
+
|
|
39
|
+
APPLY=0
|
|
40
|
+
PROBLEMS_DIR=""
|
|
41
|
+
|
|
42
|
+
for arg in "$@"; do
|
|
43
|
+
case "$arg" in
|
|
44
|
+
--apply) APPLY=1 ;;
|
|
45
|
+
-*) echo "PARSE_ERROR: unknown flag: $arg" >&2; exit 2 ;;
|
|
46
|
+
*)
|
|
47
|
+
if [ -z "$PROBLEMS_DIR" ]; then
|
|
48
|
+
PROBLEMS_DIR="$arg"
|
|
49
|
+
else
|
|
50
|
+
echo "PARSE_ERROR: multiple positional args: $arg" >&2
|
|
51
|
+
exit 2
|
|
52
|
+
fi
|
|
53
|
+
;;
|
|
54
|
+
esac
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
PROBLEMS_DIR="${PROBLEMS_DIR:-docs/problems}"
|
|
58
|
+
|
|
59
|
+
if [ ! -d "$PROBLEMS_DIR" ]; then
|
|
60
|
+
echo "PARSE_ERROR: problems-dir not found: $PROBLEMS_DIR" >&2
|
|
61
|
+
exit 2
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
drift=0
|
|
65
|
+
shopt -s nullglob
|
|
66
|
+
for f in "$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.open.md \
|
|
67
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.known-error.md \
|
|
68
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.verifying.md \
|
|
69
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.parked.md \
|
|
70
|
+
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.closed.md; do
|
|
71
|
+
base="$(basename "$f")"
|
|
72
|
+
|
|
73
|
+
# Idempotency check: any line matching `**Type**: <value>` in the
|
|
74
|
+
# first 30 lines counts as already-migrated.
|
|
75
|
+
if head -n 30 "$f" | grep -q '^\*\*Type\*\*:'; then
|
|
76
|
+
continue
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Find the LAST body-field marker line (Status / Reported / Priority /
|
|
80
|
+
# Effort / WSJF) in the first 30 lines. Insertion anchor.
|
|
81
|
+
anchor=$(head -n 30 "$f" \
|
|
82
|
+
| grep -n -E '^\*\*(Status|Reported|Priority|Effort|WSJF)\*\*:' \
|
|
83
|
+
| tail -1 | cut -d: -f1)
|
|
84
|
+
|
|
85
|
+
if [ -z "$anchor" ]; then
|
|
86
|
+
echo "SKIP $base (no recognisable header field markers)" >&2
|
|
87
|
+
continue
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
drift=1
|
|
91
|
+
|
|
92
|
+
if [ "$APPLY" -eq 0 ]; then
|
|
93
|
+
echo "$base"
|
|
94
|
+
continue
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Apply: insert `**Type**: technical` after line $anchor.
|
|
98
|
+
# Use a temp file for atomic replace.
|
|
99
|
+
tmp=$(mktemp)
|
|
100
|
+
awk -v anchor="$anchor" '
|
|
101
|
+
NR == anchor { print; print "**Type**: technical"; next }
|
|
102
|
+
{ print }
|
|
103
|
+
' "$f" > "$tmp"
|
|
104
|
+
mv "$tmp" "$f"
|
|
105
|
+
done
|
|
106
|
+
shopt -u nullglob
|
|
107
|
+
|
|
108
|
+
if [ "$APPLY" -eq 1 ]; then
|
|
109
|
+
exit 0
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
if [ "$drift" -eq 1 ]; then
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
exit 0
|
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
# packages/itil/scripts/reconcile-readme.sh
|
|
3
3
|
#
|
|
4
4
|
# Diagnose-only drift detector for docs/problems/README.md vs filesystem
|
|
5
|
-
# truth. Reads
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# truth. Reads ticket files from BOTH the flat layout
|
|
6
|
+
# `<problems-dir>/<NNN>-*.<status>.md` AND the per-state subdir layout
|
|
7
|
+
# `<problems-dir>/<status>/<NNN>-*.md` (RFC-002 dual-tolerant migration
|
|
8
|
+
# window), parses the README's WSJF Rankings + Verification Queue +
|
|
9
|
+
# Closed tables, and reports each disagreement.
|
|
8
10
|
#
|
|
9
11
|
# Usage:
|
|
10
12
|
# reconcile-readme.sh [<problems-dir>]
|
|
11
13
|
#
|
|
12
14
|
# Default <problems-dir> is ./docs/problems.
|
|
13
15
|
#
|
|
16
|
+
# Dual-layout precedence: when the same ID appears in both layout-halves
|
|
17
|
+
# (transient mid-migration race between `git mv` and README refresh),
|
|
18
|
+
# the per-state subdir wins — ADR-031 §"Authoritative state signal"
|
|
19
|
+
# treats subdirectory as the post-migration ground truth.
|
|
20
|
+
#
|
|
14
21
|
# Exit codes:
|
|
15
22
|
# 0 = clean (README matches filesystem)
|
|
16
23
|
# 1 = drift detected (structured diff to stdout)
|
|
@@ -28,8 +35,10 @@
|
|
|
28
35
|
# only job is to report ground truth.
|
|
29
36
|
#
|
|
30
37
|
# @problem P118
|
|
38
|
+
# @problem P170 (RFC-002 — dual-tolerant migration window)
|
|
31
39
|
# @adr ADR-014 (Reconciliation as preflight robustness layer)
|
|
32
40
|
# @adr ADR-022 (Verification Pending lifecycle excludes from WSJF Rankings)
|
|
41
|
+
# @adr ADR-031 (Per-state subdir is post-migration authoritative state signal)
|
|
33
42
|
# @adr ADR-038 (Progressive disclosure — per-row byte budget)
|
|
34
43
|
|
|
35
44
|
set -uo pipefail
|
|
@@ -50,9 +59,15 @@ if ! grep -q '^## WSJF Rankings' "$README"; then
|
|
|
50
59
|
fi
|
|
51
60
|
|
|
52
61
|
# ── Build filesystem truth: ID → status ─────────────────────────────────────
|
|
62
|
+
#
|
|
63
|
+
# RFC-002 dual-tolerant enumeration: walk BOTH the flat layout and the
|
|
64
|
+
# per-state subdir layout. Per-state subdir wins on collision (mid-
|
|
65
|
+
# migration race; per-state is the migration target per ADR-031).
|
|
53
66
|
|
|
54
67
|
declare -A FS_STATUS
|
|
55
68
|
shopt -s nullglob
|
|
69
|
+
# Flat layout: docs/problems/<NNN>-<title>.<state>.md
|
|
70
|
+
# Status classified from filename suffix.
|
|
56
71
|
for f in "$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.open.md \
|
|
57
72
|
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.known-error.md \
|
|
58
73
|
"$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.verifying.md \
|
|
@@ -74,6 +89,18 @@ for f in "$PROBLEMS_DIR"/[0-9][0-9][0-9]-*.open.md \
|
|
|
74
89
|
esac
|
|
75
90
|
FS_STATUS["$id"]="$ticket_status"
|
|
76
91
|
done
|
|
92
|
+
# Per-state subdir layout: docs/problems/<state>/<NNN>-<title>.md
|
|
93
|
+
# Status derived from parent directory name (the subdirectory IS the
|
|
94
|
+
# state signal post-migration). Writes after the flat loop so per-state
|
|
95
|
+
# wins on cross-layout ID collision (ADR-031 authoritative state).
|
|
96
|
+
for ticket_status in open known-error verifying closed parked; do
|
|
97
|
+
for f in "$PROBLEMS_DIR"/"$ticket_status"/[0-9][0-9][0-9]-*.md; do
|
|
98
|
+
base="$(basename "$f")"
|
|
99
|
+
num="${base%%-*}"
|
|
100
|
+
id="P${num}"
|
|
101
|
+
FS_STATUS["$id"]="$ticket_status"
|
|
102
|
+
done
|
|
103
|
+
done
|
|
77
104
|
shopt -u nullglob
|
|
78
105
|
|
|
79
106
|
# ── Parse README sections into ID buckets ───────────────────────────────────
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# @rfc RFC-002 T2 — Dual-tolerant SKILL.md glob updates
|
|
4
|
+
# @adr ADR-031 (Problem-ticket directory layout)
|
|
5
|
+
# @adr ADR-051 (load-bearing-from-the-start — the dual-tolerant glob
|
|
6
|
+
# contract ships with a behavioural enforcement test, not later by
|
|
7
|
+
# graceful drift)
|
|
8
|
+
# @adr ADR-052 (behavioural-bats default — this test exercises the
|
|
9
|
+
# glob shape against synthetic fixtures and asserts observable
|
|
10
|
+
# enumeration behaviour; it does NOT structurally grep SKILL.md
|
|
11
|
+
# prose for the dual-pattern string, which would be P081-class
|
|
12
|
+
# structural-test-disguised-as-behavioural)
|
|
13
|
+
# @adr ADR-014 (single-purpose: one mechanical contract — dual-glob
|
|
14
|
+
# enumeration parity across both layouts)
|
|
15
|
+
# @problem P069 (driving — flat layout unskimmable; the migration this
|
|
16
|
+
# contract guards is the relief)
|
|
17
|
+
# @problem P081 (no structural-grep on SKILL.md content — this test
|
|
18
|
+
# is the behavioural alternative)
|
|
19
|
+
# @jtbd JTBD-001 (extended scope — multi-commit RFC-grain coordinated
|
|
20
|
+
# change; the test is the cross-skill invariant the SKILL.md edits
|
|
21
|
+
# share)
|
|
22
|
+
# @jtbd JTBD-006 (work-backlog-AFK — dual-tolerant globs preserve
|
|
23
|
+
# AFK-loop continuity across the migration window; without this
|
|
24
|
+
# contract, mid-migration loop iterations silently miss tickets in
|
|
25
|
+
# the un-migrated layout)
|
|
26
|
+
#
|
|
27
|
+
# Contract: SKILL.md enumeration globs of `docs/problems/<state>/...`
|
|
28
|
+
# during the RFC-002 migration window MUST match BOTH the flat layout
|
|
29
|
+
# (`docs/problems/<NNN>-<title>.<state>.md`) AND the per-state subdir
|
|
30
|
+
# layout (`docs/problems/<state>/<NNN>-<title>.md`). The dual-tolerant
|
|
31
|
+
# pattern shape is:
|
|
32
|
+
#
|
|
33
|
+
# ls docs/problems/*.<state>.md docs/problems/<state>/*.md 2>/dev/null
|
|
34
|
+
#
|
|
35
|
+
# (with `2>/dev/null` swallowing the no-match error from whichever
|
|
36
|
+
# half of the OR currently has zero matches.)
|
|
37
|
+
#
|
|
38
|
+
# This test exercises the canonical dual-tolerant pattern shapes (state-
|
|
39
|
+
# filtered enumeration, ID-anchored lookup, all-state-all-tickets) on
|
|
40
|
+
# synthetic fixtures of three shapes (flat-only, per-state-only, mixed).
|
|
41
|
+
# Each cross-product asserts non-empty enumeration of every present
|
|
42
|
+
# ticket and zero false-positive enumeration of absent tickets.
|
|
43
|
+
#
|
|
44
|
+
# CONTRACT NOTE: when one half of the dual-glob has zero matches in the
|
|
45
|
+
# current fixture (single-layout fixtures), `ls X Y 2>/dev/null` exits
|
|
46
|
+
# nonzero — the unmatched literal pathname propagates to ls's argv and
|
|
47
|
+
# `2>/dev/null` only suppresses the stderr noise, not the exit code.
|
|
48
|
+
# This is CORRECT behaviour — SKILL.md call sites MUST treat STDOUT
|
|
49
|
+
# emptiness as the canonical "no tickets" signal, NOT exit code zero.
|
|
50
|
+
# Test assertions therefore probe stdout content, not `$status`, except
|
|
51
|
+
# in the empty-fixture and missing-ID cases where nonzero exit is the
|
|
52
|
+
# intended contract.
|
|
53
|
+
#
|
|
54
|
+
# T6 (post-T5 verification) drops the flat-layout half. This test
|
|
55
|
+
# updates at T6 to single-pattern, NOT removed — the contract becomes
|
|
56
|
+
# "per-state layout enumerates correctly" but remains behavioural.
|
|
57
|
+
|
|
58
|
+
setup() {
|
|
59
|
+
REPO_ROOT="$(mktemp -d)"
|
|
60
|
+
cd "$REPO_ROOT"
|
|
61
|
+
mkdir -p docs/problems
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
teardown() {
|
|
65
|
+
cd /
|
|
66
|
+
rm -rf "$REPO_ROOT"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# ── fixture builders ─────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
build_flat_layout() {
|
|
72
|
+
cat > docs/problems/100-foo.open.md <<'EOF'
|
|
73
|
+
# Problem 100: Foo
|
|
74
|
+
**Status**: Open
|
|
75
|
+
EOF
|
|
76
|
+
cat > docs/problems/101-bar.known-error.md <<'EOF'
|
|
77
|
+
# Problem 101: Bar
|
|
78
|
+
**Status**: Known Error
|
|
79
|
+
EOF
|
|
80
|
+
cat > docs/problems/102-baz.verifying.md <<'EOF'
|
|
81
|
+
# Problem 102: Baz
|
|
82
|
+
**Status**: Verification Pending
|
|
83
|
+
EOF
|
|
84
|
+
cat > docs/problems/103-qux.parked.md <<'EOF'
|
|
85
|
+
# Problem 103: Qux
|
|
86
|
+
**Status**: Parked
|
|
87
|
+
EOF
|
|
88
|
+
cat > docs/problems/104-quux.closed.md <<'EOF'
|
|
89
|
+
# Problem 104: Quux
|
|
90
|
+
**Status**: Closed
|
|
91
|
+
EOF
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
build_per_state_layout() {
|
|
95
|
+
mkdir -p docs/problems/open docs/problems/known-error docs/problems/verifying docs/problems/parked docs/problems/closed
|
|
96
|
+
cat > docs/problems/open/200-foo2.md <<'EOF'
|
|
97
|
+
# Problem 200: Foo2
|
|
98
|
+
**Status**: Open
|
|
99
|
+
EOF
|
|
100
|
+
cat > docs/problems/known-error/201-bar2.md <<'EOF'
|
|
101
|
+
# Problem 201: Bar2
|
|
102
|
+
**Status**: Known Error
|
|
103
|
+
EOF
|
|
104
|
+
cat > docs/problems/verifying/202-baz2.md <<'EOF'
|
|
105
|
+
# Problem 202: Baz2
|
|
106
|
+
**Status**: Verification Pending
|
|
107
|
+
EOF
|
|
108
|
+
cat > docs/problems/parked/203-qux2.md <<'EOF'
|
|
109
|
+
# Problem 203: Qux2
|
|
110
|
+
**Status**: Parked
|
|
111
|
+
EOF
|
|
112
|
+
cat > docs/problems/closed/204-quux2.md <<'EOF'
|
|
113
|
+
# Problem 204: Quux2
|
|
114
|
+
**Status**: Closed
|
|
115
|
+
EOF
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
build_mixed_layout() {
|
|
119
|
+
build_flat_layout
|
|
120
|
+
build_per_state_layout
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# ── Pattern A: state-filtered enumeration ────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
@test "dual-tolerant state-filtered glob: flat-only fixture enumerates open ticket" {
|
|
126
|
+
build_flat_layout
|
|
127
|
+
run bash -c 'ls docs/problems/*.open.md docs/problems/open/*.md 2>/dev/null'
|
|
128
|
+
# ls exits nonzero because the per-state half has no match — stdout
|
|
129
|
+
# content is the canonical signal, not exit code.
|
|
130
|
+
[[ "$output" == *"100-foo.open.md"* ]]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@test "dual-tolerant state-filtered glob: per-state-only fixture enumerates open ticket" {
|
|
134
|
+
build_per_state_layout
|
|
135
|
+
run bash -c 'ls docs/problems/*.open.md docs/problems/open/*.md 2>/dev/null'
|
|
136
|
+
[[ "$output" == *"open/200-foo2.md"* ]]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@test "dual-tolerant state-filtered glob: mixed fixture enumerates BOTH layouts simultaneously" {
|
|
140
|
+
build_mixed_layout
|
|
141
|
+
run bash -c 'ls docs/problems/*.open.md docs/problems/open/*.md 2>/dev/null'
|
|
142
|
+
[ "$status" -eq 0 ]
|
|
143
|
+
[[ "$output" == *"100-foo.open.md"* ]]
|
|
144
|
+
[[ "$output" == *"open/200-foo2.md"* ]]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@test "dual-tolerant state-filtered glob: mixed fixture, known-error" {
|
|
148
|
+
build_mixed_layout
|
|
149
|
+
run bash -c 'ls docs/problems/*.known-error.md docs/problems/known-error/*.md 2>/dev/null'
|
|
150
|
+
[ "$status" -eq 0 ]
|
|
151
|
+
[[ "$output" == *"101-bar.known-error.md"* ]]
|
|
152
|
+
[[ "$output" == *"known-error/201-bar2.md"* ]]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@test "dual-tolerant state-filtered glob: mixed fixture, verifying" {
|
|
156
|
+
build_mixed_layout
|
|
157
|
+
run bash -c 'ls docs/problems/*.verifying.md docs/problems/verifying/*.md 2>/dev/null'
|
|
158
|
+
[ "$status" -eq 0 ]
|
|
159
|
+
[[ "$output" == *"102-baz.verifying.md"* ]]
|
|
160
|
+
[[ "$output" == *"verifying/202-baz2.md"* ]]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@test "dual-tolerant state-filtered glob: mixed fixture, parked" {
|
|
164
|
+
build_mixed_layout
|
|
165
|
+
run bash -c 'ls docs/problems/*.parked.md docs/problems/parked/*.md 2>/dev/null'
|
|
166
|
+
[ "$status" -eq 0 ]
|
|
167
|
+
[[ "$output" == *"103-qux.parked.md"* ]]
|
|
168
|
+
[[ "$output" == *"parked/203-qux2.md"* ]]
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@test "dual-tolerant state-filtered glob: state filter excludes other-state tickets in flat layout" {
|
|
172
|
+
build_flat_layout
|
|
173
|
+
run bash -c 'ls docs/problems/*.open.md docs/problems/open/*.md 2>/dev/null'
|
|
174
|
+
[[ "$output" != *"known-error"* ]]
|
|
175
|
+
[[ "$output" != *"verifying"* ]]
|
|
176
|
+
[[ "$output" != *"parked"* ]]
|
|
177
|
+
[[ "$output" != *"closed"* ]]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@test "dual-tolerant state-filtered glob: state filter excludes other-state subdirs in per-state layout" {
|
|
181
|
+
build_per_state_layout
|
|
182
|
+
run bash -c 'ls docs/problems/*.open.md docs/problems/open/*.md 2>/dev/null'
|
|
183
|
+
# No known-error/, verifying/, parked/, closed/ subdir contents leak into open enumeration.
|
|
184
|
+
[[ "$output" != *"201-bar2.md"* ]]
|
|
185
|
+
[[ "$output" != *"202-baz2.md"* ]]
|
|
186
|
+
[[ "$output" != *"203-qux2.md"* ]]
|
|
187
|
+
[[ "$output" != *"204-quux2.md"* ]]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# ── Pattern B: ID-anchored lookup (any state) ────────────────────────────────
|
|
191
|
+
|
|
192
|
+
@test "dual-tolerant ID-anchored glob: flat-only fixture finds ticket by ID" {
|
|
193
|
+
build_flat_layout
|
|
194
|
+
run bash -c 'ls docs/problems/100-*.md docs/problems/*/100-*.md 2>/dev/null'
|
|
195
|
+
# Single-layout fixture: ls exits nonzero on the unmatched half;
|
|
196
|
+
# stdout content is the contract signal.
|
|
197
|
+
[[ "$output" == *"100-foo.open.md"* ]]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@test "dual-tolerant ID-anchored glob: per-state-only fixture finds ticket by ID" {
|
|
201
|
+
build_per_state_layout
|
|
202
|
+
run bash -c 'ls docs/problems/200-*.md docs/problems/*/200-*.md 2>/dev/null'
|
|
203
|
+
[[ "$output" == *"open/200-foo2.md"* ]]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@test "dual-tolerant ID-anchored glob: mixed fixture finds tickets across both layouts" {
|
|
207
|
+
build_mixed_layout
|
|
208
|
+
run bash -c 'ls docs/problems/100-*.md docs/problems/*/100-*.md 2>/dev/null'
|
|
209
|
+
[[ "$output" == *"100-foo.open.md"* ]]
|
|
210
|
+
run bash -c 'ls docs/problems/200-*.md docs/problems/*/200-*.md 2>/dev/null'
|
|
211
|
+
[[ "$output" == *"open/200-foo2.md"* ]]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
@test "dual-tolerant ID-anchored glob: missing ID returns empty (status nonzero from ls)" {
|
|
215
|
+
build_flat_layout
|
|
216
|
+
set +e
|
|
217
|
+
result=$(ls docs/problems/999-*.md docs/problems/*/999-*.md 2>/dev/null)
|
|
218
|
+
rc=$?
|
|
219
|
+
set -e
|
|
220
|
+
[ -z "$result" ]
|
|
221
|
+
[ "$rc" -ne 0 ]
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# ── Pattern C: all-states-all-tickets (next-ID compute, count) ───────────────
|
|
225
|
+
|
|
226
|
+
@test "dual-tolerant all-tickets glob: flat-only enumerates every ticket regardless of state" {
|
|
227
|
+
build_flat_layout
|
|
228
|
+
run bash -c 'ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null'
|
|
229
|
+
# Single-layout fixture: stdout is contract signal, not $status.
|
|
230
|
+
[[ "$output" == *"100-foo.open.md"* ]]
|
|
231
|
+
[[ "$output" == *"101-bar.known-error.md"* ]]
|
|
232
|
+
[[ "$output" == *"102-baz.verifying.md"* ]]
|
|
233
|
+
[[ "$output" == *"103-qux.parked.md"* ]]
|
|
234
|
+
[[ "$output" == *"104-quux.closed.md"* ]]
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@test "dual-tolerant all-tickets glob: per-state-only enumerates every ticket regardless of state" {
|
|
238
|
+
build_per_state_layout
|
|
239
|
+
run bash -c 'ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null'
|
|
240
|
+
[[ "$output" == *"open/200-foo2.md"* ]]
|
|
241
|
+
[[ "$output" == *"known-error/201-bar2.md"* ]]
|
|
242
|
+
[[ "$output" == *"verifying/202-baz2.md"* ]]
|
|
243
|
+
[[ "$output" == *"parked/203-qux2.md"* ]]
|
|
244
|
+
[[ "$output" == *"closed/204-quux2.md"* ]]
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@test "dual-tolerant all-tickets glob: mixed fixture enumerates ALL tickets in BOTH layouts" {
|
|
248
|
+
build_mixed_layout
|
|
249
|
+
run bash -c 'ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null'
|
|
250
|
+
[ "$status" -eq 0 ]
|
|
251
|
+
for ticket in 100-foo.open.md 101-bar.known-error.md 102-baz.verifying.md 103-qux.parked.md 104-quux.closed.md \
|
|
252
|
+
open/200-foo2.md known-error/201-bar2.md verifying/202-baz2.md parked/203-qux2.md closed/204-quux2.md; do
|
|
253
|
+
[[ "$output" == *"${ticket}"* ]]
|
|
254
|
+
done
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
@test "dual-tolerant all-tickets next-ID compute: highest ID across both layouts" {
|
|
258
|
+
# Critical for capture-problem next-ID compute (architect finding 2):
|
|
259
|
+
# the next-ID surface MUST recurse so flat-layout 104 and per-state 204
|
|
260
|
+
# both contribute to max-ID; missing the per-state half re-allocates
|
|
261
|
+
# already-taken IDs.
|
|
262
|
+
build_mixed_layout
|
|
263
|
+
result=$(ls docs/problems/*.md docs/problems/*/*.md 2>/dev/null \
|
|
264
|
+
| sed 's/.*\///' \
|
|
265
|
+
| grep -oE '^[0-9]+' \
|
|
266
|
+
| sort -n \
|
|
267
|
+
| tail -1)
|
|
268
|
+
[ "$result" = "204" ]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# ── Pattern D: pathspec-pair shell-glob equivalence ──────────────────────────
|
|
272
|
+
# git log accepts multiple pathspecs; the dual-tolerant filter is two
|
|
273
|
+
# pathspecs side-by-side. We validate this against the working-tree
|
|
274
|
+
# semantics that git uses (the same shell-glob shape).
|
|
275
|
+
|
|
276
|
+
@test "dual-tolerant pathspec pair: each pathspec enumerates its layout half independently" {
|
|
277
|
+
build_mixed_layout
|
|
278
|
+
flat_count=$(ls docs/problems/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
279
|
+
subdir_count=$(ls docs/problems/*/*.md 2>/dev/null | wc -l | tr -d ' ')
|
|
280
|
+
[ "$flat_count" -ge 5 ]
|
|
281
|
+
[ "$subdir_count" -ge 5 ]
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# ── Pattern E: brace-expansion ID + state-set (report-upstream) ──────────────
|
|
285
|
+
|
|
286
|
+
@test "dual-tolerant ID + state-set lookup: flat brace expansion + per-state lookup" {
|
|
287
|
+
build_mixed_layout
|
|
288
|
+
# Old shape: ls docs/problems/${ID}-*.{open,known-error,verifying,closed}.md
|
|
289
|
+
# Dual-tolerant: add docs/problems/*/${ID}-*.md as a sibling pathspec.
|
|
290
|
+
run bash -c 'ls docs/problems/100-*.{open,known-error,verifying,closed}.md docs/problems/*/100-*.md 2>/dev/null'
|
|
291
|
+
[[ "$output" == *"100-foo.open.md"* ]]
|
|
292
|
+
run bash -c 'ls docs/problems/200-*.{open,known-error,verifying,closed}.md docs/problems/*/200-*.md 2>/dev/null'
|
|
293
|
+
[[ "$output" == *"open/200-foo2.md"* ]]
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# ── Composition: empty-tree fixture exit-code semantics ──────────────────────
|
|
297
|
+
|
|
298
|
+
@test "dual-tolerant glob: empty fixture produces empty output and ls exits nonzero" {
|
|
299
|
+
# Critical for null-safe `2>/dev/null` semantics — `ls` on a
|
|
300
|
+
# non-matching glob exits nonzero. SKILL.md call sites must rely on
|
|
301
|
+
# `2>/dev/null` to suppress the stderr noise but still treat empty
|
|
302
|
+
# stdout as the canonical "no tickets" signal.
|
|
303
|
+
set +e
|
|
304
|
+
result=$(ls docs/problems/*.open.md docs/problems/open/*.md 2>/dev/null)
|
|
305
|
+
rc=$?
|
|
306
|
+
set -e
|
|
307
|
+
[ -z "$result" ]
|
|
308
|
+
[ "$rc" -ne 0 ]
|
|
309
|
+
}
|