@windyroad/itil 0.56.0-preview.879 → 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/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.879",
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"
@@ -17,6 +17,12 @@
17
17
  # Authority: ADR-090. Driver: P404 Phase 2. Test: check-rfc-stories-ratified.bats.
18
18
  set -euo pipefail
19
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
+
20
26
  rfc="${1:-}"
21
27
  stories_root="${2:-docs/stories}"
22
28
  if [ -z "$rfc" ]; then
@@ -45,7 +51,7 @@ for id in $ids; do
45
51
  unratified="$unratified $id(missing)"
46
52
  continue
47
53
  fi
48
- if ! grep -qE '^human-oversight:[[:space:]]*confirmed([[:space:]]|$)' "$f"; then
54
+ if ! is_story_map_ratified "$f"; then
49
55
  unratified="$unratified $id(unratified)"
50
56
  fi
51
57
  done
@@ -1,42 +1,43 @@
1
1
  #!/usr/bin/env bash
2
2
  # detect-unratified-stories-maps.sh — ADR-090 detector.
3
3
  #
4
- # Token-cheap detection of story maps + stories lacking the ratified
5
- # `human-oversight: confirmed` marker. Mirrors wr-architect detect-unoversighted.sh
6
- # but spans two artefact types with different marker encodings:
7
- # - stories: markdown YAML frontmatter `human-oversight: confirmed`
8
- # - story-maps: HTML meta tag `<meta name="human-oversight" content="confirmed">`
9
- #
10
- # ADR-090's marker is DRIFT-INVALIDATED (unlike ADR-066 write-once): an edit
11
- # re-opens it to `unconfirmed`, so this detector surfaces both the never-ratified
12
- # (no marker) and the drift-reopened (`unconfirmed`) cases as the drain queue.
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
13
  #
14
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 confirmed.
15
+ # Output: one unratified artefact path per line, sorted. Empty = all ratified.
16
16
  # Always exits 0 (detector, not a gate). Consumed by the work-problems Step 2.4
17
- # oversight-unconfirmed drain (mirror of the architect/jtbd detectors).
17
+ # oversight-unconfirmed drain.
18
18
  set -euo pipefail
19
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
+
20
26
  STORIES_DIR="${1:-docs/stories}"
21
27
  MAPS_DIR="${2:-docs/story-maps}"
22
28
 
23
29
  shopt -s nullglob
24
30
 
25
31
  {
26
- # Stories — markdown YAML frontmatter.
27
32
  if [ -d "$STORIES_DIR" ]; then
28
33
  for f in "$STORIES_DIR"/*.md "$STORIES_DIR"/*/*.md; do
29
34
  [ "$(basename "$f")" = "README.md" ] && continue
30
- fm="$(awk 'NR==1 && $0 != "---" { exit } NR==1 { next } /^---[[:space:]]*$/ { exit } { print }' "$f")"
31
- printf '%s\n' "$fm" | grep -qiE '^human-oversight:[[:space:]]*confirmed[[:space:]]*$' && continue
32
- echo "$f"
35
+ is_story_map_ratified "$f" || echo "$f"
33
36
  done
34
37
  fi
35
- # Story maps — HTML meta tag.
36
38
  if [ -d "$MAPS_DIR" ]; then
37
39
  for f in "$MAPS_DIR"/*.html "$MAPS_DIR"/*/*.html; do
38
- grep -qiE '<meta[^>]*name="human-oversight"[^>]*content="confirmed"' "$f" && continue
39
- echo "$f"
40
+ is_story_map_ratified "$f" || echo "$f"
40
41
  done
41
42
  fi
42
43
  } | sort
@@ -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
@@ -1,26 +1,29 @@
1
1
  #!/usr/bin/env bats
2
2
  # Behavioural test for check-rfc-stories-ratified.sh (ADR-090 — an RFC may
