@windyroad/itil 0.61.1 → 0.61.2

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.
Files changed (171) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +7 -2
  3. package/agents/test/fixtures/proceed-new-genuinely-new.md +0 -43
  4. package/agents/test/fixtures/proceed-new-subtle-sibling.md +0 -47
  5. package/agents/test/fixtures/regression-p347-vs-p346.md +0 -54
  6. package/agents/test/hang-off-check.bats +0 -225
  7. package/hooks/test/block-list.bats +0 -153
  8. package/hooks/test/command-detect.bats +0 -181
  9. package/hooks/test/itil-assistant-output-gate.bats +0 -114
  10. package/hooks/test/itil-assistant-output-review.bats +0 -224
  11. package/hooks/test/itil-bash-polling-antipattern-detect.bats +0 -154
  12. package/hooks/test/itil-changeset-discipline.bats +0 -603
  13. package/hooks/test/itil-claude-space-protection.bats +0 -294
  14. package/hooks/test/itil-commit-trailer-transition-advisory.bats +0 -80
  15. package/hooks/test/itil-correction-detect.bats +0 -189
  16. package/hooks/test/itil-deferral-cadence-gate.bats +0 -187
  17. package/hooks/test/itil-fictional-defer-detect.bats +0 -292
  18. package/hooks/test/itil-fix-title-lifecycle-advisory.bats +0 -129
  19. package/hooks/test/itil-mid-loop-ask-detect.bats +0 -220
  20. package/hooks/test/itil-no-implement-draft-gate.bats +0 -210
  21. package/hooks/test/itil-pending-questions-surface.bats +0 -307
  22. package/hooks/test/itil-readme-refresh-discipline.bats +0 -642
  23. package/hooks/test/itil-rfc-oversight-nudge.bats +0 -83
  24. package/hooks/test/itil-rfc-trailer-advisory.bats +0 -375
  25. package/hooks/test/itil-story-mirror-migration-nudge.bats +0 -122
  26. package/hooks/test/manage-problem-enforce-create.bats +0 -424
  27. package/hooks/test/p057-staging-trap-detect.bats +0 -231
  28. package/hooks/test/pre-publish-intake-gate.bats +0 -144
  29. package/hooks/test/runtime-sid-marker.bats +0 -90
  30. package/hooks/test/session-id.bats +0 -370
  31. package/scripts/migrate-problems-add-type.sh +0 -116
  32. package/scripts/test/catchup-scan.bats +0 -274
  33. package/scripts/test/check-afk-accept-eligible.bats +0 -310
  34. package/scripts/test/check-fail-soft-skip-discipline.bats +0 -172
  35. package/scripts/test/check-fix-rfc-trace.bats +0 -164
  36. package/scripts/test/check-locale-discipline.bats +0 -291
  37. package/scripts/test/check-problems-readme-budget.bats +0 -192
  38. package/scripts/test/check-rfc-has-stories.bats +0 -69
  39. package/scripts/test/check-rfc-rejected-alternatives.bats +0 -108
  40. package/scripts/test/check-rfc-stories-ratified.bats +0 -82
  41. package/scripts/test/check-upstream-responses.bats +0 -503
  42. package/scripts/test/classify-readme-drift.bats +0 -392
  43. package/scripts/test/derive-first-dispatch.bats +0 -274
  44. package/scripts/test/derive-release-vehicle.bats +0 -454
  45. package/scripts/test/detect-unratified-stories-maps.bats +0 -60
  46. package/scripts/test/dual-tolerant-glob-rfc-002-t2.bats +0 -309
  47. package/scripts/test/effort-tally.bats +0 -170
  48. package/scripts/test/evaluate-relevance.bats +0 -773
  49. package/scripts/test/kind-a-commands.bats +0 -46
  50. package/scripts/test/mark-create-gate.bats +0 -59
  51. package/scripts/test/mark-story-oversight-confirmed.bats +0 -53
  52. package/scripts/test/migrate-problems-add-type.bats +0 -280
  53. package/scripts/test/migrate-story-status-mirror.bats +0 -167
  54. package/scripts/test/no-type-regression-guard.bats +0 -74
  55. package/scripts/test/oversight-mirror-corpus-lint.bats +0 -150
  56. package/scripts/test/plugin-exercise-index.bats +0 -386
  57. package/scripts/test/plugin-maturity-doc-lint.bats +0 -470
  58. package/scripts/test/plugin-maturity-populate.bats +0 -631
  59. package/scripts/test/plugin-maturity-render.bats +0 -469
  60. package/scripts/test/plugin-validate-ci-gate.bats +0 -208
  61. package/scripts/test/reconcile-readme.bats +0 -1007
  62. package/scripts/test/reconcile-rfcs.bats +0 -496
  63. package/scripts/test/reconcile-stories.bats +0 -173
  64. package/scripts/test/reconcile-story-maps.bats +0 -74
  65. package/scripts/test/release-watch-poll-loop.bats +0 -180
  66. package/scripts/test/resolve-governance-plugin-dirs.bats +0 -78
  67. package/scripts/test/rfc-stories-extension.bats +0 -175
  68. package/scripts/test/skill-invocations.bats +0 -535
  69. package/scripts/test/skill-md-dual-tolerant-coverage-rfc-002-t3.bats +0 -374
  70. package/scripts/test/story-oversight-lib.bats +0 -190
  71. package/scripts/test/update-jtbd-references-related-problems.bats +0 -178
  72. package/scripts/test/update-problem-references-section.bats +0 -242
  73. package/scripts/test/update-problem-rfcs-section.bats +0 -242
  74. package/scripts/test/update-references-section-sibling-helpers.bats +0 -80
  75. package/scripts/test/update-rfc-commits-section.bats +0 -77
  76. package/scripts/test/verify-iter-summary.bats +0 -196
  77. package/scripts/test/working-the-problem-traversal.bats +0 -116
  78. package/skills/capture-problem/test/capture-problem-step-1-5b-jtbd-trace.bats +0 -398
  79. package/skills/capture-problem/test/capture-problem.bats +0 -389
  80. package/skills/capture-story/test/capture-story-behavioural.bats +0 -317
  81. package/skills/capture-story-map/test/capture-story-map-behavioural.bats +0 -98
  82. package/skills/close-incident/test/close-incident-contract.bats +0 -146
  83. package/skills/link-incident/test/link-incident-contract.bats +0 -125
  84. package/skills/list-incidents/test/list-incidents-contract.bats +0 -115
  85. package/skills/list-problems/test/list-problems-contract.bats +0 -100
  86. package/skills/list-stories/test/list-stories-contract.bats +0 -127
  87. package/skills/list-story-maps/test/list-story-maps-contract.bats +0 -46
  88. package/skills/manage-incident/test/manage-incident-adr-044-contract.bats +0 -206
  89. package/skills/manage-incident/test/manage-incident-close-forwarder.bats +0 -67
  90. package/skills/manage-incident/test/manage-incident-link-forwarder.bats +0 -67
  91. package/skills/manage-incident/test/manage-incident-list-forwarder.bats +0 -68
  92. package/skills/manage-incident/test/manage-incident-mitigate-forwarder.bats +0 -66
  93. package/skills/manage-incident/test/manage-incident-restore-forwarder.bats +0 -66
  94. package/skills/manage-incident/test/manage-incident.bats +0 -171
  95. package/skills/manage-problem/test/manage-problem-adr-044-step4-derive-first.bats +0 -151
  96. package/skills/manage-problem/test/manage-problem-auto-migrate-step.bats +0 -53
  97. package/skills/manage-problem/test/manage-problem-concern-boundary.bats +0 -64
  98. package/skills/manage-problem/test/manage-problem-effort-buckets.bats +0 -56
  99. package/skills/manage-problem/test/manage-problem-external-root-cause-detection.bats +0 -119
  100. package/skills/manage-problem/test/manage-problem-first-run-intake-prompt.bats +0 -43
  101. package/skills/manage-problem/test/manage-problem-git-mv-restage-reminder.bats +0 -48
  102. package/skills/manage-problem/test/manage-problem-id-collision-guard.bats +0 -39
  103. package/skills/manage-problem/test/manage-problem-list-forwarder.bats +0 -66
  104. package/skills/manage-problem/test/manage-problem-next-id-origin-lookup.bats +0 -50
  105. package/skills/manage-problem/test/manage-problem-no-prose-options.bats +0 -74
  106. package/skills/manage-problem/test/manage-problem-output-formatting.bats +0 -30
  107. package/skills/manage-problem/test/manage-problem-parked-and-cache.bats +0 -73
  108. package/skills/manage-problem/test/manage-problem-readme-refresh-on-transition.bats +0 -62
  109. package/skills/manage-problem/test/manage-problem-readme-tie-break-order.bats +0 -318
  110. package/skills/manage-problem/test/manage-problem-readme-vq-sort-order.bats +0 -206
  111. package/skills/manage-problem/test/manage-problem-release-vehicle-seed.bats +0 -83
  112. package/skills/manage-problem/test/manage-problem-review-forwarder.bats +0 -71
  113. package/skills/manage-problem/test/manage-problem-step-9d-recovery-path.bats +0 -97
  114. package/skills/manage-problem/test/manage-problem-transition-forwarder.bats +0 -96
  115. package/skills/manage-problem/test/manage-problem-transitive-dependencies.bats +0 -365
  116. package/skills/manage-problem/test/manage-problem-verification-detection.bats +0 -64
  117. package/skills/manage-problem/test/manage-problem-verification-pending.bats +0 -114
  118. package/skills/manage-problem/test/manage-problem-work-forwarder.bats +0 -88
  119. package/skills/manage-story/test/manage-story-contract.bats +0 -174
  120. package/skills/manage-story-map/test/manage-story-map-contract.bats +0 -63
  121. package/skills/mitigate-incident/test/mitigate-incident-contract.bats +0 -258
  122. package/skills/reconcile-readme/test/reconcile-readme-contract.bats +0 -201
  123. package/skills/report-upstream/test/report-upstream-contract.bats +0 -394
  124. package/skills/restore-incident/test/restore-incident-contract.bats +0 -170
  125. package/skills/review-problems/test/inbound-channels-cache-shape.bats +0 -127
  126. package/skills/review-problems/test/inbound-discovery-contract.bats +0 -383
  127. package/skills/review-problems/test/jtbd-301-verdict-shape-contract.bats +0 -225
  128. package/skills/review-problems/test/review-problems-contract.bats +0 -235
  129. package/skills/review-problems/test/review-problems-likely-verified-cell-shape.bats +0 -229
  130. package/skills/scaffold-intake/test/scaffold-intake-contract.bats +0 -126
  131. package/skills/scaffold-intake/test/scaffold-intake-fixture.bats +0 -164
  132. package/skills/scaffold-intake/test/scaffold-intake-secrets-absent.bats +0 -87
  133. package/skills/transition-problem/test/transition-problem-contract.bats +0 -285
  134. package/skills/transition-problems/test/transition-problems-contract.bats +0 -331
  135. package/skills/update-upstream/test/update-upstream-contract.bats +0 -375
  136. package/skills/work-problem/test/work-problem-contract.bats +0 -294
  137. package/skills/work-problems/test/work-problems-above-appetite-remediation.bats +0 -139
  138. package/skills/work-problems/test/work-problems-adr-013-rule-6-p352-amendment.bats +0 -166
  139. package/skills/work-problems/test/work-problems-auto-migrate-step.bats +0 -57
  140. package/skills/work-problems/test/work-problems-cost-logging.bats +0 -109
  141. package/skills/work-problems/test/work-problems-deviation-candidate-shape.bats +0 -121
  142. package/skills/work-problems/test/work-problems-first-run-intake-prompt.bats +0 -36
  143. package/skills/work-problems/test/work-problems-inter-iteration-verify.bats +0 -59
  144. package/skills/work-problems/test/work-problems-mid-loop-userpromptsubmit-handler.bats +0 -69
  145. package/skills/work-problems/test/work-problems-no-mid-loop-asking.bats +0 -192
  146. package/skills/work-problems/test/work-problems-p341-pre-all-done-gate.bats +0 -122
  147. package/skills/work-problems/test/work-problems-p342-r007-derive-then-pass-flags.bats +0 -170
  148. package/skills/work-problems/test/work-problems-p342-retro-auto-ticket-carveout.bats +0 -120
  149. package/skills/work-problems/test/work-problems-preflight-failure-handling.bats +0 -248
  150. package/skills/work-problems/test/work-problems-preflight-session-continuity.bats +0 -139
  151. package/skills/work-problems/test/work-problems-preflight.bats +0 -69
  152. package/skills/work-problems/test/work-problems-release-cadence.bats +0 -63
  153. package/skills/work-problems/test/work-problems-step-0-iter-error-staleness.bats +0 -93
  154. package/skills/work-problems/test/work-problems-step-0b-cache-staleness-behavioural.bats +0 -138
  155. package/skills/work-problems/test/work-problems-step-0c-deferred-placeholder-staleness-behavioural.bats +0 -238
  156. package/skills/work-problems/test/work-problems-step-0d-outbound-responses-staleness-behavioural.bats +0 -174
  157. package/skills/work-problems/test/work-problems-step-2-5-routing.bats +0 -80
  158. package/skills/work-problems/test/work-problems-step-2-5b-cross-halt-routing.bats +0 -149
  159. package/skills/work-problems/test/work-problems-step-3-5-jtbd-ratification-predicate.bats +0 -191
  160. package/skills/work-problems/test/work-problems-step-5-bats-polling-discipline.bats +0 -91
  161. package/skills/work-problems/test/work-problems-step-5-delegation.bats +0 -333
  162. package/skills/work-problems/test/work-problems-step-5-idle-timeout-sigterm.bats +0 -421
  163. package/skills/work-problems/test/work-problems-step-5-is-error-transient-halt.bats +0 -278
  164. package/skills/work-problems/test/work-problems-step-5-iter-changeset-required.bats +0 -95
  165. package/skills/work-problems/test/work-problems-step-5-prompt-body-re-grounding.bats +0 -128
  166. package/skills/work-problems/test/work-problems-step-5-stream-timeout-salvage.bats +0 -251
  167. package/skills/work-problems/test/work-problems-step-6-5-always-drain.bats +0 -203
  168. package/skills/work-problems/test/work-problems-step-6-5-cache-refresh-chain.bats +0 -174
  169. package/skills/work-problems/test/work-problems-step-6-5-fix-and-continue.bats +0 -254
  170. package/skills/work-problems/test/work-problems-step-6-5-postrelease-kv-callback.bats +0 -209
  171. package/skills/work-problems/test/work-problems-stop-condition-questions.bats +0 -85
