@windyroad/retrospective 0.19.0 → 0.20.0
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,8 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/bin/wr-retrospective-check-plugin-maturity-drift
|
|
3
|
+
#
|
|
4
|
+
# ADR-049 shim — resolves the canonical body in this package's scripts/
|
|
5
|
+
# dir. Phase 3b drift detector entrypoint; sibling to ADR-051's
|
|
6
|
+
# `wr-retrospective-check-readme-jtbd-currency`.
|
|
7
|
+
|
|
8
|
+
exec "$(dirname "$0")/../scripts/check-plugin-maturity-drift.sh" "$@"
|
package/package.json
CHANGED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/retrospective/scripts/check-plugin-maturity-drift.sh
|
|
3
|
+
#
|
|
4
|
+
# Phase 3b (P087 / P238) plugin-maturity drift advisory.
|
|
5
|
+
#
|
|
6
|
+
# Compares each plugin's rendered README.md maturity badge against the
|
|
7
|
+
# canonical `plugin.json` `maturity:` field and emits an NDJSON-shaped
|
|
8
|
+
# drift signal per plugin to stdout. Sibling to ADR-051's
|
|
9
|
+
# `check-readme-jtbd-currency.sh` — same detector pattern, different
|
|
10
|
+
# anchor (maturity rollup vs JTBD citation), different failure mode
|
|
11
|
+
# (render-vs-canonical drift vs citation drift).
|
|
12
|
+
#
|
|
13
|
+
# Drift hint vocabulary:
|
|
14
|
+
#
|
|
15
|
+
# missing-badge — plugin.json carries `maturity:` but the README
|
|
16
|
+
# has no `*Maturity: ...*` prose-woven badge
|
|
17
|
+
# stale-band — README badge band mismatches canonical record
|
|
18
|
+
# orphan-badge — README has badge but plugin.json has no
|
|
19
|
+
# `maturity:` field (renderer ran ahead of
|
|
20
|
+
# populate; or populate was reverted)
|
|
21
|
+
# anti-pattern-section — README has a standalone `## Maturity` heading
|
|
22
|
+
# (ADR-063 §"README badge rendering format"
|
|
23
|
+
# rejects the section shape)
|
|
24
|
+
# anti-pattern-url — README has a shields.io URL or inline SVG
|
|
25
|
+
# (ADR-063 §F5 rejects external-dependency
|
|
26
|
+
# rendering)
|
|
27
|
+
#
|
|
28
|
+
# Output format (one line per package, alphabetical):
|
|
29
|
+
# README package=<name> badge_band=<band|none> record_band=<band|none>
|
|
30
|
+
# drift_hints=<csv>
|
|
31
|
+
# Plus a trailing TOTAL line:
|
|
32
|
+
# TOTAL packages=<N> drift_instances=<K>
|
|
33
|
+
#
|
|
34
|
+
# Exit code is always 0 — advisory only per ADR-013 Rule 6 / ADR-040
|
|
35
|
+
# declarative-first / ADR-051 Phase 1 precedent. Drift count is emitted
|
|
36
|
+
# as data on stdout; downstream consumers (run-retro Step 2b wiring,
|
|
37
|
+
# release-pre-flight habit, Phase 4 escalation per ADR-051 Phase 2
|
|
38
|
+
# criterion) decide whether to act.
|
|
39
|
+
#
|
|
40
|
+
# Usage:
|
|
41
|
+
# check-plugin-maturity-drift.sh [<packages-dir>]
|
|
42
|
+
#
|
|
43
|
+
# Default:
|
|
44
|
+
# <packages-dir> = ./packages
|
|
45
|
+
#
|
|
46
|
+
# Exit codes:
|
|
47
|
+
# 0 = always (advisory only — count is signal, not failure)
|
|
48
|
+
# 2 = parse error (packages-dir missing or unreadable)
|
|
49
|
+
#
|
|
50
|
+
# @problem P238 (Phase 3b — drift detector)
|
|
51
|
+
# @problem P087 (parent — no maturity signal on plugin features)
|
|
52
|
+
# @problem P152 (sibling drift-detector pattern — JTBD-currency)
|
|
53
|
+
# @adr ADR-063 (Plugin maturity presentation layer — Phase 3b contract)
|
|
54
|
+
# @adr ADR-051 (Sibling drift-detector pattern)
|
|
55
|
+
# @adr ADR-013 Rule 6 (non-interactive fail-safe — advisory exit 0)
|
|
56
|
+
# @adr ADR-040 (Declarative-first / advisory-then-escalate)
|
|
57
|
+
# @adr ADR-049 (Shim grammar — `wr-retrospective-check-plugin-maturity-drift`)
|
|
58
|
+
# @adr ADR-052 (Behavioural tests default)
|
|
59
|
+
# @jtbd JTBD-302 (Trust That the README Describes the Plugin I Just Installed)
|
|
60
|
+
# @jtbd JTBD-007 (Keep Plugins Current Across Projects — maturity-band-currency
|
|
61
|
+
# as third currency dimension alongside code + JTBD-content)
|
|
62
|
+
# @jtbd JTBD-101 (Extend the Suite — clear patterns include stability signal)
|
|
63
|
+
|
|
64
|
+
set -uo pipefail
|
|
65
|
+
|
|
66
|
+
PACKAGES_DIR="${1:-packages}"
|
|
67
|
+
|
|
68
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
if [ ! -d "$PACKAGES_DIR" ]; then
|
|
71
|
+
echo "check-plugin-maturity-drift: packages dir not found: $PACKAGES_DIR" >&2
|
|
72
|
+
exit 2
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# ── Python body ─────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export CPMD_PACKAGES_DIR="$PACKAGES_DIR"
|
|
78
|
+
|
|
79
|
+
python3 - <<'PYEOF'
|
|
80
|
+
import json, os, re, sys
|
|
81
|
+
from pathlib import Path
|
|
82
|
+
|
|
83
|
+
packages_dir = Path(os.environ["CPMD_PACKAGES_DIR"]).resolve()
|
|
84
|
+
|
|
85
|
+
if not packages_dir.is_dir():
|
|
86
|
+
print(f"check-plugin-maturity-drift: packages dir not found: {packages_dir}", file=sys.stderr)
|
|
87
|
+
sys.exit(2)
|
|
88
|
+
|
|
89
|
+
# Badge prose-woven pattern: `*Maturity: <Band>...*` per ADR-063
|
|
90
|
+
# §"README badge rendering format". Captures the band token as the
|
|
91
|
+
# first word after `Maturity:`.
|
|
92
|
+
BADGE_RE = re.compile(r"\*Maturity:\s+([A-Za-z]+)[^*]*\*")
|
|
93
|
+
ANTI_SECTION_RE = re.compile(r"(?m)^#{1,3}\s+Maturity\s*$")
|
|
94
|
+
ANTI_URL_RE = re.compile(r"shields\.io|img\.shields", re.IGNORECASE)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def extract_badge_band(readme_text):
|
|
98
|
+
"""Returns the band token from a `*Maturity: <Band>...*` span, or
|
|
99
|
+
None when no such span is present.
|
|
100
|
+
"""
|
|
101
|
+
m = BADGE_RE.search(readme_text)
|
|
102
|
+
if m:
|
|
103
|
+
return m.group(1)
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def append_hint(current, hint):
|
|
108
|
+
if not current:
|
|
109
|
+
return hint
|
|
110
|
+
parts = current.split(",")
|
|
111
|
+
if hint in parts:
|
|
112
|
+
return current
|
|
113
|
+
return current + "," + hint
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def evaluate_plugin(pkg_dir):
|
|
117
|
+
"""Returns (line_dict, hint_count) for one plugin, or None when the
|
|
118
|
+
plugin should be silently skipped (no README).
|
|
119
|
+
"""
|
|
120
|
+
plugin_json_path = pkg_dir / ".claude-plugin" / "plugin.json"
|
|
121
|
+
readme_path = pkg_dir / "README.md"
|
|
122
|
+
if not readme_path.is_file():
|
|
123
|
+
return None
|
|
124
|
+
try:
|
|
125
|
+
plugin_doc = json.loads(plugin_json_path.read_text(encoding="utf-8"))
|
|
126
|
+
except Exception:
|
|
127
|
+
plugin_doc = {}
|
|
128
|
+
if not isinstance(plugin_doc, dict):
|
|
129
|
+
plugin_doc = {}
|
|
130
|
+
|
|
131
|
+
record_band = None
|
|
132
|
+
maturity = plugin_doc.get("maturity")
|
|
133
|
+
if isinstance(maturity, dict):
|
|
134
|
+
record_band = maturity.get("band")
|
|
135
|
+
|
|
136
|
+
readme_text = readme_path.read_text(encoding="utf-8")
|
|
137
|
+
badge_band = extract_badge_band(readme_text)
|
|
138
|
+
|
|
139
|
+
hints = ""
|
|
140
|
+
|
|
141
|
+
if record_band and not badge_band:
|
|
142
|
+
hints = append_hint(hints, "missing-badge")
|
|
143
|
+
elif badge_band and not record_band:
|
|
144
|
+
hints = append_hint(hints, "orphan-badge")
|
|
145
|
+
elif record_band and badge_band and record_band != badge_band:
|
|
146
|
+
hints = append_hint(hints, "stale-band")
|
|
147
|
+
|
|
148
|
+
if ANTI_SECTION_RE.search(readme_text):
|
|
149
|
+
hints = append_hint(hints, "anti-pattern-section")
|
|
150
|
+
if ANTI_URL_RE.search(readme_text):
|
|
151
|
+
hints = append_hint(hints, "anti-pattern-url")
|
|
152
|
+
|
|
153
|
+
line = {
|
|
154
|
+
"package": pkg_dir.name,
|
|
155
|
+
"badge_band": badge_band or "none",
|
|
156
|
+
"record_band": record_band or "none",
|
|
157
|
+
"drift_hints": hints,
|
|
158
|
+
}
|
|
159
|
+
return (line, 1 if hints else 0)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ── Walk packages/ ─────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
plugin_dirs = sorted(
|
|
165
|
+
[d for d in packages_dir.iterdir()
|
|
166
|
+
if d.is_dir() and (d / ".claude-plugin" / "plugin.json").is_file()
|
|
167
|
+
or (d.is_dir() and (d / "README.md").is_file())]
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
total_packages = 0
|
|
171
|
+
total_drift_instances = 0
|
|
172
|
+
|
|
173
|
+
for pkg_dir in plugin_dirs:
|
|
174
|
+
result = evaluate_plugin(pkg_dir)
|
|
175
|
+
if result is None:
|
|
176
|
+
continue
|
|
177
|
+
line, drift = result
|
|
178
|
+
total_packages += 1
|
|
179
|
+
total_drift_instances += drift
|
|
180
|
+
print(
|
|
181
|
+
f"README package={line['package']} "
|
|
182
|
+
f"badge_band={line['badge_band']} "
|
|
183
|
+
f"record_band={line['record_band']} "
|
|
184
|
+
f"drift_hints={line['drift_hints']}"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if total_packages > 0:
|
|
188
|
+
print(f"TOTAL packages={total_packages} drift_instances={total_drift_instances}")
|
|
189
|
+
|
|
190
|
+
PYEOF
|
|
191
|
+
|
|
192
|
+
exit 0
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# @problem P238 — Phase 3b drift detector behavioural confirmation.
|
|
4
|
+
# @problem P087 — parent: plugin maturity battle-hardening signal.
|
|
5
|
+
# @problem P152 — sibling drift-detector pattern (JTBD-currency).
|
|
6
|
+
#
|
|
7
|
+
# Contract under test:
|
|
8
|
+
# `packages/retrospective/scripts/check-plugin-maturity-drift.sh` is an
|
|
9
|
+
# advisory drift detector. It compares each plugin's rendered README
|
|
10
|
+
# maturity badge against the canonical `plugin.json` `maturity:` field
|
|
11
|
+
# and emits NDJSON-per-drift signals to stdout. Exit code 0 always per
|
|
12
|
+
# ADR-013 Rule 6 — drift is data, not failure.
|
|
13
|
+
#
|
|
14
|
+
# Drift classes:
|
|
15
|
+
# missing-badge — plugin.json has maturity but README has no badge
|
|
16
|
+
# stale-band — badge band mismatches canonical record
|
|
17
|
+
# orphan-badge — README has badge but plugin.json has no maturity
|
|
18
|
+
# anti-pattern-section — README has standalone ## Maturity section
|
|
19
|
+
# anti-pattern-url — README has shields.io / SVG badge URL
|
|
20
|
+
#
|
|
21
|
+
# @adr ADR-063 (Plugin maturity presentation layer — Phase 3b contract)
|
|
22
|
+
# @adr ADR-051 (Sibling drift-detector pattern — `check-readme-jtbd-currency.sh`)
|
|
23
|
+
# @adr ADR-013 Rule 6 (non-interactive fail-safe — exit 0 always)
|
|
24
|
+
# @adr ADR-049 (Shim grammar — `wr-retrospective-check-plugin-maturity-drift` on $PATH)
|
|
25
|
+
# @adr ADR-052 (Behavioural tests default)
|
|
26
|
+
|
|
27
|
+
setup() {
|
|
28
|
+
SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
29
|
+
SCRIPT="$SCRIPTS_DIR/check-plugin-maturity-drift.sh"
|
|
30
|
+
FIXTURE_DIR="$(mktemp -d)"
|
|
31
|
+
PROJECT_ROOT="$FIXTURE_DIR/project"
|
|
32
|
+
PKG_DIR="$PROJECT_ROOT/packages"
|
|
33
|
+
mkdir -p "$PKG_DIR"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
teardown() {
|
|
37
|
+
rm -rf "$FIXTURE_DIR"
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Helper: write a synthetic plugin layout with plugin.json + README.md
|
|
41
|
+
make_plugin() {
|
|
42
|
+
local name="$1"
|
|
43
|
+
local plugin_json="$2"
|
|
44
|
+
local readme="$3"
|
|
45
|
+
local pkg="$PKG_DIR/$name"
|
|
46
|
+
mkdir -p "$pkg/.claude-plugin"
|
|
47
|
+
printf '%s\n' "$plugin_json" > "$pkg/.claude-plugin/plugin.json"
|
|
48
|
+
printf '%s\n' "$readme" > "$pkg/README.md"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# ── Pre-checks ──────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
@test "script file exists and is executable" {
|
|
54
|
+
[ -f "$SCRIPT" ]
|
|
55
|
+
[ -x "$SCRIPT" ]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@test "missing packages dir exits 2 with error message on stderr" {
|
|
59
|
+
run bash "$SCRIPT" "$FIXTURE_DIR/does-not-exist"
|
|
60
|
+
[ "$status" -eq 2 ]
|
|
61
|
+
[[ "$output" == *"packages dir not found"* ]]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@test "empty packages dir exits 0 with empty stdout" {
|
|
65
|
+
run bash "$SCRIPT" "$PKG_DIR"
|
|
66
|
+
[ "$status" -eq 0 ]
|
|
67
|
+
[[ "$output" == *"TOTAL"* ]] || [ -z "$output" ]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# ── Clean fixture: badge matches plugin.json record ─────────────────────────
|
|
71
|
+
|
|
72
|
+
@test "clean fixture: matching badge + record emits no drift hints" {
|
|
73
|
+
make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
|
|
74
|
+
"# @windyroad/stub
|
|
75
|
+
|
|
76
|
+
**Stub plugin description.** *Maturity: Alpha.*
|
|
77
|
+
|
|
78
|
+
## Skills
|
|
79
|
+
"
|
|
80
|
+
|
|
81
|
+
run bash "$SCRIPT" "$PKG_DIR"
|
|
82
|
+
[ "$status" -eq 0 ]
|
|
83
|
+
[[ "$output" == *"package=stub"* ]]
|
|
84
|
+
[[ "$output" == *"drift_hints="* ]]
|
|
85
|
+
[[ "$output" != *"stale-band"* ]]
|
|
86
|
+
[[ "$output" != *"missing-badge"* ]]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# ── Stale-band fixture: badge != canonical record ───────────────────────────
|
|
90
|
+
|
|
91
|
+
@test "stale-band fixture: README badge band mismatches plugin.json record" {
|
|
92
|
+
make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub","maturity":{"schema_version":"1.0","band":"Beta"}}' \
|
|
93
|
+
"# @windyroad/stub
|
|
94
|
+
|
|
95
|
+
**Stub plugin description.** *Maturity: Alpha.*
|
|
96
|
+
|
|
97
|
+
## Skills
|
|
98
|
+
"
|
|
99
|
+
|
|
100
|
+
run bash "$SCRIPT" "$PKG_DIR"
|
|
101
|
+
[ "$status" -eq 0 ]
|
|
102
|
+
[[ "$output" == *"package=stub"* ]]
|
|
103
|
+
[[ "$output" == *"stale-band"* ]]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# ── Missing-badge fixture: plugin.json has maturity, README has no badge ────
|
|
107
|
+
|
|
108
|
+
@test "missing-badge fixture: plugin.json has maturity but README has no badge" {
|
|
109
|
+
make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
|
|
110
|
+
"# @windyroad/stub
|
|
111
|
+
|
|
112
|
+
**Stub plugin description.**
|
|
113
|
+
|
|
114
|
+
## Skills
|
|
115
|
+
"
|
|
116
|
+
|
|
117
|
+
run bash "$SCRIPT" "$PKG_DIR"
|
|
118
|
+
[ "$status" -eq 0 ]
|
|
119
|
+
[[ "$output" == *"package=stub"* ]]
|
|
120
|
+
[[ "$output" == *"missing-badge"* ]]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# ── Orphan-badge fixture: README has badge, plugin.json has no maturity ─────
|
|
124
|
+
|
|
125
|
+
@test "orphan-badge fixture: README badge present but plugin.json lacks maturity field" {
|
|
126
|
+
make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub"}' \
|
|
127
|
+
"# @windyroad/stub
|
|
128
|
+
|
|
129
|
+
**Stub plugin description.** *Maturity: Alpha.*
|
|
130
|
+
|
|
131
|
+
## Skills
|
|
132
|
+
"
|
|
133
|
+
|
|
134
|
+
run bash "$SCRIPT" "$PKG_DIR"
|
|
135
|
+
[ "$status" -eq 0 ]
|
|
136
|
+
[[ "$output" == *"package=stub"* ]]
|
|
137
|
+
[[ "$output" == *"orphan-badge"* ]]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# ── Anti-pattern: standalone ## Maturity section ────────────────────────────
|
|
141
|
+
|
|
142
|
+
@test "anti-pattern: standalone ## Maturity section emits anti-pattern-section hint" {
|
|
143
|
+
make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
|
|
144
|
+
"# @windyroad/stub
|
|
145
|
+
|
|
146
|
+
**Stub plugin description.** *Maturity: Alpha.*
|
|
147
|
+
|
|
148
|
+
## Maturity
|
|
149
|
+
|
|
150
|
+
Alpha band.
|
|
151
|
+
|
|
152
|
+
## Skills
|
|
153
|
+
"
|
|
154
|
+
|
|
155
|
+
run bash "$SCRIPT" "$PKG_DIR"
|
|
156
|
+
[ "$status" -eq 0 ]
|
|
157
|
+
[[ "$output" == *"package=stub"* ]]
|
|
158
|
+
[[ "$output" == *"anti-pattern-section"* ]]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# ── Anti-pattern: shields.io URL ────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
@test "anti-pattern: shields.io URL emits anti-pattern-url hint" {
|
|
164
|
+
make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
|
|
165
|
+
"# @windyroad/stub
|
|
166
|
+
|
|
167
|
+
**Stub plugin description.** *Maturity: Alpha.* 
|
|
168
|
+
|
|
169
|
+
## Skills
|
|
170
|
+
"
|
|
171
|
+
|
|
172
|
+
run bash "$SCRIPT" "$PKG_DIR"
|
|
173
|
+
[ "$status" -eq 0 ]
|
|
174
|
+
[[ "$output" == *"package=stub"* ]]
|
|
175
|
+
[[ "$output" == *"anti-pattern-url"* ]]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# ── TOTAL summary line ──────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
@test "multi-plugin aggregation: emits TOTAL summary with drift count" {
|
|
181
|
+
make_plugin "alpha" '{"name":"wr-alpha","version":"0.1.0","description":"Alpha","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
|
|
182
|
+
"# @windyroad/alpha
|
|
183
|
+
|
|
184
|
+
**Alpha plugin.** *Maturity: Alpha.*
|
|
185
|
+
|
|
186
|
+
## Skills
|
|
187
|
+
"
|
|
188
|
+
make_plugin "bravo" '{"name":"wr-bravo","version":"0.1.0","description":"Bravo","maturity":{"schema_version":"1.0","band":"Beta"}}' \
|
|
189
|
+
"# @windyroad/bravo
|
|
190
|
+
|
|
191
|
+
**Bravo plugin.**
|
|
192
|
+
|
|
193
|
+
## Skills
|
|
194
|
+
"
|
|
195
|
+
|
|
196
|
+
run bash "$SCRIPT" "$PKG_DIR"
|
|
197
|
+
[ "$status" -eq 0 ]
|
|
198
|
+
[[ "$output" == *"package=alpha"* ]]
|
|
199
|
+
[[ "$output" == *"package=bravo"* ]]
|
|
200
|
+
[[ "$output" == *"TOTAL packages=2"* ]]
|
|
201
|
+
[[ "$output" == *"drift_instances=1"* ]]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# ── Exit-0-always invariant ─────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
@test "exit-0-always: even with multiple drift entries, exit code is 0" {
|
|
207
|
+
make_plugin "alpha" '{"name":"wr-alpha","version":"0.1.0","description":"Alpha","maturity":{"schema_version":"1.0","band":"Beta"}}' \
|
|
208
|
+
"# @windyroad/alpha
|
|
209
|
+
|
|
210
|
+
**Alpha plugin.** *Maturity: Alpha.*
|
|
211
|
+
|
|
212
|
+
## Skills
|
|
213
|
+
"
|
|
214
|
+
make_plugin "bravo" '{"name":"wr-bravo","version":"0.1.0","description":"Bravo","maturity":{"schema_version":"1.0","band":"Stable"}}' \
|
|
215
|
+
"# @windyroad/bravo
|
|
216
|
+
|
|
217
|
+
**Bravo plugin.**
|
|
218
|
+
|
|
219
|
+
## Skills
|
|
220
|
+
"
|
|
221
|
+
|
|
222
|
+
run bash "$SCRIPT" "$PKG_DIR"
|
|
223
|
+
[ "$status" -eq 0 ]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# ── Package without README is silently skipped ──────────────────────────────
|
|
227
|
+
|
|
228
|
+
@test "package without README.md is silently skipped" {
|
|
229
|
+
local pkg="$PKG_DIR/no-readme"
|
|
230
|
+
mkdir -p "$pkg/.claude-plugin"
|
|
231
|
+
echo '{"name":"wr-no-readme","version":"0.1.0","description":"No README","maturity":{"schema_version":"1.0","band":"Alpha"}}' > "$pkg/.claude-plugin/plugin.json"
|
|
232
|
+
|
|
233
|
+
make_plugin "with-readme" '{"name":"wr-with-readme","version":"0.1.0","description":"With README","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
|
|
234
|
+
"# @windyroad/with-readme
|
|
235
|
+
|
|
236
|
+
**With README.** *Maturity: Alpha.*
|
|
237
|
+
"
|
|
238
|
+
|
|
239
|
+
run bash "$SCRIPT" "$PKG_DIR"
|
|
240
|
+
[ "$status" -eq 0 ]
|
|
241
|
+
[[ "$output" != *"package=no-readme"* ]]
|
|
242
|
+
[[ "$output" == *"package=with-readme"* ]]
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# ── No-network primitive: ADR-035 privacy posture ───────────────────────────
|
|
246
|
+
|
|
247
|
+
@test "ADR-035: script body invokes no network primitive" {
|
|
248
|
+
run grep -E "(curl|wget|nc -|netcat|ssh |scp |rsync|http\.client|urllib|requests)" "$SCRIPT"
|
|
249
|
+
[ "$status" -ne 0 ]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# ── NDJSON output: per-line shape parseable as structured signal ────────────
|
|
253
|
+
|
|
254
|
+
@test "NDJSON output: each README line follows package=<name> <key>=<value> shape" {
|
|
255
|
+
make_plugin "stub" '{"name":"wr-stub","version":"0.1.0","description":"Stub","maturity":{"schema_version":"1.0","band":"Alpha"}}' \
|
|
256
|
+
"# @windyroad/stub
|
|
257
|
+
|
|
258
|
+
**Stub plugin description.** *Maturity: Alpha.*
|
|
259
|
+
|
|
260
|
+
## Skills
|
|
261
|
+
"
|
|
262
|
+
|
|
263
|
+
run bash "$SCRIPT" "$PKG_DIR"
|
|
264
|
+
[ "$status" -eq 0 ]
|
|
265
|
+
# Per-package README line is space-separated key=value tokens
|
|
266
|
+
[[ "$output" == *"README package=stub"* ]]
|
|
267
|
+
[[ "$output" == *"badge_band="* ]]
|
|
268
|
+
[[ "$output" == *"record_band="* ]]
|
|
269
|
+
}
|