3
- # reference only RATIFIED stories). For each STORY-NNN in the RFC's stories:
4
- # frontmatter, the predicate resolves the story file and verifies
5
- # human-oversight: confirmed. Composes with check-rfc-has-stories (ADR-089):
6
- # has-stories checks >=1 exists; this checks each listed one is ratified.
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
7
  #
8
- # @adr ADR-090 (story maps and stories carry a drift-invalidated human-oversight marker)
8
+ # @adr ADR-090
9
9
  # @problem P404 (implement ADR-089 + ADR-090) — Phase 2
10
10
 
11
11
  setup() {
12
12
  REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
13
13
  SCRIPT="${REPO_ROOT}/packages/itil/scripts/check-rfc-stories-ratified.sh"
14
+ MARK="${REPO_ROOT}/packages/itil/scripts/mark-story-oversight-confirmed.sh"
14
15
  TMPD="$(mktemp -d)"
15
16
  mkdir -p "$TMPD/stories/accepted"
16
17
  }
17
18
  teardown() { rm -rf "$TMPD"; }
18
19
 
19
- mkstory() { # $1=id $2=oversight-value(or "none")
20
+ mkstory() { # $1=id $2=confirmed|unconfirmed|none
20
21
  local f="$TMPD/stories/accepted/$1-x.md"
21
- { echo "---"; echo "status: accepted"; echo "story-id: x"
22
- [ "$2" != "none" ] && echo "human-oversight: $2"
23
- echo "---"; echo "# $1"; } > "$f"
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
24
27
  }
25
28
  mkrfc() { printf -- '---\nstatus: proposed\nstories: [%s]\n---\n# rfc\n' "$1" > "$TMPD/RFC.proposed.md"; }
26
29
 
@@ -30,14 +33,14 @@ mkrfc() { printf -- '---\nstatus: proposed\nstories: [%s]\n---\n# rfc\n' "$1" >
30
33
  [ "$status" -eq 0 ]
31
34
  }
32
35
 
33
- @test "check-rfc-stories-ratified: a confirmed story is ACCEPTED (exit 0)" {
36
+ @test "check-rfc-stories-ratified: a ratified (confirmed+hash) story is ACCEPTED (exit 0)" {
34
37
  mkstory STORY-020 confirmed
35
38
  mkrfc STORY-020
36
39
  run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
37
40
  [ "$status" -eq 0 ]
38
41
  }
39
42
 
40
- @test "check-rfc-stories-ratified: an unconfirmed story is REJECTED (exit non-zero)" {
43
+ @test "check-rfc-stories-ratified: an unconfirmed story is REJECTED" {
41
44
  mkstory STORY-020 unconfirmed
42
45
  mkrfc STORY-020
43
46
  run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
@@ -53,6 +56,15 @@ mkrfc() { printf -- '---\nstatus: proposed\nstories: [%s]\n---\n# rfc\n' "$1" >
53
56
  [[ "$output" == *"STORY-020"* ]]
54
57
  }
55
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
+
56
68
  @test "check-rfc-stories-ratified: a missing story file is REJECTED" {
57
69
  mkrfc STORY-099
58
70
  run bash "$SCRIPT" "$TMPD/RFC.proposed.md" "$TMPD/stories"
@@ -60,7 +72,7 @@ mkrfc() { printf -- '---\nstatus: proposed\nstories: [%s]\n---\n# rfc\n' "$1" >
60
72
  [[ "$output" == *"STORY-099"* ]]
61
73
  }
62
74
 
63
- @test "check-rfc-stories-ratified: mixed — one confirmed one unconfirmed is REJECTED" {
75
+ @test "check-rfc-stories-ratified: mixed — one ratified one unconfirmed is REJECTED" {
64
76
  mkstory STORY-020 confirmed
65
77
  mkstory STORY-021 unconfirmed
66
78
  mkrfc "STORY-020, STORY-021"
@@ -1,44 +1,54 @@
1
1
  #!/usr/bin/env bats
2
2
  # Behavioural test for detect-unratified-stories-maps.sh (ADR-090 detector).
3
- # Mirrors wr-architect detect-unoversighted.sh, but spans TWO artefact types
4
- # with DIFFERENT marker encodings:
5
- # - stories: markdown YAML frontmatter `human-oversight: confirmed`
6
- # - story-maps: HTML `<meta name="human-oversight" content="confirmed">`
7
- # Lists every artefact whose marker is missing or not `confirmed`. Always exit 0.
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.
8
7
  #
9
- # @adr ADR-090 (drift-invalidated story-map/story human-oversight marker)
10
- # @problem P404 (implement ADR-089 + ADR-090) — Phase 2
8
+ # @adr ADR-090
9
+ # @problem P404 (Phase 2)
11
10
 
12
11
  setup() {
13
12
  REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
14
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
15
  TMPD="$(mktemp -d)"
16
16
  mkdir -p "$TMPD/stories/accepted" "$TMPD/story-maps/draft"
17
- # stories (markdown frontmatter)
18
- printf -- '---\nstatus: accepted\nhuman-oversight: confirmed\n---\n# ok\n' > "$TMPD/stories/accepted/STORY-1-ok.md"
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.
19
25
  printf -- '---\nstatus: accepted\nhuman-oversight: unconfirmed\n---\n# no\n' > "$TMPD/stories/accepted/STORY-2-unconf.md"
20
26
  printf -- '---\nstatus: accepted\n---\n# nomarker\n' > "$TMPD/stories/accepted/STORY-3-none.md"
21
- # story-maps (HTML meta)
22
- printf -- '<meta name="human-oversight" content="confirmed">\n' > "$TMPD/story-maps/draft/STORY-MAP-1-ok.html"
23
- printf -- '<meta name="human-oversight" content="unconfirmed">\n' > "$TMPD/story-maps/draft/STORY-MAP-2-unconf.html"
24
- printf -- '<h1>no meta</h1>\n' > "$TMPD/story-maps/draft/STORY-MAP-3-none.html"
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"
25
35
  }
26
36
  teardown() { rm -rf "$TMPD"; }
27
37
 
28
- @test "detect: lists unconfirmed + unmarked, omits confirmed; exits 0" {
38
+ @test "detect: lists unconfirmed + unmarked + DRIFTED, omits genuinely-ratified; exit 0" {
29
39
  run bash "$SCRIPT" "$TMPD/stories" "$TMPD/story-maps"
30
40
  [ "$status" -eq 0 ]
31
41
  [[ "$output" == *"STORY-2-unconf.md"* ]]
32
42
  [[ "$output" == *"STORY-3-none.md"* ]]
33
- [[ "$output" == *"STORY-MAP-2-unconf.html"* ]]
43
+ [[ "$output" == *"STORY-4-drift.md"* ]]
34
44
  [[ "$output" == *"STORY-MAP-3-none.html"* ]]
35
45
  [[ "$output" != *"STORY-1-ok.md"* ]]
36
46
  [[ "$output" != *"STORY-MAP-1-ok.html"* ]]
37
47
  }
38
48
 
39
- @test "detect: all-confirmed set yields empty output, exit 0" {
49
+ @test "detect: all-ratified set yields empty output, exit 0" {
40
50
  rm "$TMPD/stories/accepted/STORY-2-unconf.md" "$TMPD/stories/accepted/STORY-3-none.md" \
41
- "$TMPD/story-maps/draft/STORY-MAP-2-unconf.html" "$TMPD/story-maps/draft/STORY-MAP-3-none.html"
51
+ "$TMPD/stories/accepted/STORY-4-drift.md" "$TMPD/story-maps/draft/STORY-MAP-3-none.html"
42
52
  run bash "$SCRIPT" "$TMPD/stories" "$TMPD/story-maps"
43
53
  [ "$status" -eq 0 ]
44
54
  [ -z "$output" ]
@@ -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
+ }