@windyroad/itil 0.56.0-preview.878 → 0.56.0-preview.880

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,51 @@
1
+ #!/usr/bin/env bash
2
+ # Generated by scripts/sync-shim-wrappers.sh from
3
+ # packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
4
+ # shim files in packages/*/bin/wr-* directly; edit the template + run
5
+ # `npm run sync:shim-wrappers` to regenerate.
6
+ #
7
+ # Resolution (ADR-080):
8
+ # 1. If the wrapper's parent dir is semver-shaped, treat as installed-
9
+ # cache execution and resolve to the highest-version sibling's
10
+ # scripts/ entry below.
11
+ # 2. Otherwise (parent dir is e.g. `architect`), treat as source-
12
+ # monorepo execution and dispatch to own scripts/. The source-repo-
13
+ # guard `exec` is the anchor parsed by
14
+ # packages/retrospective/scripts/check-tarball-shipped-shims.sh.
15
+ # 3. If the cache parent contains zero semver-shaped siblings, exit
16
+ # 127 with a stderr message naming the cache parent (per SQ-080-2).
17
+ #
18
+ # @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
19
+ # @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
20
+ # @problem P343 (mid-session staleness window)
21
+
22
+ set -euo pipefail
23
+
24
+ SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
25
+ OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
26
+ OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
27
+ CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
28
+
29
+ SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
30
+
31
+ # Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
32
+ if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
33
+ exec "$SHIM_DIR/../scripts/check-rfc-stories-ratified.sh" "$@"
34
+ fi
35
+
36
+ # Cache execution: pick the highest-semver sibling under CACHE_PARENT.
37
+ HIGHEST=""
38
+ while IFS= read -r dir; do
39
+ name="$(basename "$dir")"
40
+ [[ "$name" =~ $SEMVER_RE ]] || continue
41
+ if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
42
+ HIGHEST="$name"
43
+ fi
44
+ done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
45
+
46
+ if [[ -z "$HIGHEST" ]]; then
47
+ printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
48
+ exit 127
49
+ fi
50
+
51
+ exec "$CACHE_PARENT/$HIGHEST/scripts/check-rfc-stories-ratified.sh" "$@"
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # Generated by scripts/sync-shim-wrappers.sh from
3
+ # packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
4
+ # shim files in packages/*/bin/wr-* directly; edit the template + run
5
+ # `npm run sync:shim-wrappers` to regenerate.
6
+ #
7
+ # Resolution (ADR-080):
8
+ # 1. If the wrapper's parent dir is semver-shaped, treat as installed-
9
+ # cache execution and resolve to the highest-version sibling's
10
+ # scripts/ entry below.
11
+ # 2. Otherwise (parent dir is e.g. `architect`), treat as source-
12
+ # monorepo execution and dispatch to own scripts/. The source-repo-
13
+ # guard `exec` is the anchor parsed by
14
+ # packages/retrospective/scripts/check-tarball-shipped-shims.sh.
15
+ # 3. If the cache parent contains zero semver-shaped siblings, exit
16
+ # 127 with a stderr message naming the cache parent (per SQ-080-2).
17
+ #
18
+ # @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
19
+ # @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
20
+ # @problem P343 (mid-session staleness window)
21
+
22
+ set -euo pipefail
23
+
24
+ SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
25
+ OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
26
+ OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
27
+ CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
28
+
29
+ SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
30
+
31
+ # Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
32
+ if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
33
+ exec "$SHIM_DIR/../scripts/detect-unratified-stories-maps.sh" "$@"
34
+ fi
35
+
36
+ # Cache execution: pick the highest-semver sibling under CACHE_PARENT.
37
+ HIGHEST=""
38
+ while IFS= read -r dir; do
39
+ name="$(basename "$dir")"
40
+ [[ "$name" =~ $SEMVER_RE ]] || continue
41
+ if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
42
+ HIGHEST="$name"
43
+ fi
44
+ done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
45
+
46
+ if [[ -z "$HIGHEST" ]]; then
47
+ printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
48
+ exit 127
49
+ fi
50
+
51
+ exec "$CACHE_PARENT/$HIGHEST/scripts/detect-unratified-stories-maps.sh" "$@"
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # Generated by scripts/sync-shim-wrappers.sh from
3
+ # packages/shared/lib/shim-wrapper-template.sh. DO NOT EDIT individual
4
+ # shim files in packages/*/bin/wr-* directly; edit the template + run
5
+ # `npm run sync:shim-wrappers` to regenerate.
6
+ #
7
+ # Resolution (ADR-080):
8
+ # 1. If the wrapper's parent dir is semver-shaped, treat as installed-
9
+ # cache execution and resolve to the highest-version sibling's
10
+ # scripts/ entry below.
11
+ # 2. Otherwise (parent dir is e.g. `architect`), treat as source-
12
+ # monorepo execution and dispatch to own scripts/. The source-repo-
13
+ # guard `exec` is the anchor parsed by
14
+ # packages/retrospective/scripts/check-tarball-shipped-shims.sh.
15
+ # 3. If the cache parent contains zero semver-shaped siblings, exit
16
+ # 127 with a stderr message naming the cache parent (per SQ-080-2).
17
+ #
18
+ # @adr ADR-080 (highest-version-wins shim wrapper plugin scaffold)
19
+ # @adr ADR-049 (plugin-bundled scripts resolve via bin/ on $PATH — amended)
20
+ # @problem P343 (mid-session staleness window)
21
+
22
+ set -euo pipefail
23
+
24
+ SHIM_DIR="$(cd "$(dirname "$0")" && pwd)"
25
+ OWN_VERSION_DIR="$(dirname "$SHIM_DIR")"
26
+ OWN_VERSION_NAME="$(basename "$OWN_VERSION_DIR")"
27
+ CACHE_PARENT="$(dirname "$OWN_VERSION_DIR")"
28
+
29
+ SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$'
30
+
31
+ # Source-repo guard: own parent dir is NOT semver → dispatch to own scripts/.
32
+ if ! [[ "$OWN_VERSION_NAME" =~ $SEMVER_RE ]]; then
33
+ exec "$SHIM_DIR/../scripts/mark-story-oversight-confirmed.sh" "$@"
34
+ fi
35
+
36
+ # Cache execution: pick the highest-semver sibling under CACHE_PARENT.
37
+ HIGHEST=""
38
+ while IFS= read -r dir; do
39
+ name="$(basename "$dir")"
40
+ [[ "$name" =~ $SEMVER_RE ]] || continue
41
+ if [[ -z "$HIGHEST" ]] || [[ "$(printf '%s\n%s\n' "$HIGHEST" "$name" | sort -V | tail -1)" == "$name" ]]; then
42
+ HIGHEST="$name"
43
+ fi
44
+ done < <(find "$CACHE_PARENT" -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
45
+
46
+ if [[ -z "$HIGHEST" ]]; then
47
+ printf 'wr-shim: no cached versions in %s\n' "$CACHE_PARENT" >&2
48
+ exit 127
49
+ fi
50
+
51
+ exec "$CACHE_PARENT/$HIGHEST/scripts/mark-story-oversight-confirmed.sh" "$@"
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env bash
2
+ # story-oversight.sh — ADR-090 lazy-fingerprint ratification helpers.
3
+ #
4
+ # Shared by detect-unratified-stories-maps.sh, check-rfc-stories-ratified.sh, and
5
+ # mark-story-oversight-confirmed.sh so all three agree on ONE hash definition
6
+ # (if they diverged, a freshly-ratified artefact would read as drifted forever).
7
+ #
8
+ # A story/map is RATIFIED when it carries a `confirmed` human-oversight marker
9
+ # AND a stored oversight-hash that matches a fresh hash of its content-minus-
10
+ # marker. Any content edit changes the hash → the artefact reads as drifted /
11
+ # unratified until re-ratified. This is ADR-090's drift-invalidation (ADR-009
12
+ # drift lineage, NOT ADR-066 write-once) — the same hash-the-artefact pattern
13
+ # the external-comms gate uses.
14
+ #
15
+ # Sourced, not executed. Requires `shasum` (BSD + coreutils both ship it).
16
+
17
+ # Stable hash of the artefact's content EXCLUDING the two marker lines, so
18
+ # writing/updating the marker is idempotent w.r.t. the hash. Covers both
19
+ # encodings in one filter:
20
+ # - markdown: `human-oversight:` / `oversight-hash:` frontmatter lines
21
+ # - HTML: <meta name="human-oversight" ...> / <meta name="oversight-hash" ...>
22
+ oversight_content_hash() {
23
+ grep -vE '^(human-oversight|oversight-hash):|<meta[^>]*name="(human-oversight|oversight-hash)"' "$1" \
24
+ | shasum -a 256 | awk '{print $1}'
25
+ }
26
+
27
+ # Echo the stored oversight-hash (md frontmatter OR HTML meta), empty if none.
28
+ oversight_stored_hash() {
29
+ local h
30
+ h="$(grep -oE '^oversight-hash:[[:space:]]*[a-f0-9]{64}' "$1" 2>/dev/null | grep -oE '[a-f0-9]{64}' | head -1)"
31
+ [ -z "$h" ] && h="$(grep -oE '<meta[^>]*name="oversight-hash"[^>]*content="[a-f0-9]{64}"' "$1" 2>/dev/null | grep -oE '[a-f0-9]{64}' | head -1)"
32
+ printf '%s' "$h"
33
+ }
34
+
35
+ # True (0) if the file carries a `confirmed` human-oversight marker (md or HTML).
36
+ oversight_is_confirmed() {
37
+ grep -qiE '^human-oversight:[[:space:]]*confirmed([[:space:]]|$)' "$1" && return 0
38
+ grep -qiE '<meta[^>]*name="human-oversight"[^>]*content="confirmed"' "$1" && return 0
39
+ return 1
40
+ }
41
+
42
+ # True (0) if RATIFIED: confirmed AND a stored hash that matches current content.
43
+ # A confirmed marker with NO stored hash (legacy hand-ratified) is NOT ratified —
44
+ # it must be re-ratified once to gain its fingerprint.
45
+ is_story_map_ratified() {
46
+ local f="$1" stored
47
+ oversight_is_confirmed "$f" || return 1
48
+ stored="$(oversight_stored_hash "$f")"
49
+ [ -z "$stored" ] && return 1
50
+ [ "$stored" = "$(oversight_content_hash "$f")" ]
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/itil",
3
- "version": "0.56.0-preview.878",
3
+ "version": "0.56.0-preview.880",
4
4
  "description": "ITIL-aligned IT service management for Claude Code (problem, and future incident/change skills)",
5
5
  "bin": {
6
6
  "windyroad-itil": "./bin/install.mjs"
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env bash
2
+ # check-rfc-stories-ratified.sh — ADR-090 (an RFC may reference only RATIFIED stories).
3
+ #
4
+ # For each STORY-NNN in the RFC's `stories:` frontmatter, resolve the story file
5
+ # under <stories-root>/*/STORY-NNN-*.md and verify it carries
6
+ # `human-oversight: confirmed`. Exit non-zero (with a stderr directive naming the
7
+ # offending stories) if any listed story is unratified, unconfirmed, or missing.
8
+ #
9
+ # Composes with check-rfc-has-stories.sh (ADR-089): has-stories checks >=1 story
10
+ # exists; this checks each listed story is ratified. An empty `stories:` passes
11
+ # vacuously here (the has-stories gate owns the emptiness rejection).
12
+ #
13
+ # Usage: check-rfc-stories-ratified.sh <rfc-file> [stories-root=docs/stories]
14
+ # Exit: 0 = all listed stories ratified (or none listed); 1 = >=1 unratified/missing;
15
+ # 2 = usage / file error.
16
+ #
17
+ # Authority: ADR-090. Driver: P404 Phase 2. Test: check-rfc-stories-ratified.bats.
18
+ set -euo pipefail
19
+
20
+ # Adopter-safe: source the shared lazy-fingerprint lib RELATIVE TO THIS SCRIPT (P317).
21
+ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
22
+ echo "check-rfc-stories-ratified: cannot locate lib dir" >&2; exit 2; }
23
+ # shellcheck source=/dev/null
24
+ source "$LIB/story-oversight.sh"
25
+
26
+ rfc="${1:-}"
27
+ stories_root="${2:-docs/stories}"
28
+ if [ -z "$rfc" ]; then
29
+ echo "check-rfc-stories-ratified: usage: check-rfc-stories-ratified.sh <rfc-file> [stories-root]" >&2
30
+ exit 2
31
+ fi
32
+ if [ ! -f "$rfc" ]; then
33
+ echo "check-rfc-stories-ratified: file not found: $rfc" >&2
34
+ exit 2
35
+ fi
36
+
37
+ # Collect STORY-NNN ids from the stories: frontmatter (inline list OR block list).
38
+ stories_line="$(awk '/^stories:/{print; exit}' "$rfc")"
39
+ ids="$(printf '%s' "$stories_line" | grep -oE 'STORY-[0-9]+' || true)"
40
+ if [ -z "$ids" ]; then
41
+ ids="$(awk '/^stories:/{f=1;next} f&&/^[[:space:]]*-/{print} f&&/^[^[:space:]-]/{exit}' "$rfc" | grep -oE 'STORY-[0-9]+' || true)"
42
+ fi
43
+
44
+ # No stories listed → vacuously ratified (has-stories gate owns emptiness).
45
+ [ -z "$ids" ] && exit 0
46
+
47
+ unratified=""
48
+ for id in $ids; do
49
+ f="$(ls "$stories_root"/*/"$id"-*.md 2>/dev/null | head -1 || true)"
50
+ if [ -z "$f" ]; then
51
+ unratified="$unratified $id(missing)"
52
+ continue
53
+ fi
54
+ if ! is_story_map_ratified "$f"; then
55
+ unratified="$unratified $id(unratified)"
56
+ fi
57
+ done
58
+
59
+ if [ -n "$unratified" ]; then
60
+ echo "check-rfc-stories-ratified: $rfc references unratified stories:$unratified — ADR-090: an RFC may reference only ratified (human-oversight: confirmed) stories. Ratify them first." >&2
61
+ exit 1
62
+ fi
63
+ exit 0
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ # detect-unratified-stories-maps.sh — ADR-090 detector.
3
+ #
4
+ # Token-cheap detection of story maps + stories that are NOT ratified. Mirrors
5
+ # wr-architect detect-unoversighted.sh but spans two artefact types and is
6
+ # DRIFT-AWARE via the shared lazy-fingerprint helper: an artefact is ratified
7
+ # only when it carries a `confirmed` human-oversight marker AND a stored
8
+ # oversight-hash matching its current content. This surfaces three cases as the
9
+ # drain queue:
10
+ # - never ratified (no marker)
11
+ # - drift-reopened (marker says confirmed, but content changed → hash mismatch)
12
+ # - legacy confirmed (confirmed but no fingerprint yet — needs one re-ratify)
13
+ #
14
+ # Usage: detect-unratified-stories-maps.sh [STORIES_DIR=docs/stories] [MAPS_DIR=docs/story-maps]
15
+ # Output: one unratified artefact path per line, sorted. Empty = all ratified.
16
+ # Always exits 0 (detector, not a gate). Consumed by the work-problems Step 2.4
17
+ # oversight-unconfirmed drain.
18
+ set -euo pipefail
19
+
20
+ # Adopter-safe: source the shared hash lib RELATIVE TO THIS SCRIPT (P317).
21
+ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
22
+ echo "detect-unratified-stories-maps: cannot locate lib dir" >&2; exit 0; }
23
+ # shellcheck source=/dev/null
24
+ source "$LIB/story-oversight.sh"
25
+
26
+ STORIES_DIR="${1:-docs/stories}"
27
+ MAPS_DIR="${2:-docs/story-maps}"
28
+
29
+ shopt -s nullglob
30
+
31
+ {
32
+ if [ -d "$STORIES_DIR" ]; then
33
+ for f in "$STORIES_DIR"/*.md "$STORIES_DIR"/*/*.md; do
34
+ [ "$(basename "$f")" = "README.md" ] && continue
35
+ is_story_map_ratified "$f" || echo "$f"
36
+ done
37
+ fi
38
+ if [ -d "$MAPS_DIR" ]; then
39
+ for f in "$MAPS_DIR"/*.html "$MAPS_DIR"/*/*.html; do
40
+ is_story_map_ratified "$f" || echo "$f"
41
+ done
42
+ fi
43
+ } | sort
44
+
45
+ exit 0
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # mark-story-oversight-confirmed.sh — ADR-090 ratify write-path.
3
+ #
4
+ # Writes `human-oversight: confirmed` + an `oversight-hash` fingerprint of the
5
+ # current content into a story (markdown frontmatter) or story-map (HTML meta).
6
+ # Idempotent: re-running recomputes the hash and replaces any existing marker
7
+ # (never duplicates). A later content edit changes the content hash, so the
8
+ # stored fingerprint no longer matches → the artefact reads as drifted /
9
+ # unratified until re-ratified (ADR-090 drift-invalidation).
10
+ #
11
+ # Usage: mark-story-oversight-confirmed.sh <story-or-map-file>
12
+ # Exit: 0 = ratified; 2 = usage / file error.
13
+ #
14
+ # Authority: ADR-090. Driver: P404 Phase 2. Test: mark-story-oversight-confirmed.bats.
15
+ set -euo pipefail
16
+
17
+ # Adopter-safe: source the shared hash lib RELATIVE TO THIS SCRIPT (P317), never
18
+ # repo-relative.
19
+ LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../lib" 2>/dev/null && pwd)" || {
20
+ echo "mark-story-oversight-confirmed: cannot locate lib dir" >&2; exit 2; }
21
+ # shellcheck source=/dev/null
22
+ source "$LIB/story-oversight.sh"
23
+
24
+ f="${1:-}"
25
+ if [ -z "$f" ]; then
26
+ echo "mark-story-oversight-confirmed: usage: mark-story-oversight-confirmed.sh <file>" >&2
27
+ exit 2
28
+ fi
29
+ [ -f "$f" ] || { echo "mark-story-oversight-confirmed: file not found: $f" >&2; exit 2; }
30
+
31
+ h="$(oversight_content_hash "$f")"
32
+ tmp="$(mktemp)"
33
+
34
+ case "$f" in
35
+ *.html)
36
+ if grep -qi '<head' "$f"; then
37
+ # Insert the two metas after the <head> tag; drop any existing marker metas.
38
+ awk -v H="$h" '
39
+ /<meta[^>]*name="(human-oversight|oversight-hash)"/ { next }
40
+ { print }
41
+ !done && tolower($0) ~ /<head[ >]/ {
42
+ print " <meta name=\"human-oversight\" content=\"confirmed\">"
43
+ print " <meta name=\"oversight-hash\" content=\"" H "\">"
44
+ done=1
45
+ }
46
+ ' "$f" > "$tmp"
47
+ else
48
+ # No <head> — prepend the metas (position is irrelevant to the hash).
49
+ { printf '<meta name="human-oversight" content="confirmed">\n'
50
+ printf '<meta name="oversight-hash" content="%s">\n' "$h"
51
+ grep -vE '<meta[^>]*name="(human-oversight|oversight-hash)"' "$f"; } > "$tmp"
52
+ fi
53
+ ;;
54
+ *)
55
+ # Markdown: rewrite frontmatter — drop existing markers, insert both before
56
+ # the closing `---`.
57
+ awk -v H="$h" '
58
+ NR==1 && $0=="---" { infm=1; print; next }
59
+ infm && /^(human-oversight|oversight-hash):/ { next }
60
+ infm && /^---[[:space:]]*$/ && !done {
61
+ print "human-oversight: confirmed"
62
+ print "oversight-hash: " H
63
+ print; done=1; infm=0; next
64
+ }
65
+ { print }
66
+ ' "$f" > "$tmp"
67
+ ;;
68
+ esac
69
+
70
+ mv "$tmp" "$f"
71
+ echo "mark-story-oversight-confirmed: ratified $f (oversight-hash ${h:0:12}…)" >&2
72
+ exit 0
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bats
2
+ # Behavioural test for check-rfc-stories-ratified.sh (ADR-090 — an RFC may
3
+ # reference only RATIFIED stories). DRIFT-AWARE via the shared lazy-fingerprint
4
+ # lib: a listed story counts only when it is `confirmed` AND its stored
5
+ # oversight-hash matches current content. Composes with check-rfc-has-stories
6
+ # (ADR-089): has-stories checks >=1 exists; this checks each listed one is ratified.
7
+ #
8
+ # @adr ADR-090
9
+ # @problem P404 (implement ADR-089 + ADR-090) — Phase 2
10
+
11
+ setup() {
12
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
13
+ SCRIPT="${REPO_ROOT}/packages/itil/scripts/check-rfc-stories-ratified.sh"
14
+ MARK="${REPO_ROOT}/packages/itil/scripts/mark-story-oversight-confirmed.sh"
15
+ TMPD="$(mktemp -d)"
16
+ mkdir -p "$TMPD/stories/accepted"
17
+ }
18
+ teardown() { rm -rf "$TMPD"; }
19
+
20
+ mkstory() { # $1=id $2=confirmed|unconfirmed|none
21
+ local f="$TMPD/stories/accepted/$1-x.md"
22
+ case "$2" in
23
+ confirmed) printf -- '---\nstatus: accepted\n---\n# %s\n' "$1" > "$f"; bash "$MARK" "$f" ;;
24
+ unconfirmed) printf -- '---\nstatus: accepted\nhuman-oversight: unconfirmed\n---\n# %s\n' "$1" > "$f" ;;
25
+ none) printf -- '---\nstatus: accepted\n---\n# %s\n' "$1" > "$f" ;;
26
+ esac
27
+ }
28
+ mkrfc() { printf -- '---\nstatus: proposed\nstories: [%s]\n---\n# rfc\n' "$1" > "$TMPD/RFC.proposed.md"; }
29
+
30
+ @test "check-rfc-stories-ratified: empty stories: [] passes vacuously (exit 0)" {
31
+ printf -- '---\nstatus: proposed\nstories: []\n---\n# rfc\n' > "$TMPD/RFC.proposed.md"
32
+ run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
33
+ [ "$status" -eq 0 ]
34
+ }
35
+
36
+ @test "check-rfc-stories-ratified: a ratified (confirmed+hash) story is ACCEPTED (exit 0)" {
37
+ mkstory STORY-020 confirmed
38
+ mkrfc STORY-020
39
+ run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
40
+ [ "$status" -eq 0 ]
41
+ }
42
+
43
+ @test "check-rfc-stories-ratified: an unconfirmed story is REJECTED" {
44
+ mkstory STORY-020 unconfirmed
45
+ mkrfc STORY-020
46
+ run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
47
+ [ "$status" -ne 0 ]
48
+ [[ "$output" == *"STORY-020"* ]]
49
+ }
50
+
51
+ @test "check-rfc-stories-ratified: a story with NO oversight marker is REJECTED" {
52
+ mkstory STORY-020 none
53
+ mkrfc STORY-020
54
+ run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
55
+ [ "$status" -ne 0 ]
56
+ [[ "$output" == *"STORY-020"* ]]
57
+ }
58
+
59
+ @test "check-rfc-stories-ratified: a confirmed-but-DRIFTED story is REJECTED" {
60
+ mkstory STORY-020 confirmed
61
+ printf '\nedit after ratify\n' >> "$TMPD/stories/accepted/STORY-020-x.md"
62
+ mkrfc STORY-020
63
+ run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
64
+ [ "$status" -ne 0 ]
65
+ [[ "$output" == *"STORY-020"* ]]
66
+ }
67
+
68
+ @test "check-rfc-stories-ratified: a missing story file is REJECTED" {
69
+ mkrfc STORY-099
70
+ run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
71
+ [ "$status" -ne 0 ]
72
+ [[ "$output" == *"STORY-099"* ]]
73
+ }
74
+
75
+ @test "check-rfc-stories-ratified: mixed — one ratified one unconfirmed is REJECTED" {
76
+ mkstory STORY-020 confirmed
77
+ mkstory STORY-021 unconfirmed
78
+ mkrfc "STORY-020, STORY-021"
79
+ run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
80
+ [ "$status" -ne 0 ]
81
+ [[ "$output" == *"STORY-021"* ]]
82
+ }
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bats
2
+ # Behavioural test for detect-unratified-stories-maps.sh (ADR-090 detector).
3
+ # DRIFT-AWARE via the shared lazy-fingerprint lib: an artefact counts as ratified
4
+ # only when it is `confirmed` AND its stored oversight-hash matches current
5
+ # content. Surfaces never-ratified, unconfirmed, and drift-reopened (confirmed
6
+ # then edited) cases; omits genuinely-ratified ones. Always exit 0.
7
+ #
8
+ # @adr ADR-090
9
+ # @problem P404 (Phase 2)
10
+
11
+ setup() {
12
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
13
+ SCRIPT="${REPO_ROOT}/packages/itil/scripts/detect-unratified-stories-maps.sh"
14
+ MARK="${REPO_ROOT}/packages/itil/scripts/mark-story-oversight-confirmed.sh"
15
+ TMPD="$(mktemp -d)"
16
+ mkdir -p "$TMPD/stories/accepted" "$TMPD/story-maps/draft"
17
+
18
+ # Genuinely ratified (via the mark write-path) — should NOT be listed.
19
+ printf -- '---\nstatus: accepted\n---\n# ok\n' > "$TMPD/stories/accepted/STORY-1-ok.md"
20
+ bash "$MARK" "$TMPD/stories/accepted/STORY-1-ok.md"
21
+ printf -- '<head></head>\n<h1>ok map</h1>\n' > "$TMPD/story-maps/draft/STORY-MAP-1-ok.html"
22
+ bash "$MARK" "$TMPD/story-maps/draft/STORY-MAP-1-ok.html"
23
+
24
+ # Unconfirmed + unmarked — listed.
25
+ printf -- '---\nstatus: accepted\nhuman-oversight: unconfirmed\n---\n# no\n' > "$TMPD/stories/accepted/STORY-2-unconf.md"
26
+ printf -- '---\nstatus: accepted\n---\n# nomarker\n' > "$TMPD/stories/accepted/STORY-3-none.md"
27
+
28
+ # Drifted: ratified, then content edited afterward — listed.
29
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/stories/accepted/STORY-4-drift.md"
30
+ bash "$MARK" "$TMPD/stories/accepted/STORY-4-drift.md"
31
+ printf '\nAn edit made after ratification.\n' >> "$TMPD/stories/accepted/STORY-4-drift.md"
32
+
33
+ # HTML map with no marker — listed.
34
+ printf -- '<h1>no meta</h1>\n' > "$TMPD/story-maps/draft/STORY-MAP-3-none.html"
35
+ }
36
+ teardown() { rm -rf "$TMPD"; }
37
+
38
+ @test "detect: lists unconfirmed + unmarked + DRIFTED, omits genuinely-ratified; exit 0" {
39
+ run bash "$SCRIPT" "$TMPD/stories" "$TMPD/story-maps"
40
+ [ "$status" -eq 0 ]
41
+ [[ "$output" == *"STORY-2-unconf.md"* ]]
42
+ [[ "$output" == *"STORY-3-none.md"* ]]
43
+ [[ "$output" == *"STORY-4-drift.md"* ]]
44
+ [[ "$output" == *"STORY-MAP-3-none.html"* ]]
45
+ [[ "$output" != *"STORY-1-ok.md"* ]]
46
+ [[ "$output" != *"STORY-MAP-1-ok.html"* ]]
47
+ }
48
+
49
+ @test "detect: all-ratified set yields empty output, exit 0" {
50
+ rm "$TMPD/stories/accepted/STORY-2-unconf.md" "$TMPD/stories/accepted/STORY-3-none.md" \
51
+ "$TMPD/stories/accepted/STORY-4-drift.md" "$TMPD/story-maps/draft/STORY-MAP-3-none.html"
52
+ run bash "$SCRIPT" "$TMPD/stories" "$TMPD/story-maps"
53
+ [ "$status" -eq 0 ]
54
+ [ -z "$output" ]
55
+ }
56
+
57
+ @test "detect: missing dirs are tolerated (exit 0, no crash)" {
58
+ run bash "$SCRIPT" "$TMPD/nope-stories" "$TMPD/nope-maps"
59
+ [ "$status" -eq 0 ]
60
+ }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bats
2
+ # Behavioural test for mark-story-oversight-confirmed.sh (ADR-090 ratify write-path).
3
+ # Writes `confirmed` + the content fingerprint into a story (md frontmatter) or a
4
+ # story-map (HTML meta). After marking, is_story_map_ratified is true; a later
5
+ # content edit drifts it back to not-ratified.
6
+ #
7
+ # @adr ADR-090
8
+ # @problem P404 (Phase 2)
9
+
10
+ setup() {
11
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
12
+ SCRIPT="${REPO_ROOT}/packages/itil/scripts/mark-story-oversight-confirmed.sh"
13
+ # shellcheck source=/dev/null
14
+ source "${REPO_ROOT}/packages/itil/lib/story-oversight.sh"
15
+ TMPD="$(mktemp -d)"
16
+ }
17
+ teardown() { rm -rf "$TMPD"; }
18
+
19
+ @test "mark: md story with no marker → ratified after mark" {
20
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/s.md"
21
+ run bash "$SCRIPT" "$TMPD/s.md"; [ "$status" -eq 0 ]
22
+ run is_story_map_ratified "$TMPD/s.md"; [ "$status" -eq 0 ]
23
+ }
24
+
25
+ @test "mark: md story with existing unconfirmed marker → confirmed + ratified" {
26
+ printf -- '---\nstatus: accepted\nhuman-oversight: unconfirmed\n---\n# body\n' > "$TMPD/s.md"
27
+ bash "$SCRIPT" "$TMPD/s.md"
28
+ grep -qE '^human-oversight:[[:space:]]*confirmed' "$TMPD/s.md"
29
+ run is_story_map_ratified "$TMPD/s.md"; [ "$status" -eq 0 ]
30
+ # no duplicate marker line left behind
31
+ [ "$(grep -cE '^human-oversight:' "$TMPD/s.md")" -eq 1 ]
32
+ }
33
+
34
+ @test "mark: idempotent — marking twice stays ratified, single marker" {
35
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/s.md"
36
+ bash "$SCRIPT" "$TMPD/s.md"; bash "$SCRIPT" "$TMPD/s.md"
37
+ run is_story_map_ratified "$TMPD/s.md"; [ "$status" -eq 0 ]
38
+ [ "$(grep -cE '^oversight-hash:' "$TMPD/s.md")" -eq 1 ]
39
+ }
40
+
41
+ @test "mark: drift — mark then edit body → not ratified" {
42
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/s.md"
43
+ bash "$SCRIPT" "$TMPD/s.md"
44
+ printf -- '---\nstatus: accepted\nhuman-oversight: confirmed\n---\n# body EDITED\n' > "$TMPD/s.md"
45
+ run is_story_map_ratified "$TMPD/s.md"; [ "$status" -ne 0 ]
46
+ }
47
+
48
+ @test "mark: HTML map → ratified after mark" {
49
+ printf -- '<!doctype html>\n<head><title>map</title></head>\n<body><h1>map</h1></body>\n' > "$TMPD/m.html"
50
+ run bash "$SCRIPT" "$TMPD/m.html"; [ "$status" -eq 0 ]
51
+ grep -qE '<meta[^>]*name="human-oversight"[^>]*content="confirmed"' "$TMPD/m.html"
52
+ run is_story_map_ratified "$TMPD/m.html"; [ "$status" -eq 0 ]
53
+ }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bats
2
+ # Behavioural test for lib/story-oversight.sh — ADR-090 lazy-fingerprint helpers.
3
+ # The hash EXCLUDES the marker lines (so writing the marker is idempotent), and
4
+ # "ratified" = confirmed marker AND stored hash matches current content. Any
5
+ # content edit drifts the hash → not ratified (ADR-090 drift-invalidation).
6
+ #
7
+ # @adr ADR-090
8
+ # @problem P404 (Phase 2)
9
+
10
+ setup() {
11
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
12
+ LIB="${REPO_ROOT}/packages/itil/lib/story-oversight.sh"
13
+ TMPD="$(mktemp -d)"
14
+ # shellcheck source=/dev/null
15
+ source "$LIB"
16
+ }
17
+ teardown() { rm -rf "$TMPD"; }
18
+
19
+ @test "oversight_content_hash: identical content → identical hash" {
20
+ printf '# body\nline two\n' > "$TMPD/a"; printf '# body\nline two\n' > "$TMPD/b"
21
+ [ "$(oversight_content_hash "$TMPD/a")" = "$(oversight_content_hash "$TMPD/b")" ]
22
+ }
23
+
24
+ @test "oversight_content_hash: adding/changing the marker lines does NOT change the hash" {
25
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/f"
26
+ before="$(oversight_content_hash "$TMPD/f")"
27
+ printf -- '---\nstatus: accepted\nhuman-oversight: confirmed\noversight-hash: %064d\n---\n# body\n' 0 > "$TMPD/f"
28
+ [ "$before" = "$(oversight_content_hash "$TMPD/f")" ]
29
+ }
30
+
31
+ @test "oversight_content_hash: a real content edit DOES change the hash" {
32
+ printf '# body\n' > "$TMPD/f"; before="$(oversight_content_hash "$TMPD/f")"
33
+ printf '# body EDITED\n' > "$TMPD/f"
34
+ [ "$before" != "$(oversight_content_hash "$TMPD/f")" ]
35
+ }
36
+
37
+ @test "is_story_map_ratified: confirmed + matching hash → ratified (md)" {
38
+ printf -- '---\nstatus: accepted\n---\n# body\n' > "$TMPD/f"
39
+ h="$(oversight_content_hash "$TMPD/f")"
40
+ printf -- '---\nstatus: accepted\nhuman-oversight: confirmed\noversight-hash: %s\n---\n# body\n' "$h" > "$TMPD/f"
41
+ run is_story_map_ratified "$TMPD/f"; [ "$status" -eq 0 ]
42
+ }
43
+
44
+ @test "is_story_map_ratified: confirmed but STALE hash (drifted) → not ratified" {
45
+ printf -- '---\nhuman-oversight: confirmed\noversight-hash: %064d\n---\n# body EDITED AFTER RATIFY\n' 1 > "$TMPD/f"
46
+ run is_story_map_ratified "$TMPD/f"; [ "$status" -ne 0 ]
47
+ }
48
+
49
+ @test "is_story_map_ratified: confirmed with NO hash (legacy hand-ratified) → not ratified" {
50
+ printf -- '---\nhuman-oversight: confirmed\n---\n# body\n' > "$TMPD/f"
51
+ run is_story_map_ratified "$TMPD/f"; [ "$status" -ne 0 ]
52
+ }
53
+
54
+ @test "is_story_map_ratified: unconfirmed → not ratified" {
55
+ printf -- '---\nhuman-oversight: unconfirmed\n---\n# body\n' > "$TMPD/f"
56
+ run is_story_map_ratified "$TMPD/f"; [ "$status" -ne 0 ]
57
+ }
58
+
59
+ @test "is_story_map_ratified: HTML map confirmed + matching hash → ratified" {
60
+ printf '<h1>map</h1>\n' > "$TMPD/m.html"
61
+ h="$(oversight_content_hash "$TMPD/m.html")"
62
+ printf '<meta name="human-oversight" content="confirmed">\n<meta name="oversight-hash" content="%s">\n<h1>map</h1>\n' "$h" > "$TMPD/m.html"
63
+ run is_story_map_ratified "$TMPD/m.html"; [ "$status" -eq 0 ]
64
+ }
@@ -118,7 +118,7 @@ Each transition is a `git mv` + Edit + `git add` (P057 staging trap) + README re
118
118
 
