@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,74 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # P287 regression guard — replaces the historical i2-no-type-branching.bats
4
- # (which asserted no-control-flow-branch-on-type as an I2 invariant).
5
- #
6
- # With the type axis retired per twice-confirmed user direction
7
- # (2026-05-25 + 2026-06-02), the I2 branch-protection invariant is
8
- # vacuous (no `type` field to branch on). This regression guard
9
- # preserves the audit-trail compliance evidence per architect-review
10
- # verdict 2026-06-02 by asserting the POSITIVE state:
11
- #
12
- # - The `**Type**:` body field is GONE from the capture-problem
13
- # skeleton template and from every committed problem ticket.
14
- # - The `lexical_classify_two_sided` helper function is removed.
15
- # - capture-problem SKILL.md no longer carries the Step 1.5 Type
16
- # classification section or its dispatch flags.
17
- #
18
- # Drift here = silent reintroduction of the type axis = P287 regression.
19
- #
20
- # @problem P287 (type-classification retirement)
21
- # @problem P176 (agent-side I2 coverage gap — historical; with the
22
- # type axis retired the gap is vacuous on that axis)
23
- # @adr ADR-052 (behavioural-by-default; this is a regression-guard
24
- # structural assertion per Surface 2 escape-hatch contract — the
25
- # contract surface IS the on-disk artefact state)
26
- # @adr ADR-014 (single-purpose; one mechanical regression invariant)
27
- # @jtbd JTBD-001 (enforce governance without slowing down — primary)
28
-
29
- setup() {
30
- REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
31
- PKG_ROOT="$REPO_ROOT/packages/itil"
32
- SKILL_FILE="$PKG_ROOT/skills/capture-problem/SKILL.md"
33
- HELPER="$REPO_ROOT/packages/shared/derive-first-dispatch.sh"
34
- [ -f "$SKILL_FILE" ]
35
- [ -f "$HELPER" ]
36
- }
37
-
38
- @test "P287: no committed problem ticket carries a **Type**: body field" {
39
- # Find any docs/problems/**/*.md ticket that still carries the
40
- # retired type field on a body line. Any match = P287 regression.
41
- result=$(grep -r --include='*.md' -l '^\*\*Type\*\*: ' "$REPO_ROOT/docs/problems/" 2>/dev/null || true)
42
- [ -z "$result" ]
43
- }
44
-
45
- @test "P287: capture-problem skeleton template does not include a **Type**: line" {
46
- ! grep -F '**Type**: <type_value>' "$SKILL_FILE"
47
- ! grep -F '**Type**: technical' "$SKILL_FILE"
48
- ! grep -F '**Type**: user-business' "$SKILL_FILE"
49
- }
50
-
51
- @test "P287: capture-problem SKILL.md does not carry a Step 1.5 Type classification header" {
52
- # The retired Step 1.5 section was titled "### 1.5 Type classification".
53
- ! grep -E '^### 1\.5 Type classification' "$SKILL_FILE"
54
- }
55
-
56
- @test "P287: shared dispatch helper does not export lexical_classify_two_sided" {
57
- ! grep -E '^lexical_classify_two_sided\(\)' "$HELPER"
58
- }
59
-
60
- @test "P287: per-package lib/ copies are byte-identical to the canonical shared/ source" {
61
- # ADR-017 sync compliance — the strip removed lexical_classify_two_sided
62
- # from the canonical packages/shared/derive-first-dispatch.sh; the
63
- # sync script must propagate the removal to per-package copies.
64
- diff -q "$HELPER" "$REPO_ROOT/packages/itil/lib/derive-first-dispatch.sh"
65
- diff -q "$HELPER" "$REPO_ROOT/packages/architect/lib/derive-first-dispatch.sh"
66
- }
67
-
68
- @test "P287: capture-problem SKILL.md does not carry --type= flag rows (--no-prompt REVIVED per ADR-060 Amendment 2026-06-02 as AFK marker)" {
69
- # The retired Step 1.5 dispatch flags must not appear in the flag table.
70
- # NB: --no-prompt was REVIVED per ADR-060 Amendment 2026-06-02 — see
71
- # capture-problem-step-1-5b-jtbd-trace.bats line 327 (positive assertion).
72
- ! grep -E '^\| `--type=technical`' "$SKILL_FILE"
73
- ! grep -E '^\| `--type=user-business`' "$SKILL_FILE"
74
- }
@@ -1,150 +0,0 @@
1
- #!/usr/bin/env bats
2
- # P474 / STORY-055 — corpus lint: no artefact may mirror an excluded key.
3
- #
4
- # This is a LINT, not a behavioural test, and the distinction is deliberate rather
5
- # than an ADR-052 exception. ADR-052's forbidden class is structural assertion on
6
- # document content *as a proxy for behaviour*; all its documented failure modes
7
- # are proxy failures. Here the content IS the deliverable — "no live artefact
8
- # carries a mirror" is a property of data, and data has no behaviour to assert.
9
- # So it homes under ADR-005's script/data-validation authority, which ADR-052
10
- # explicitly leaves intact.
11
- #
12
- # WHY it exists: P474's mirror audit was a one-time inspection, and a one-time
13
- # inspection has no cadence. The fingerprint excludes four frontmatter keys; if
14
- # any of them acquires a body mirror, accepting a story silently drops its own
15
- # ratification and the no-implement gate then denies its own implementing commit.
16
- # That already happened once with `status`.
17
- #
18
- # The behavioural half lives in the last test: it covers a mirror of ANY excluded
19
- # key in ANY shape, including an HTML meta mirror, which the lint's line-anchored
20
- # bold-key pattern structurally cannot reach.
21
- #
22
- # Two things this lint does NOT catch, recorded rather than implied:
23
- # 1. The story-map `href` leg (open P474 task) — a card links
24
- # `../../stories/<state>/…`, so the lifecycle directory sits inside the map's
25
- # hashed content while the card's `data-status` is normalised out. No
26
- # bold-key pattern can see that.
27
- # 2. A mirror written with leading whitespace or inside a list item — the cost
28
- # of line-anchoring, taken so no exclusion set is needed.
29
- #
30
- # @problem P474 @adr ADR-090 ADR-005 ADR-052 @story STORY-055
31
-
32
- setup() {
33
- REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
34
- LIB="${REPO_ROOT}/packages/itil/lib/story-oversight.sh"
35
- # shellcheck source=/dev/null
36
- source "$LIB"
37
- STORIES="$REPO_ROOT/docs/stories"
38
- MAPS="$REPO_ROOT/docs/story-maps"
39
- TMPD="$(mktemp -d)"
40
- }
41
- teardown() { rm -rf "$TMPD"; }
42
-
43
- # Build the pattern from the lib's own accessor, so a key added to the exclusion
44
- # set is covered without editing this file. Case-INSENSITIVE is load-bearing: the
45
- # accessor holds lowercase keys (`status`) while every real mirror is Title-Case
46
- # (`**Status**:`), so a case-sensitive pattern built from the accessor matches
47
- # nothing on arrival AND stays green when a mirror is reintroduced.
48
- mirror_pattern() {
49
- local alt; alt="$(oversight_excluded_keys | paste -sd'|' -)"
50
- printf '^\\*\\*(%s)\\*\\*[[:space:]]*:' "$alt"
51
- }
52
-
53
- # Line-anchored on purpose. Unanchored, this matches four lines of legitimate
54
- # prose *about* the defect (the stories README, two lines in STORY-054, and
55
- # STORY-055's own problem trace), which would redden on arrival and require an
56
- # exclusion list growing with every future story that discusses a mirror.
57
- scan() {
58
- # -H is load-bearing, not decoration. Given exactly ONE file argument, GNU grep
59
- # OMITS the filename prefix while this machine's grep emits it — so a fixture
60
- # scan over a single file produced output containing the matched line but not
61
- # the file it came from, and the assertion naming that file passed locally and
62
- # failed on CI. Forcing the prefix makes output identical across grep
63
- # implementations, and makes the two production diagnostics below name the
64
- # offending artefact rather than only quoting the line.
65
- grep -rHniE "$(mirror_pattern)" "$@" 2>/dev/null || true
66
- }
67
-
68
- @test "lint: the excluded-key accessor is non-empty and the pattern is well-formed" {
69
- # Guard against a silently-empty pattern — the whole lint would go vacuous.
70
- local keys; keys="$(oversight_excluded_keys)"
71
- [ -n "$keys" ]
72
- [ "$(printf '%s\n' "$keys" | wc -l | tr -d ' ')" -ge 4 ]
73
- [[ "$(mirror_pattern)" == *'status'* ]]
74
- }
75
-
76
- @test "lint: search roots exist and hold artefacts — fails loudly, never vacuously" {
77
- # A renamed or adopter-relative path must fail here rather than let the scan
78
- # below pass by matching nothing. This is the shape that reddened main once.
79
- [ -d "$STORIES" ]
80
- [ -d "$MAPS" ]
81
- local n
82
- n="$(find "$STORIES" -name 'STORY-*.md' -type f 2>/dev/null | wc -l | tr -d ' ')"
83
- [ "$n" -ge 1 ] || { echo "no STORY-*.md under $STORIES — lint would be vacuous"; false; }
84
- n="$(find "$MAPS" -name '*.html' -type f 2>/dev/null | wc -l | tr -d ' ')"
85
- [ "$n" -ge 1 ] || { echo "no maps under $MAPS — lint would be vacuous"; false; }
86
- }
87
-
88
- @test "lint: no story artefact mirrors an excluded frontmatter key in its body" {
89
- local hits
90
- hits="$(scan "$STORIES"/*/STORY-*.md)"
91
- [ -z "$hits" ] || {
92
- printf 'body mirror of an excluded key found — accepting these stories will drop their ratification:\n%s\n' "$hits"
93
- printf 'remove the line; run `wr-itil-migrate-story-status-mirror docs/stories` for an existing corpus.\n'
94
- false
95
- }
96
- }
97
-
98
- @test "lint: no story map mirrors an excluded frontmatter key in its body" {
99
- # The map's own `<meta name="status">` is the key's HOME in the HTML encoding,
100
- # not a mirror of it, and the lib's filter already excludes it — so the
101
- # line-anchored bold-key pattern correctly does not flag it.
102
- local hits
103
- hits="$(scan "$MAPS"/*/*.html)"
104
- [ -z "$hits" ] || {
105
- printf 'body mirror of an excluded key found in a map:\n%s\n' "$hits"
106
- false
107
- }
108
- }
109
-
110
- @test "lint: RED on an injected mirror — proves the pattern actually fires" {
111
- mkdir -p "$TMPD/stories/draft"
112
- printf -- '---\nstatus: accepted\n---\n\n# STORY-999: x\n\n**Status**: accepted\n**Reported**: 2026-07-30\n' \
113
- > "$TMPD/stories/draft/STORY-999-x.md"
114
- local hits; hits="$(scan "$TMPD/stories"/*/STORY-*.md)"
115
- [ -n "$hits" ] || { echo "lint failed to detect an injected line-start **Status**: mirror"; false; }
116
- [[ "$hits" == *"STORY-999"* ]]
117
- }
118
-
119
- @test "lint: prose ABOUT a mirror is not flagged — that is what line-anchoring buys" {
120
- mkdir -p "$TMPD/prose/draft"
121
- printf -- '---\nstatus: accepted\n---\n\n# STORY-998: y\n\nThe `**Status**:` line used to be hashed.\n<!-- No **Status**: body line, deliberately. -->\n' \
122
- > "$TMPD/prose/draft/STORY-998-y.md"
123
- [ -z "$(scan "$TMPD/prose"/*/STORY-*.md)" ]
124
- }
125
-
126
- # The behavioural half. Covers a mirror of ANY excluded key in ANY shape,
127
- # including the HTML meta form the lint's pattern cannot reach.
128
- @test "behavioural: a mirror of any excluded key drifts a ratified artefact" {
129
- local k
130
- for k in $(oversight_excluded_keys); do
131
- # Skip the marker keys themselves — those are excluded BY the marker write,
132
- # so a "mirror" of them is what the filter is for.
133
- case "$k" in human-oversight|oversight-hash|oversight-basis) continue ;; esac
134
- local f="$TMPD/mirror-$k.md"
135
- printf -- '---\n%s: draft\nhuman-oversight: confirmed\noversight-hash: P\n---\n\n# STORY-997\n\nbody\n' "$k" > "$f"
136
- local h; h="$(oversight_content_hash "$f")"
137
- sed -i.bak "s/oversight-hash: P/oversight-hash: $h/" "$f" && rm -f "$f.bak"
138
- run is_story_map_ratified "$f"
139
- [ "$status" -eq 0 ]
140
- # Add a body mirror of that key, then advance BOTH copies the way a
141
- # transition would. The frontmatter change alone is excluded; the body
142
- # mirror is not — so ratification must drop, which is P474 exactly.
143
- printf -- '**%s**: accepted\n' "$k" >> "$f"
144
- h="$(oversight_content_hash "$f")"
145
- sed -i.bak "s/^oversight-hash: .*/oversight-hash: $h/" "$f" && rm -f "$f.bak"
146
- sed -i.bak "s/^${k}: draft/${k}: accepted/; s/^\\*\\*${k}\\*\\*: accepted/**${k}**: DIFFERENT/" "$f" && rm -f "$f.bak"
147
- run is_story_map_ratified "$f"
148
- [ "$status" -ne 0 ] || { echo "a body mirror of '$k' did not drift the hash"; false; }
149
- done
150
- }
@@ -1,386 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # @problem P087 — Phase 2b git-axis script behavioural confirmation.
4
- #
5
- # Contract under test: `packages/itil/scripts/plugin-exercise-index.sh` runs
6
- # `git log --since=<N>d --name-only --pretty=format:%H|%aI|%s` once at the
7
- # project root, auto-discovers plugins by listing `packages/*/`, and emits
8
- # one NDJSON record per plugin with the v1.0 schema fields per ADR-058
9
- # §Script contracts (line 87-113). Exit code 0 always per ADR-013 Rule 6
10
- # fail-safe (outside-git-repo, missing `packages/`, opt-out marker all hit
11
- # the zero-records/stderr-comment path).
12
- #
13
- # Confirmation criteria 6-8 from ADR-058 §Confirmation are the load-bearing
14
- # behavioural assertions in this file. Sibling to Phase 2a's
15
- # `skill-invocations.bats` (criteria 1-5).
16
- #
17
- # @adr ADR-058 (Plugin maturity measurement mechanism)
18
- # @adr ADR-049 (Shim grammar — `wr-itil-plugin-exercise-index` on $PATH)
19
- # @adr ADR-035 (Privacy posture — opt-out marker, no network primitive,
20
- # content sanitisation — commit subjects parsed only for `BREAKING|feat!|fix!`
21
- # token presence, never echoed to stdout)
22
- # @adr ADR-052 (Behavioural tests default — NDJSON-output-driven against
23
- # fixture git repos, not source-greps on script body; the no-network
24
- # negative-grep at Confirmation #3 is the documented carve-out)
25
- # @adr ADR-013 Rule 6 (non-interactive fail-safe — exit 0 always)
26
- # @jtbd JTBD-101 (Extend the Suite — hardening-prioritisation outcome,
27
- # commits_window + closed_tickets_window + days_shipped serve the
28
- # git-axis half of the 2026-05-04 outcome amendment)
29
- # @jtbd JTBD-201 (Restore Service Fast — audit-trail composition)
30
-
31
- setup() {
32
- SCRIPTS_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
33
- SCRIPT="$SCRIPTS_DIR/plugin-exercise-index.sh"
34
- FIXTURE_DIR="$(mktemp -d)"
35
- REPO_ROOT="$FIXTURE_DIR/repo"
36
- mkdir -p "$REPO_ROOT"
37
- }
38
-
39
- teardown() {
40
- rm -rf "$FIXTURE_DIR"
41
- }
42
-
43
- # Helper: initialise a temp git repo at REPO_ROOT with a stable author/email
44
- # (otherwise `git commit` aborts on systems without user.name configured).
45
- init_repo() {
46
- (
47
- cd "$REPO_ROOT"
48
- git init -q -b main
49
- git config user.email "bats@example.com"
50
- git config user.name "bats"
51
- git config commit.gpgsign false
52
- )
53
- }
54
-
55
- # Helper: stage a file under packages/<plugin>/ and commit at a given date.
56
- # Date is ISO 8601 (e.g. 2026-05-01T12:00:00) — fed to GIT_AUTHOR_DATE /
57
- # GIT_COMMITTER_DATE so log --since works deterministically.
58
- commit_under_plugin() {
59
- local plugin="$1"; local relpath="$2"; local subject="$3"; local date="$4"
60
- local full="$REPO_ROOT/packages/$plugin/$relpath"
61
- mkdir -p "$(dirname "$full")"
62
- echo "content-$RANDOM" >> "$full"
63
- (
64
- cd "$REPO_ROOT"
65
- GIT_AUTHOR_DATE="$date" GIT_COMMITTER_DATE="$date" \
66
- git -c user.email=bats@example.com -c user.name=bats \
67
- add "packages/$plugin/$relpath" >/dev/null
68
- GIT_AUTHOR_DATE="$date" GIT_COMMITTER_DATE="$date" \
69
- git -c user.email=bats@example.com -c user.name=bats \
70
- commit -q -m "$subject"
71
- )
72
- }
73
-
74
- # Helper: ISO timestamp N days ago (UTC). Cross-platform — uses python3.
75
- days_ago_iso() {
76
- python3 -c "import sys, datetime; print((datetime.datetime.utcnow() - datetime.timedelta(days=int(sys.argv[1]))).strftime('%Y-%m-%dT%H:%M:%S'))" "$1"
77
- }
78
-
79
- # ── Existence / executable ──────────────────────────────────────────────────
80
-
81
- @test "plugin-exercise-index: canonical script exists" {
82
- [ -f "$SCRIPT" ]
83
- }
84
-
85
- @test "plugin-exercise-index: canonical script is executable" {
86
- [ -x "$SCRIPT" ]
87
- }
88
-
89
- @test "plugin-exercise-index: shim file exists with ADR-049 grammar" {
90
- local shim="$SCRIPTS_DIR/../bin/wr-itil-plugin-exercise-index"
91
- [ -f "$shim" ]
92
- [ -x "$shim" ]
93
- grep -q 'exec.*scripts/plugin-exercise-index.sh' "$shim"
94
- }
95
-
96
- # ── Confirmation #6: git-axis composite fixture ─────────────────────────────
97
- # Seed a temp git repo with packages/dummy/ + three commits (one in window,
98
- # two out of window). Assert NDJSON record for plugin="dummy" has
99
- # commits_window=1 under the 60-day default window.
100
-
101
- @test "Confirmation #6: three commits, one in window, commits_window=1" {
102
- init_repo
103
- local in_window=$(days_ago_iso 10)
104
- local out_window_1=$(days_ago_iso 90)
105
- local out_window_2=$(days_ago_iso 120)
106
- commit_under_plugin "dummy" "src/a.txt" "feat: in-window change" "$in_window"
107
- commit_under_plugin "dummy" "src/b.txt" "feat: out-of-window 1" "$out_window_1"
108
- commit_under_plugin "dummy" "src/c.txt" "feat: out-of-window 2" "$out_window_2"
109
-
110
- run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
111
- [ "$status" -eq 0 ]
112
- # Exactly one NDJSON record for plugin="dummy"
113
- local rec
114
- rec="$(echo "$output" | grep '"plugin":"dummy"')"
115
- [ -n "$rec" ]
116
- echo "$rec" | python3 -c "
117
- import json, sys
118
- r = json.loads(sys.stdin.read().strip())
119
- assert r['schema_version'] == '1.0', r
120
- assert r['axis'] == 'plugin-exercise-index', r
121
- assert r['plugin'] == 'dummy', r
122
- assert r['commits_window'] == 1, r
123
- assert r['window_days'] == 60, r
124
- assert r['days_shipped'] >= 120, r
125
- "
126
- }
127
-
128
- @test "Confirmation #6: emits one record per discovered plugin" {
129
- init_repo
130
- local ts=$(days_ago_iso 5)
131
- commit_under_plugin "alpha" "src/a.txt" "feat: alpha change" "$ts"
132
- commit_under_plugin "beta" "src/b.txt" "feat: beta change" "$ts"
133
-
134
- run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
135
- [ "$status" -eq 0 ]
136
- echo "$output" | grep -q '"plugin":"alpha"'
137
- echo "$output" | grep -q '"plugin":"beta"'
138
- # Line count = number of discovered plugins (alpha + beta)
139
- local line_count
140
- line_count="$(printf '%s' "$output" | grep -c .)"
141
- [ "$line_count" -eq 2 ]
142
- }
143
-
144
- @test "Confirmation #6: commit subject containing literal pipe parses correctly" {
145
- # ADR-058 line 89 pins `--pretty=format:%H|%aI|%s` — the parser must
146
- # split on first two `|` only (line.split('|', 2)) so subjects with
147
- # literal `|` characters do not corrupt parsing. Architect advisory.
148
- init_repo
149
- local ts=$(days_ago_iso 5)
150
- commit_under_plugin "pipey" "src/a.txt" "feat: subject with | pipe in it" "$ts"
151
-
152
- run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
153
- [ "$status" -eq 0 ]
154
- echo "$output" | grep -q '"plugin":"pipey"'
155
- echo "$output" | grep -q '"commits_window":1'
156
- }
157
-
158
- # ── Confirmation #7: outside-git-repo fixture ───────────────────────────────
159
-
160
- @test "Confirmation #7: outside-git-repo emits zero records, stderr comment, exit 0" {
161
- # REPO_ROOT exists but no `git init` ran — it is not a git repo.
162
- local stdout_file="$FIXTURE_DIR/nogit.out"
163
- local stderr_file="$FIXTURE_DIR/nogit.err"
164
- "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT" >"$stdout_file" 2>"$stderr_file"
165
- local rc=$?
166
- [ "$rc" -eq 0 ]
167
- [ ! -s "$stdout_file" ]
168
- grep -q "not a git repository" "$stderr_file"
169
- }
170
-
171
- @test "Confirmation #7: missing packages/ directory emits zero records, exit 0" {
172
- init_repo
173
- # Repo has no packages/ directory at all. Make a non-packages commit so
174
- # `git log` returns something but no plugins are discovered.
175
- (
176
- cd "$REPO_ROOT"
177
- echo readme > README.md
178
- GIT_AUTHOR_DATE="$(days_ago_iso 5)" GIT_COMMITTER_DATE="$(days_ago_iso 5)" \
179
- git -c user.email=bats@example.com -c user.name=bats add README.md >/dev/null
180
- GIT_AUTHOR_DATE="$(days_ago_iso 5)" GIT_COMMITTER_DATE="$(days_ago_iso 5)" \
181
- git -c user.email=bats@example.com -c user.name=bats commit -q -m "feat: readme"
182
- )
183
-
184
- local stdout_file="$FIXTURE_DIR/nopkg.out"
185
- local stderr_file="$FIXTURE_DIR/nopkg.err"
186
- "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT" >"$stdout_file" 2>"$stderr_file"
187
- local rc=$?
188
- [ "$rc" -eq 0 ]
189
- [ ! -s "$stdout_file" ]
190
- grep -q "no packages/ directory" "$stderr_file"
191
- }
192
-
193
- # ── Confirmation #8: schema-version contract ────────────────────────────────
194
-
195
- @test "Confirmation #8: every NDJSON record has schema_version=1.0" {
196
- init_repo
197
- local ts=$(days_ago_iso 5)
198
- commit_under_plugin "p1" "src/a.txt" "feat: p1" "$ts"
199
- commit_under_plugin "p2" "src/b.txt" "feat: p2" "$ts"
200
-
201
- run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
202
- [ "$status" -eq 0 ]
203
- # Validate every line carries schema_version="1.0"
204
- echo "$output" | python3 -c "
205
- import json, sys
206
- for line in sys.stdin:
207
- line = line.strip()
208
- if not line: continue
209
- r = json.loads(line)
210
- assert r['schema_version'] == '1.0', r
211
- assert r['axis'] == 'plugin-exercise-index', r
212
- "
213
- }
214
-
215
- # ── Privacy: opt-out marker ─────────────────────────────────────────────────
216
-
217
- @test "opt-out marker disables reads, stderr comment, exit 0" {
218
- init_repo
219
- local ts=$(days_ago_iso 5)
220
- commit_under_plugin "x" "src/a.txt" "feat: would-be-recorded" "$ts"
221
- mkdir -p "$REPO_ROOT/.claude"
222
- touch "$REPO_ROOT/.claude/.skill-metrics-opt-out"
223
-
224
- local stdout_file="$FIXTURE_DIR/opt.out"
225
- local stderr_file="$FIXTURE_DIR/opt.err"
226
- "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT" >"$stdout_file" 2>"$stderr_file"
227
- local rc=$?
228
- [ "$rc" -eq 0 ]
229
- [ ! -s "$stdout_file" ]
230
- grep -q "opt-out marker present at" "$stderr_file"
231
- }
232
-
233
- # ── Privacy: no-network-primitive (ADR-052 negative-grep carve-out) ─────────
234
-
235
- @test "canonical body contains no network primitives" {
236
- run grep -nE '\bcurl\b|\bwget\b|\bnc[[:space:]]|\bfetch\b|http\.client|\burllib\b' "$SCRIPT"
237
- [ "$status" -eq 1 ]
238
- [ -z "$output" ]
239
- }
240
-
241
- # ── Privacy: commit-subject prose never leaks beyond breaking-marker test ───
242
-
243
- @test "commit subjects never appear in NDJSON output beyond BREAKING/feat!/fix! parse" {
244
- init_repo
245
- local ts=$(days_ago_iso 5)
246
- # Subject contains a recognisable token that must NOT echo to stdout.
247
- commit_under_plugin "secrety" "src/a.txt" "feat: contains SECRETPROSEXYZ token" "$ts"
248
-
249
- run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
250
- [ "$status" -eq 0 ]
251
- echo "$output" | grep -q '"plugin":"secrety"'
252
- ! echo "$output" | grep -q "SECRETPROSEXYZ"
253
- }
254
-
255
- # ── composite_index calculation ─────────────────────────────────────────────
256
-
257
- @test "composite_index = log10(commits+1) + log10(closed+1) + days_shipped_bonus" {
258
- init_repo
259
- # Set the OLDEST commit > 60 days ago to earn the days_shipped >= 60 bonus
260
- # of +1.0. Then add 9 in-window commits so log10(9+1) = 1.0.
261
- commit_under_plugin "calc" "src/old.txt" "feat: old" "$(days_ago_iso 90)"
262
- for i in 1 2 3 4 5 6 7 8 9; do
263
- commit_under_plugin "calc" "src/n$i.txt" "feat: n$i" "$(days_ago_iso 5)"
264
- done
265
-
266
- run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
267
- [ "$status" -eq 0 ]
268
- # commits_window=9 (only the 9 recent ones), days_shipped >= 60 (90 days
269
- # since oldest), closed_tickets_window=0 (no docs/problems/).
270
- # composite_index = log10(10) + log10(1) + 1.0 = 1.0 + 0.0 + 1.0 = 2.0
271
- echo "$output" | grep '"plugin":"calc"' | python3 -c "
272
- import json, sys
273
- r = json.loads(sys.stdin.read().strip())
274
- assert r['commits_window'] == 9, r
275
- assert r['days_shipped'] >= 60, r
276
- assert r['closed_tickets_window'] == 0, r
277
- assert abs(r['composite_index'] - 2.0) < 0.01, r
278
- "
279
- }
280
-
281
- @test "composite_index respects days_shipped < 60 (no bonus)" {
282
- init_repo
283
- # All commits within last 30 days; days_shipped < 60.
284
- for i in 1 2 3 4 5 6 7 8 9; do
285
- commit_under_plugin "young" "src/n$i.txt" "feat: n$i" "$(days_ago_iso 5)"
286
- done
287
-
288
- run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
289
- [ "$status" -eq 0 ]
290
- # commits_window=9, days_shipped=5, closed_tickets_window=0
291
- # composite_index = log10(10) + log10(1) + 0.0 = 1.0
292
- echo "$output" | grep '"plugin":"young"' | python3 -c "
293
- import json, sys
294
- r = json.loads(sys.stdin.read().strip())
295
- assert r['commits_window'] == 9, r
296
- assert r['days_shipped'] < 60, r
297
- assert abs(r['composite_index'] - 1.0) < 0.01, r
298
- "
299
- }
300
-
301
- # ── closed_tickets_window ──────────────────────────────────────────────────
302
-
303
- @test "closed_tickets_window counts .closed/.verifying tickets citing the plugin" {
304
- init_repo
305
- local ts=$(days_ago_iso 5)
306
- commit_under_plugin "cited" "src/a.txt" "feat: cited" "$ts"
307
- # Seed two closed tickets in docs/problems/ — one cites packages/cited/,
308
- # one does not. Both have recent mtime so the 90-day window includes them.
309
- mkdir -p "$REPO_ROOT/docs/problems"
310
- echo "## Related
311
- - packages/cited/scripts/foo.sh" > "$REPO_ROOT/docs/problems/100-something.closed.md"
312
- echo "## Related
313
- - packages/other/scripts/bar.sh" > "$REPO_ROOT/docs/problems/101-other.closed.md"
314
-
315
- run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
316
- [ "$status" -eq 0 ]
317
- echo "$output" | grep '"plugin":"cited"' | python3 -c "
318
- import json, sys
319
- r = json.loads(sys.stdin.read().strip())
320
- assert r['closed_tickets_window'] == 1, r
321
- "
322
- }
323
-
324
- # ── breaking_change_age_days ───────────────────────────────────────────────
325
-
326
- @test "breaking_change_age_days surfaces BREAKING/feat!/fix! marker presence" {
327
- init_repo
328
- local recent=$(days_ago_iso 3)
329
- local older=$(days_ago_iso 20)
330
- commit_under_plugin "brk" "src/a.txt" "feat: normal change" "$older"
331
- commit_under_plugin "brk" "src/b.txt" "feat!: breaking change" "$recent"
332
-
333
- run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
334
- [ "$status" -eq 0 ]
335
- echo "$output" | grep '"plugin":"brk"' | python3 -c "
336
- import json, sys
337
- r = json.loads(sys.stdin.read().strip())
338
- v = r['breaking_change_age_days']
339
- assert v is not None, r
340
- assert 0 <= v <= 7, r
341
- "
342
- }
343
-
344
- @test "breaking_change_age_days is null when no breaking marker in window" {
345
- init_repo
346
- local ts=$(days_ago_iso 5)
347
- commit_under_plugin "nobreak" "src/a.txt" "feat: ordinary" "$ts"
348
- commit_under_plugin "nobreak" "src/b.txt" "fix: ordinary" "$ts"
349
-
350
- run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT"
351
- [ "$status" -eq 0 ]
352
- echo "$output" | grep '"plugin":"nobreak"' | python3 -c "
353
- import json, sys
354
- r = json.loads(sys.stdin.read().strip())
355
- assert r['breaking_change_age_days'] is None, r
356
- "
357
- }
358
-
359
- # ── Forward-extension flag: --category-overrides ────────────────────────────
360
-
361
- @test "category-overrides flag is accepted without functional effect" {
362
- init_repo
363
- commit_under_plugin "cat" "src/a.txt" "feat: cat" "$(days_ago_iso 5)"
364
- echo '{}' > "$FIXTURE_DIR/overrides.json"
365
-
366
- run "$SCRIPT" --window-days=60 --project-root="$REPO_ROOT" --category-overrides="$FIXTURE_DIR/overrides.json"
367
- [ "$status" -eq 0 ]
368
- echo "$output" | grep -q '"plugin":"cat"'
369
- }
370
-
371
- # ── Window-days filter ──────────────────────────────────────────────────────
372
-
373
- @test "window-days filter excludes commits older than the window" {
374
- init_repo
375
- commit_under_plugin "win" "src/old.txt" "feat: old" "$(days_ago_iso 30)"
376
- commit_under_plugin "win" "src/new.txt" "feat: new" "$(days_ago_iso 1)"
377
-
378
- run "$SCRIPT" --window-days=7 --project-root="$REPO_ROOT"
379
- [ "$status" -eq 0 ]
380
- echo "$output" | grep '"plugin":"win"' | python3 -c "
381
- import json, sys
382
- r = json.loads(sys.stdin.read().strip())
383
- assert r['commits_window'] == 1, r
384
- assert r['window_days'] == 7, r
385
- "
386
- }