@@ -1,470 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # @problem P239 — Phase 3c doc-lint per plugin: `plugin.json` maturity field
4
- # shape + rendered README badge currency + anti-pattern absence.
5
- # @problem P087 — parent: plugin maturity battle-hardening signal.
6
- #
7
- # Contract under test: every `packages/<plugin>/.claude-plugin/plugin.json`
8
- # that carries a `maturity:` field MUST conform to the schema pinned by
9
- # ADR-063 §Decision Outcome + iter-10 Amendment 2026-05-18, AND the sibling
10
- # `packages/<plugin>/README.md` MUST carry the prose-woven badge marker
11
- # matching the canonical rollup band. Anti-patterns (standalone `## Maturity`
12
- # heading, shields.io URL, compound bootstrapping rendering inside per-skill
13
- # table cells) are negative-asserted.
14
- #
15
- # Discovery is dynamic — the lint walks `packages/*/.claude-plugin/plugin.json`
16
- # at run time. Plugins without a `maturity:` field are SKIPPED (Phase 3a
17
- # hasn't been run for that plugin yet; the lint asserts SHAPE WHEN PRESENT,
18
- # not mandatory presence per-plugin — presence enforcement belongs to a
19
- # Phase 4+ release-blocking gate, not the doc-lint).
20
- #
21
- # Compound-vs-bare badge form is **out of scope** for this lint per
22
- # architect adjustment A3 (P087 iter-11 architect review 2026-05-18). The
23
- # renderer's compound-rendering fall-through is a separate sub-iter defect;
24
- # the lint asserts band-substring-match only and remains agnostic to the
25
- # `(suite-bootstrap window; <N> invocations / 30d)` form.
26
- #
27
- # Schema_version range: closed enum `{"1.0", "2.0"}` per ADR-058 §Confirmation
28
- # #8 precedent + iter-10 Amendment 2026-05-18 hotfix. `"2.0"` is the canonical
29
- # value post-hotfix; `"1.0"` records exist in pre-hotfix history (architect
30
- # adjustment A4 — both accepted; future amendment may close to `"2.0"` only).
31
- #
32
- # @adr ADR-063 (Plugin maturity presentation layer — Phase 3c contract)
33
- # @adr ADR-053 (Plugin maturity taxonomy — granularity contract,
34
- # rollup-equals-worst-case invariant)
35
- # @adr ADR-058 (Phase 2 measurement — schema_version precedent)
36
- # @adr ADR-052 (Behavioural tests default — JSON-shape + README-marker
37
- # structural-grep on the renderer-emitted stable marker per the documented
38
- # carve-out for renderer-output assertions)
39
- # @adr ADR-013 Rule 6 (non-interactive fail-safe — exit 0 always; the lint
40
- # asserts contract, not policy enforcement)
41
-
42
- setup() {
43
- REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
44
- FIXTURE_DIR="$(mktemp -d)"
45
- }
46
-
47
- teardown() {
48
- rm -rf "$FIXTURE_DIR"
49
- }
50
-
51
- # ── Helper: list plugin packages that carry a `maturity:` field ─────────────
52
- # Returns one bare plugin name per line (e.g. `itil`, `architect`).
53
- plugins_with_maturity() {
54
- local pkg
55
- for pkg in "$REPO_ROOT"/packages/*/; do
56
- local pj="$pkg.claude-plugin/plugin.json"
57
- [ -f "$pj" ] || continue
58
- python3 -c "
59
- import json
60
- try:
61
- d = json.load(open('$pj'))
62
- except Exception:
63
- raise SystemExit
64
- if isinstance(d, dict) and isinstance(d.get('maturity'), dict) and 'band' in d['maturity']:
65
- print('$(basename "$pkg")')
66
- " 2>/dev/null
67
- done
68
- }
69
-
70
- # ── Helper: assert a JSON Python expression evaluates truthy, with diagnostic ─
71
- # Args: plugin-json-path, python-expr, diagnostic-prefix
72
- assert_json() {
73
- local file="$1" expr="$2" diag="$3"
74
- python3 -c "
75
- import json, sys
76
- d = json.load(open('$file'))
77
- ok = bool($expr)
78
- if not ok:
79
- print(f'$diag: failed on $file', file=sys.stderr)
80
- sys.exit(1)
81
- "
82
- }
83
-
84
- # ── Existence + discovery ───────────────────────────────────────────────────
85
-
86
- @test "plugin-maturity-doc-lint: at least one plugin in the monorepo carries maturity field" {
87
- # Defensive — the lint is a no-op without discovered plugins. After the
88
- # iter-10 retroactive rollout (P087 line 133) every shipped plugin carries
89
- # `maturity:`; this test guards against a future regression where the
90
- # rollout is reverted.
91
- local n
92
- n=$(plugins_with_maturity | wc -l | tr -d ' ')
93
- [ "$n" -ge 1 ]
94
- }
95
-
96
- # ── Confirmation: per-surface schema shape (ADR-063 §rich record per-surface) ─
97
-
98
- @test "plugin-maturity-doc-lint: every per-surface record carries full schema" {
99
- # For every entry under `maturity.skills`, `maturity.agents`,
100
- # `maturity.hooks`, `maturity.commands`, assert presence of mandatory keys
101
- # and correct value types. `invocations_30d` MAY be null (hook surfaces are
102
- # not transcript-observable); `breaking_change_age_days` MAY be null (no
103
- # breaking-marker commits observed).
104
- local plugin
105
- for plugin in $(plugins_with_maturity); do
106
- local pj="$REPO_ROOT/packages/$plugin/.claude-plugin/plugin.json"
107
- python3 <<PYEOF
108
- import json, sys
109
- d = json.load(open("$pj"))
110
- m = d["maturity"]
111
- bands = {"Experimental", "Alpha", "Beta", "Stable", "Deprecated"}
112
- schema_versions = {"1.0", "2.0"}
113
- for kind in ("skills", "agents", "hooks", "commands"):
114
- section = m.get(kind, {})
115
- if not isinstance(section, dict):
116
- sys.exit(f"$plugin: maturity.{kind} is not a dict")
117
- for name, rec in section.items():
118
- if not isinstance(rec, dict):
119
- sys.exit(f"$plugin: maturity.{kind}.{name} is not a dict")
120
- sv = rec.get("schema_version")
121
- if sv not in schema_versions:
122
- sys.exit(f"$plugin: maturity.{kind}.{name}.schema_version = {sv!r} not in {schema_versions}")
123
- band = rec.get("band")
124
- if band not in bands:
125
- sys.exit(f"$plugin: maturity.{kind}.{name}.band = {band!r} not in {bands}")
126
- if not isinstance(rec.get("computed_at"), str):
127
- sys.exit(f"$plugin: maturity.{kind}.{name}.computed_at is not a string")
128
- ev = rec.get("evidence")
129
- if not isinstance(ev, dict):
130
- sys.exit(f"$plugin: maturity.{kind}.{name}.evidence is not a dict")
131
- for field in ("invocations_30d", "days_shipped", "closed_tickets_window", "breaking_change_age_days"):
132
- if field not in ev:
133
- sys.exit(f"$plugin: maturity.{kind}.{name}.evidence.{field} missing")
134
- # invocations_30d: int OR null (hooks)
135
- inv = ev["invocations_30d"]
136
- if inv is not None and not isinstance(inv, int):
137
- sys.exit(f"$plugin: invocations_30d must be int or null, got {type(inv).__name__}")
138
- # days_shipped: int
139
- if not isinstance(ev["days_shipped"], int):
140
- sys.exit(f"$plugin: days_shipped must be int")
141
- # closed_tickets_window: int
142
- if not isinstance(ev["closed_tickets_window"], int):
143
- sys.exit(f"$plugin: closed_tickets_window must be int")
144
- # breaking_change_age_days: int OR null
145
- bca = ev["breaking_change_age_days"]
146
- if bca is not None and not isinstance(bca, int):
147
- sys.exit(f"$plugin: breaking_change_age_days must be int or null")
148
- PYEOF
149
- done
150
- }
151
-
152
- # ── Confirmation: rollup shape (ADR-063 §rollup schema + iter-10 Amendment) ──
153
-
154
- @test "plugin-maturity-doc-lint: rollup carries schema_version + band (mandatory pair)" {
155
- # ADR-063 §rollup-schema names `{schema_version, band}` as the rollup keys.
156
- # iter-10 Amendment 2026-05-18 (P0 hotfix) nests per-kind maps UNDER
157
- # `maturity:` — kind-keyed entries are tolerated additionally; the
158
- # schema_version + band pair remains mandatory.
159
- local plugin
160
- for plugin in $(plugins_with_maturity); do
161
- local pj="$REPO_ROOT/packages/$plugin/.claude-plugin/plugin.json"
162
- python3 <<PYEOF
163
- import json, sys
164
- d = json.load(open("$pj"))
165
- m = d["maturity"]
166
- sv = m.get("schema_version")
167
- if sv not in {"1.0", "2.0"}:
168
- sys.exit(f"$plugin: maturity.schema_version = {sv!r} not in {{'1.0','2.0'}}")
169
- band = m.get("band")
170
- if band not in {"Experimental", "Alpha", "Beta", "Stable", "Deprecated"}:
171
- sys.exit(f"$plugin: maturity.band = {band!r} not in taxonomy")
172
- PYEOF
173
- done
174
- }
175
-
176
- # ── P269 — rollup compound-evidence fields shape (when present) ─────────────
177
-
178
- @test "plugin-maturity-doc-lint: rollup rollup_invocations_30d is int or null (when present)" {
179
- # ADR-063 §Amendment 2026-05-18 (P269 — rollup compound-evidence write):
180
- # the rollup carries `rollup_invocations_30d: integer | null`. Integer when
181
- # at least one per-surface entry has a non-null invocations_30d; null when
182
- # ALL per-surface entries are null (e.g. hook-only plugins) — preserves the
183
- # "not measurable" vs "measurably zero" honesty contract per architect §C.
184
- # Per ADR-063 §Confirmation #10 (shape-when-present), this lint asserts
185
- # type-shape when the field is present and tolerates absence for plugins
186
- # that haven't been re-populated since the P269 amendment.
187
- local plugin
188
- for plugin in $(plugins_with_maturity); do
189
- local pj="$REPO_ROOT/packages/$plugin/.claude-plugin/plugin.json"
190
- python3 <<PYEOF
191
- import json, sys
192
- d = json.load(open("$pj"))
193
- m = d["maturity"]
194
- if "rollup_invocations_30d" not in m:
195
- sys.exit(0) # field absent — tolerated per shape-when-present
196
- v = m["rollup_invocations_30d"]
197
- if v is None:
198
- sys.exit(0)
199
- if not isinstance(v, int) or isinstance(v, bool):
200
- sys.exit(f"$plugin: maturity.rollup_invocations_30d = {v!r} ({type(v).__name__}); must be int or null")
201
- if v < 0:
202
- sys.exit(f"$plugin: maturity.rollup_invocations_30d = {v!r}; must be non-negative")
203
- PYEOF
204
- done
205
- }
206
-
207
- @test "plugin-maturity-doc-lint: rollup bootstrapping is bool (when present)" {
208
- # ADR-063 §Amendment 2026-05-18 (P269 — rollup compound-evidence write):
209
- # the rollup carries `bootstrapping: bool` — populate-time snapshot of the
210
- # bootstrapping-window state. The renderer's AND-gated compound predicate
211
- # (plugin-maturity-render.sh:146) reads this flag to decide whether to
212
- # emit the compound form. Shape-when-present per the same tolerance as
213
- # rollup_invocations_30d above.
214
- local plugin
215
- for plugin in $(plugins_with_maturity); do
216
- local pj="$REPO_ROOT/packages/$plugin/.claude-plugin/plugin.json"
217
- python3 <<PYEOF
218
- import json, sys
219
- d = json.load(open("$pj"))
220
- m = d["maturity"]
221
- if "bootstrapping" not in m:
222
- sys.exit(0)
223
- v = m["bootstrapping"]
224
- if not isinstance(v, bool):
225
- sys.exit(f"$plugin: maturity.bootstrapping = {v!r} ({type(v).__name__}); must be bool")
226
- PYEOF
227
- done
228
- }
229
-
230
- # ── Regression fence (architect adjustment A1, iter-11): no top-level kind maps ─
231
-
232
- @test "plugin-maturity-doc-lint: no top-level skills/agents/hooks/commands maturity-shaped records (iter-10 P0 hotfix fence)" {
233
- # The iter-10 P0 hotfix moved per-kind maturity maps from TOP-LEVEL
234
- # (which the Claude Code plugin manifest validator rejects) to NESTED
235
- # UNDER `maturity:`. Regression fence: assert no top-level `skills:` /
236
- # `agents:` / `hooks:` / `commands:` key carries a maturity-shaped record
237
- # map (a dict whose values are dicts containing only `maturity` or
238
- # carrying the full per-surface record shape). Existing legitimate
239
- # top-level surfaces (e.g. itil's top-level `skills:` listing names + paths)
240
- # are NOT maturity-shaped and pass this fence.
241
- local plugin
242
- for plugin in $(plugins_with_maturity); do
243
- local pj="$REPO_ROOT/packages/$plugin/.claude-plugin/plugin.json"
244
- python3 <<PYEOF
245
- import json, sys
246
- d = json.load(open("$pj"))
247
- maturity_keys = {"schema_version", "band", "computed_at", "evidence"}
248
- for legacy_key in ("skills", "agents", "hooks", "commands"):
249
- inner = d.get(legacy_key)
250
- if not isinstance(inner, dict):
251
- continue
252
- for name, val in inner.items():
253
- if isinstance(val, dict):
254
- # Maturity-shaped if it has the maturity schema_version + band
255
- # pair OR is a wrapper that contains only `maturity`.
256
- if "schema_version" in val and "band" in val and val.get("band") in {"Experimental","Alpha","Beta","Stable","Deprecated"}:
257
- sys.exit(f"$plugin: top-level {legacy_key}.{name} carries maturity-shaped record — must nest under maturity.{legacy_key}.{name}")
258
- if set(val.keys()) <= {"maturity"} and isinstance(val.get("maturity"), dict):
259
- sys.exit(f"$plugin: top-level {legacy_key}.{name} carries .maturity wrapper — must nest under maturity.{legacy_key}.{name}")
260
- PYEOF
261
- done
262
- }
263
-
264
- # ── Confirmation: rollup = worst-case of constituent surfaces (ADR-053 granularity) ─
265
-
266
- @test "plugin-maturity-doc-lint: rollup band equals worst-case among constituent surfaces" {
267
- # ADR-053 §granularity contract: rollup band = worst-case
268
- # (Experimental ≻ Alpha ≻ Beta ≻ Stable). Deprecated is an overlay axis
269
- # elided from rollup compute. A plugin whose ONLY surfaces are Deprecated
270
- # is itself Deprecated.
271
- local plugin
272
- for plugin in $(plugins_with_maturity); do
273
- local pj="$REPO_ROOT/packages/$plugin/.claude-plugin/plugin.json"
274
- python3 <<PYEOF
275
- import json, sys
276
- d = json.load(open("$pj"))
277
- m = d["maturity"]
278
- ORDER = ["Experimental", "Alpha", "Beta", "Stable"]
279
- surface_bands = []
280
- for kind in ("skills", "agents", "hooks", "commands"):
281
- for rec in (m.get(kind, {}) or {}).values():
282
- if isinstance(rec, dict) and "band" in rec:
283
- surface_bands.append(rec["band"])
284
- if not surface_bands:
285
- # No surface records — rollup not constrained by worst-case.
286
- sys.exit(0)
287
- non_dep = [b for b in surface_bands if b in ORDER]
288
- if non_dep:
289
- expected = next(b for b in ORDER if b in non_dep)
290
- elif all(b == "Deprecated" for b in surface_bands):
291
- expected = "Deprecated"
292
- else:
293
- expected = None
294
- got = m.get("band")
295
- if expected is not None and got != expected:
296
- sys.exit(f"$plugin: rollup band = {got!r}, expected {expected!r} (worst-case of {sorted(set(surface_bands))})")
297
- PYEOF
298
- done
299
- }
300
-
301
- # ── Synthetic-fixture confirmation: multi-band rollup invariant ──────────────
302
-
303
- @test "plugin-maturity-doc-lint: synthetic multi-band fixture — Experimental ≻ Beta ⇒ rollup Experimental" {
304
- # Builds a synthetic plugin.json with one Experimental skill + one Beta
305
- # agent and asserts rollup band MUST be Experimental. This guards the
306
- # worst-case invariant against future populate-script regressions that
307
- # might compute rollup as best-case or median.
308
- local pj="$FIXTURE_DIR/synthetic-plugin.json"
309
- cat >"$pj" <<'EOF'
310
- {
311
- "name": "wr-synthetic",
312
- "version": "0.0.0",
313
- "maturity": {
314
- "schema_version": "2.0",
315
- "band": "Experimental",
316
- "skills": {
317
- "expt-skill": {
318
- "schema_version": "2.0",
319
- "band": "Experimental",
320
- "computed_at": "2026-05-18T00:00:00Z",
321
- "evidence": {"invocations_30d": 5, "days_shipped": 10, "closed_tickets_window": 0, "breaking_change_age_days": null}
322
- }
323
- },
324
- "agents": {
325
- "beta-agent": {
326
- "schema_version": "2.0",
327
- "band": "Beta",
328
- "computed_at": "2026-05-18T00:00:00Z",
329
- "evidence": {"invocations_30d": 500, "days_shipped": 90, "closed_tickets_window": 15, "breaking_change_age_days": 60}
330
- }
331
- }
332
- }
333
- }
334
- EOF
335
- # Run the worst-case derivation as a free-standing python check; we don't
336
- # invoke the populate script here — we assert the invariant the doc-lint
337
- # enforces against a hand-crafted shape.
338
- python3 <<PYEOF
339
- import json
340
- d = json.load(open("$pj"))
341
- m = d["maturity"]
342
- ORDER = ["Experimental", "Alpha", "Beta", "Stable"]
343
- bands = []
344
- for kind in ("skills","agents","hooks","commands"):
345
- for rec in (m.get(kind, {}) or {}).values():
346
- bands.append(rec["band"])
347
- expected = next(b for b in ORDER if b in bands)
348
- assert m["band"] == expected, f"rollup {m['band']!r} != expected {expected!r}"
349
- PYEOF
350
- }
351
-
352
- @test "plugin-maturity-doc-lint: synthetic all-Deprecated fixture — rollup Deprecated" {
353
- # Companion to the worst-case test: a plugin whose ONLY surfaces are
354
- # Deprecated MUST roll up to Deprecated. Asserts the Deprecated-overlay
355
- # invariant from ADR-053 §granularity contract.
356
- local pj="$FIXTURE_DIR/all-deprecated.json"
357
- cat >"$pj" <<'EOF'
358
- {
359
- "name": "wr-deprecated",
360
- "version": "0.0.0",
361
- "maturity": {
362
- "schema_version": "2.0",
363
- "band": "Deprecated",
364
- "skills": {
365
- "dep-1": {
366
- "schema_version": "2.0",
367
- "band": "Deprecated",
368
- "computed_at": "2026-05-18T00:00:00Z",
369
- "evidence": {"invocations_30d": 0, "days_shipped": 200, "closed_tickets_window": 0, "breaking_change_age_days": null},
370
- "supersededBy": "wr-other:replacement"
371
- }
372
- }
373
- }
374
- }
375
- EOF
376
- python3 <<PYEOF
377
- import json
378
- d = json.load(open("$pj"))
379
- m = d["maturity"]
380
- bands = [rec["band"] for rec in (m.get("skills", {}) or {}).values()]
381
- assert all(b == "Deprecated" for b in bands)
382
- assert m["band"] == "Deprecated", f"all-Deprecated rollup must be Deprecated, got {m['band']!r}"
383
- PYEOF
384
- }
385
-
386
- # ── Confirmation: README badge marker matches canonical rollup band ──────────
387
-
388
- @test "plugin-maturity-doc-lint: README contains *Maturity: <band> marker matching canonical rollup" {
389
- # Architect adjustment A2 (iter-11): anchored regex requires band ∈
390
- # taxonomy followed by either `.` (bare form) or `(` (compound prefix).
391
- # Renderer emits one of these two; structural-grep on the stable marker
392
- # is the documented ADR-052 carve-out for renderer-output assertions.
393
- local plugin
394
- for plugin in $(plugins_with_maturity); do
395
- local pj="$REPO_ROOT/packages/$plugin/.claude-plugin/plugin.json"
396
- local rdm="$REPO_ROOT/packages/$plugin/README.md"
397
- [ -f "$rdm" ] || continue # README absent — skip (no marker to check)
398
- local band
399
- band=$(python3 -c "import json; print(json.load(open('$pj'))['maturity']['band'])")
400
- # Anchored regex: `*Maturity: <band>` followed by `.` (bare form) OR
401
- # ` (` (compound prefix — space + open paren per the renderer's
402
- # `*Maturity: <Band> (suite-bootstrap window; <N> invocations / 30d).*`
403
- # output shape at plugin-maturity-render.sh line 147). P269 fold-fix:
404
- # the previous `[.(]` character class missed the space-before-paren
405
- # case that became visible across the live monorepo once compound
406
- # rendering started firing post-rollout.
407
- if ! grep -qE "\\*Maturity: ${band}(\\.| \\()" "$rdm"; then
408
- echo "$plugin: README missing badge marker *Maturity: ${band}<.|space-paren>" >&2
409
- false
410
- fi
411
- done
412
- }
413
-
414
- # ── Anti-pattern: no standalone `## Maturity` heading ───────────────────────
415
-
416
- @test "plugin-maturity-doc-lint: README has no standalone ## Maturity heading" {
417
- # ADR-063 §README badge rendering format anti-pattern: NEVER emit a
418
- # standalone `## Maturity` section. The badge is woven into existing
419
- # value-framing prose per ADR-051; a standalone section drifts the JTBD
420
- # anchor away from the prose-weaving precedent.
421
- local plugin
422
- for plugin in $(plugins_with_maturity); do
423
- local rdm="$REPO_ROOT/packages/$plugin/README.md"
424
- [ -f "$rdm" ] || continue
425
- if grep -qE '^##[[:space:]]+Maturity[[:space:]]*$' "$rdm"; then
426
- echo "$plugin: README contains standalone ## Maturity heading (anti-pattern)" >&2
427
- false
428
- fi
429
- done
430
- }
431
-
432
- # ── Anti-pattern: no shields.io URL ─────────────────────────────────────────
433
-
434
- @test "plugin-maturity-doc-lint: README has no shields.io maturity badge URL" {
435
- # ADR-063 §Decision Outcome rejected shields.io: external-dep blast
436
- # radius + Bootstrapping clause compound rendering cannot be expressed
437
- # in static badge URL + offline-broken. Markdown text only.
438
- local plugin
439
- for plugin in $(plugins_with_maturity); do
440
- local rdm="$REPO_ROOT/packages/$plugin/README.md"
441
- [ -f "$rdm" ] || continue
442
- if grep -qE 'img\.shields\.io/badge/maturity' "$rdm"; then
443
- echo "$plugin: README contains shields.io maturity badge URL (anti-pattern)" >&2
444
- false
445
- fi
446
- done
447
- }
448
-
449
- # ── Anti-pattern: no compound bootstrapping rendering inside per-skill table cells ─
450
-
451
- @test "plugin-maturity-doc-lint: per-skill table cells do not contain compound bootstrapping rendering" {
452
- # ADR-063 §Bootstrapping clause rendering: compound form
453
- # `(suite-bootstrap window; <N> invocations / 30d)` stays at ROLLUP only;
454
- # per-skill cells carry band name only. This anti-pattern asserts no
455
- # table row contains the `(suite-bootstrap window;` substring (the
456
- # rollup-only compound substring should appear at most ONCE per README,
457
- # in the lead-prose line).
458
- local plugin
459
- for plugin in $(plugins_with_maturity); do
460
- local rdm="$REPO_ROOT/packages/$plugin/README.md"
461
- [ -f "$rdm" ] || continue
462
- # Grep table rows (`|`-leading, allowing leading whitespace) containing
463
- # the bootstrapping compound substring. Table rows are pipe-delimited
464
- # markdown; the compound substring inside a cell is the anti-pattern.
465
- if grep -qE '^[[:space:]]*\|.*\(suite-bootstrap window;' "$rdm"; then
466
- echo "$plugin: README has compound bootstrapping rendering inside table cell (rollup-only contract)" >&2
467
- false
468
- fi
469
- done
470
- }