119
119
  | Transition | Checks |
120
120
  |------------|--------|
121
- | `proposed → accepted` | All `problems:` entries resolve. Architect re-review PASS. JTBD re-review PASS. `## Scope` populated (not "deferred"). `## Tasks` decomposed (≥ 1 task; not just deferred placeholder). **`stories:` non-empty — run `wr-itil-check-rfc-has-stories <rfc-file>`; hard-block on non-zero exit (empty `stories: []` or missing).** Per **ADR-089** the empty-stories fallback is removed: an atomic fix is an RFC with exactly one full story; an empty `stories:` is permitted only on a `proposed`/draft RFC before the fix is scoped, never at `accepted`. |
121
+ | `proposed → accepted` | All `problems:` entries resolve. Architect re-review PASS. JTBD re-review PASS. `## Scope` populated (not "deferred"). `## Tasks` decomposed (≥ 1 task; not just deferred placeholder). **`stories:` non-empty — run `wr-itil-check-rfc-has-stories <rfc-file>`; hard-block on non-zero exit (empty `stories: []` or missing).** Per **ADR-089** the empty-stories fallback is removed: an atomic fix is an RFC with exactly one full story; an empty `stories:` is permitted only on a `proposed`/draft RFC before the fix is scoped, never at `accepted`. **Every listed story ratified — run `wr-itil-check-rfc-stories-ratified <rfc-file>`; hard-block on non-zero (any listed story carries `human-oversight: unconfirmed`, has no marker, or is missing).** Per **ADR-090** an RFC may reference only ratified stories; composes with the has-stories gate (has-stories checks ≥1 exists, ratified checks each is confirmed). |
122
122
  | `accepted → in-progress` | First commit referencing `Refs: RFC-<NNN>` exists OR is being authored in this same commit. I1 hard-block on missing/orphan trace. |
123
123
  | `in-progress → verifying` | All `## Tasks` checked. `## Verification` section drafted (release marker, user-side check, trace closure path). I1 hard-block on missing/orphan trace. |
124
124
  | `verifying → closed` | User explicitly confirms (or AFK Rule 6 fallback for evidence-based close per ADR-022 / ADR-044 framework-resolved silent dispatch). I1 advisory-with-escalation if driving problems Closed/Parked. |