@windyroad/itil 0.27.1 → 0.28.0-preview.304
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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +9 -1
- package/bin/wr-itil-reconcile-stories +2 -0
- package/bin/wr-itil-reconcile-story-maps +2 -0
- package/hooks/hooks.json +4 -0
- package/hooks/itil-readme-refresh-discipline.sh +118 -0
- package/hooks/lib/readme-refresh-detect.sh +161 -0
- package/hooks/test/itil-readme-refresh-discipline.bats +261 -0
- package/lib/migrate-problems-layout.sh +128 -0
- package/package.json +1 -1
- package/scripts/reconcile-stories.sh +236 -0
- package/scripts/reconcile-story-maps.sh +98 -0
- package/scripts/test/reconcile-stories.bats +173 -0
- package/scripts/test/reconcile-story-maps.bats +74 -0
- package/scripts/test/rfc-stories-extension.bats +173 -0
- package/scripts/test/update-problem-references-section.bats +195 -0
- package/scripts/test/update-references-section-sibling-helpers.bats +80 -0
- package/scripts/test/working-the-problem-traversal.bats +109 -0
- package/scripts/update-jtbd-references-section.sh +131 -0
- package/scripts/update-problem-references-section.sh +284 -0
- package/scripts/update-rfc-references-section.sh +152 -0
- package/scripts/update-story-references-section.sh +128 -0
- package/skills/capture-rfc/SKILL.md +28 -3
- package/skills/capture-story/SKILL.md +373 -0
- package/skills/capture-story/test/capture-story-behavioural.bats +227 -0
- package/skills/capture-story-map/SKILL.md +229 -0
- package/skills/capture-story-map/test/capture-story-map-behavioural.bats +98 -0
- package/skills/list-stories/SKILL.md +151 -0
- package/skills/list-stories/test/list-stories-contract.bats +127 -0
- package/skills/list-story-maps/SKILL.md +93 -0
- package/skills/list-story-maps/test/list-story-maps-contract.bats +46 -0
- package/skills/manage-problem/SKILL.md +42 -4
- package/skills/manage-problem/test/manage-problem-auto-migrate-step.bats +53 -0
- package/skills/manage-rfc/SKILL.md +12 -0
- package/skills/manage-story/SKILL.md +242 -0
- package/skills/manage-story/test/manage-story-contract.bats +171 -0
- package/skills/manage-story-map/SKILL.md +158 -0
- package/skills/manage-story-map/test/manage-story-map-contract.bats +63 -0
- package/skills/reconcile-stories/SKILL.md +110 -0
- package/skills/reconcile-story-maps/SKILL.md +70 -0
- package/skills/work-problem/SKILL.md +1 -1
- package/skills/work-problems/SKILL.md +25 -0
- package/skills/work-problems/test/work-problems-auto-migrate-step.bats +57 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/itil/scripts/update-jtbd-references-section.sh
|
|
3
|
+
#
|
|
4
|
+
# Generalised reverse-trace section updater for JTBD files. Lookup
|
|
5
|
+
# table supports ## RFCs, ## Story Maps, ## Stories sections on
|
|
6
|
+
# JTBD-NNN-*.md files. Mirror of update-problem-references-section.sh
|
|
7
|
+
# with jtbd: frontmatter / data-jtbd attribute extraction.
|
|
8
|
+
#
|
|
9
|
+
# Per ADR-060 § Phase 2 encoding amendment 2026-05-12 architect
|
|
10
|
+
# finding 4: no per-section-name branching; lookup-table dispatch.
|
|
11
|
+
#
|
|
12
|
+
# @adr ADR-060 (Phase 2 encoding amendment 2026-05-12)
|
|
13
|
+
# @problem P170 (Phase 2 Slice 2b)
|
|
14
|
+
|
|
15
|
+
set -uo pipefail
|
|
16
|
+
|
|
17
|
+
JTBD_FILE="${1:-}"
|
|
18
|
+
SECTION_NAME="${2:-}"
|
|
19
|
+
|
|
20
|
+
[ -n "$JTBD_FILE" ] || { echo "ERROR: missing jtbd-file argument" >&2; exit 1; }
|
|
21
|
+
[ -n "$SECTION_NAME" ] || { echo "ERROR: missing section-name argument" >&2; exit 1; }
|
|
22
|
+
[ -f "$JTBD_FILE" ] || { echo "ERROR: jtbd file not found: $JTBD_FILE" >&2; exit 1; }
|
|
23
|
+
|
|
24
|
+
declare -A SECTION_GLOB SECTION_MODE SECTION_ID_PATTERN
|
|
25
|
+
|
|
26
|
+
SECTION_GLOB["RFCs"]="docs/rfcs/RFC-*.md"
|
|
27
|
+
SECTION_MODE["RFCs"]="markdown-frontmatter-jtbd"
|
|
28
|
+
SECTION_ID_PATTERN["RFCs"]="RFC-[0-9]+"
|
|
29
|
+
|
|
30
|
+
SECTION_GLOB["Story Maps"]="docs/story-maps/*/STORY-MAP-*.html"
|
|
31
|
+
SECTION_MODE["Story Maps"]="html-data-attribute-jtbd"
|
|
32
|
+
SECTION_ID_PATTERN["Story Maps"]="STORY-MAP-[0-9]+"
|
|
33
|
+
|
|
34
|
+
SECTION_GLOB["Stories"]="docs/stories/*/STORY-*.md"
|
|
35
|
+
SECTION_MODE["Stories"]="markdown-frontmatter-jtbd"
|
|
36
|
+
SECTION_ID_PATTERN["Stories"]="STORY-[0-9]+"
|
|
37
|
+
|
|
38
|
+
glob_pattern="${SECTION_GLOB[$SECTION_NAME]:-}"
|
|
39
|
+
extraction_mode="${SECTION_MODE[$SECTION_NAME]:-}"
|
|
40
|
+
id_pattern="${SECTION_ID_PATTERN[$SECTION_NAME]:-}"
|
|
41
|
+
|
|
42
|
+
[ -n "$glob_pattern" ] || { echo "ERROR: unknown section-name '$SECTION_NAME'. Supported: RFCs, Story Maps, Stories" >&2; exit 1; }
|
|
43
|
+
|
|
44
|
+
jtbd_basename=$(basename "$JTBD_FILE")
|
|
45
|
+
jtbd_id=$(echo "$jtbd_basename" | grep -oE '^JTBD-[0-9]+' | head -1)
|
|
46
|
+
[ -n "$jtbd_id" ] || { echo "ERROR: cannot extract JTBD ID from filename: $jtbd_basename" >&2; exit 1; }
|
|
47
|
+
|
|
48
|
+
declare -a matched_ids=() matched_titles=() matched_statuses=()
|
|
49
|
+
|
|
50
|
+
extract_from_markdown_frontmatter_jtbd() {
|
|
51
|
+
local file="$1"
|
|
52
|
+
awk '/^---$/{f=!f;next} f && /^jtbd:/' "$file" | head -1 | grep -qE "\\b${jtbd_id}\\b"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
extract_from_html_data_jtbd() {
|
|
56
|
+
local file="$1"
|
|
57
|
+
grep -E '<meta[[:space:]]+name="jtbd"[[:space:]]+content="[^"]+"' "$file" | head -1 | grep -qE "${jtbd_id}\\b"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
extract_id_from_filename() { basename "$1" | grep -oE "$id_pattern" | head -1; }
|
|
61
|
+
extract_title_md() { awk '/^# / { sub(/^# /, ""); print; exit }' "$1"; }
|
|
62
|
+
extract_title_html() { grep -oE '<title>[^<]+</title>' "$1" | head -1 | sed -E 's|<title>([^<]+)</title>|\1|'; }
|
|
63
|
+
extract_status_md() { awk '/^---$/{f=!f;next} f && /^status:/{ sub(/^status:[[:space:]]*/, ""); gsub(/"/, ""); print; exit }' "$1"; }
|
|
64
|
+
extract_status_html() { grep -oE '<meta[[:space:]]+name="status"[[:space:]]+content="[^"]+"' "$1" | head -1 | sed -E 's|.*content="([^"]+)".*|\1|'; }
|
|
65
|
+
|
|
66
|
+
case "$extraction_mode" in
|
|
67
|
+
markdown-frontmatter-jtbd)
|
|
68
|
+
extract_match=extract_from_markdown_frontmatter_jtbd
|
|
69
|
+
extract_title=extract_title_md
|
|
70
|
+
extract_status=extract_status_md
|
|
71
|
+
;;
|
|
72
|
+
html-data-attribute-jtbd)
|
|
73
|
+
extract_match=extract_from_html_data_jtbd
|
|
74
|
+
extract_title=extract_title_html
|
|
75
|
+
extract_status=extract_status_html
|
|
76
|
+
;;
|
|
77
|
+
*)
|
|
78
|
+
echo "ERROR: unknown extraction-mode '$extraction_mode'" >&2
|
|
79
|
+
exit 1
|
|
80
|
+
;;
|
|
81
|
+
esac
|
|
82
|
+
|
|
83
|
+
shopt -s nullglob
|
|
84
|
+
for artefact in $glob_pattern; do
|
|
85
|
+
[ -e "$artefact" ] || continue
|
|
86
|
+
if "$extract_match" "$artefact"; then
|
|
87
|
+
aid=$(extract_id_from_filename "$artefact")
|
|
88
|
+
[ -n "$aid" ] || continue
|
|
89
|
+
matched_ids+=("$aid")
|
|
90
|
+
matched_titles+=("$("$extract_title" "$artefact" 2>/dev/null || echo "")")
|
|
91
|
+
matched_statuses+=("$("$extract_status" "$artefact" 2>/dev/null || echo "unknown")")
|
|
92
|
+
fi
|
|
93
|
+
done
|
|
94
|
+
shopt -u nullglob
|
|
95
|
+
|
|
96
|
+
new_section=""
|
|
97
|
+
if [ ${#matched_ids[@]} -gt 0 ]; then
|
|
98
|
+
new_section="## ${SECTION_NAME}"$'\n\n| ID | Title | Status |\n|----|-------|--------|\n'
|
|
99
|
+
for i in "${!matched_ids[@]}"; do
|
|
100
|
+
new_section+="| ${matched_ids[$i]} | ${matched_titles[$i]} | ${matched_statuses[$i]} |"$'\n'
|
|
101
|
+
done
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
tmp_file="$(mktemp)"
|
|
105
|
+
awk -v sec="## $SECTION_NAME" '
|
|
106
|
+
BEGIN { in_target=0; blank_buffer="" }
|
|
107
|
+
$0 == sec { in_target=1; blank_buffer=""; next }
|
|
108
|
+
in_target && /^## / && $0 != sec { in_target=0 }
|
|
109
|
+
!in_target {
|
|
110
|
+
if ($0 ~ /^[[:space:]]*$/) { if (blank_buffer == "") blank_buffer="\n"; next }
|
|
111
|
+
if (blank_buffer != "") { printf "%s", blank_buffer; blank_buffer="" }
|
|
112
|
+
print
|
|
113
|
+
}
|
|
114
|
+
END { if (blank_buffer != "") printf "%s", blank_buffer }
|
|
115
|
+
' "$JTBD_FILE" > "$tmp_file"
|
|
116
|
+
|
|
117
|
+
tmp_file2="$(mktemp)"
|
|
118
|
+
awk 'BEGIN{c=0} /^[[:space:]]*$/{c++; next} {for(i=0;i<c;i++)print ""; c=0; print} END{print ""}' "$tmp_file" > "$tmp_file2"
|
|
119
|
+
mv "$tmp_file2" "$tmp_file"
|
|
120
|
+
|
|
121
|
+
if [ -n "$new_section" ]; then
|
|
122
|
+
printf '\n%s' "$new_section" >> "$tmp_file"
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
if ! cmp -s "$tmp_file" "$JTBD_FILE"; then
|
|
126
|
+
mv "$tmp_file" "$JTBD_FILE"
|
|
127
|
+
else
|
|
128
|
+
rm -f "$tmp_file"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
exit 0
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/itil/scripts/update-problem-references-section.sh
|
|
3
|
+
#
|
|
4
|
+
# Generalised reverse-trace section updater for problem tickets.
|
|
5
|
+
# Refreshes the auto-maintained `## <section-name>` section on a
|
|
6
|
+
# problem ticket file based on which artefacts in the configured
|
|
7
|
+
# source directory claim the ticket via either HTML `<meta>` data
|
|
8
|
+
# attributes (story-maps) or YAML frontmatter `problems:` list
|
|
9
|
+
# (stories + RFCs).
|
|
10
|
+
#
|
|
11
|
+
# Per ADR-060 § Phase 2 encoding amendment 2026-05-12 architect
|
|
12
|
+
# finding 4: the helper body MUST NOT carry per-section-name
|
|
13
|
+
# branching; `<section-name>` is a positional argument; the only
|
|
14
|
+
# branch in the helper body is the per-input-extension dispatch
|
|
15
|
+
# (`.html` → data-attribute grep; `.md` → frontmatter parse), which
|
|
16
|
+
# is determined by the section-to-source-dir lookup table, NOT by
|
|
17
|
+
# the section-name value itself. Each table row maps a section-name
|
|
18
|
+
# to its (source-dir, extension, ID-pattern) tuple; the helper body
|
|
19
|
+
# treats the table as opaque data.
|
|
20
|
+
#
|
|
21
|
+
# Supersedes/absorbs the single-purpose
|
|
22
|
+
# `packages/itil/scripts/update-problem-rfcs-section.sh` for the
|
|
23
|
+
# `## RFCs` section per ADR-060 amendment 2026-05-12 § Phase 2
|
|
24
|
+
# commit-grain decomposition + ADR-010 forwarder pattern. The old
|
|
25
|
+
# single-purpose helper stays as a thin shim during the deprecation
|
|
26
|
+
# window for back-compat with any pinned callers.
|
|
27
|
+
#
|
|
28
|
+
# Usage:
|
|
29
|
+
# update-problem-references-section.sh <problem-file> <section-name>
|
|
30
|
+
#
|
|
31
|
+
# Supported section-names (lookup-table-driven, NOT branching):
|
|
32
|
+
# - RFCs → docs/rfcs/*.md (frontmatter `problems:` extraction)
|
|
33
|
+
# - Story Maps → docs/story-maps/*/*.html (data-attribute extraction)
|
|
34
|
+
# - Stories → docs/stories/*/*.md (frontmatter `problems:` extraction)
|
|
35
|
+
#
|
|
36
|
+
# Lazy-empty discipline (per JTBD-101 atomic-fix-adopter friction
|
|
37
|
+
# guard + sibling pattern from update-problem-rfcs-section.sh): if
|
|
38
|
+
# zero artefacts trace the problem, the `## <section-name>` section
|
|
39
|
+
# is REMOVED entirely from the ticket file.
|
|
40
|
+
#
|
|
41
|
+
# Idempotent: running over a current section is a no-op (no file
|
|
42
|
+
# diff). Section placement: between `## Related` and `## Fix Released`
|
|
43
|
+
# (or at EOF if neither present), mirroring the RFC-section convention.
|
|
44
|
+
#
|
|
45
|
+
# @adr ADR-060 (Phase 2 encoding amendment 2026-05-12 — architect
|
|
46
|
+
# finding 4: no per-section-name branching; per-extension dispatch
|
|
47
|
+
# via lookup table)
|
|
48
|
+
# @adr ADR-014 (called by capture/manage skills to ride single-purpose
|
|
49
|
+
# commit grain)
|
|
50
|
+
# @adr ADR-052 (behavioural bats coverage in test/)
|
|
51
|
+
# @problem P170 (Phase 2 Slice 2 deliverable)
|
|
52
|
+
|
|
53
|
+
set -uo pipefail
|
|
54
|
+
|
|
55
|
+
PROBLEM_FILE="${1:-}"
|
|
56
|
+
SECTION_NAME="${2:-}"
|
|
57
|
+
|
|
58
|
+
if [ -z "$PROBLEM_FILE" ]; then
|
|
59
|
+
echo "ERROR: missing problem-file argument" >&2
|
|
60
|
+
echo "Usage: $(basename "$0") <problem-file> <section-name>" >&2
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
if [ -z "$SECTION_NAME" ]; then
|
|
64
|
+
echo "ERROR: missing section-name argument" >&2
|
|
65
|
+
echo "Usage: $(basename "$0") <problem-file> <section-name>" >&2
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
if [ ! -f "$PROBLEM_FILE" ]; then
|
|
69
|
+
echo "ERROR: problem file not found: $PROBLEM_FILE" >&2
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# Lookup table: section-name -> (source-dir, glob, extraction-mode, id-pattern, title-render-helper).
|
|
74
|
+
# The helper body reads from this table; it does NOT branch on the
|
|
75
|
+
# section-name value directly (architect finding 4). Add a new section
|
|
76
|
+
# by extending the table.
|
|
77
|
+
declare -A SECTION_SOURCE_DIR
|
|
78
|
+
declare -A SECTION_GLOB
|
|
79
|
+
declare -A SECTION_MODE
|
|
80
|
+
declare -A SECTION_ID_PATTERN
|
|
81
|
+
|
|
82
|
+
SECTION_SOURCE_DIR["RFCs"]="docs/rfcs"
|
|
83
|
+
SECTION_GLOB["RFCs"]="docs/rfcs/RFC-*.md"
|
|
84
|
+
SECTION_MODE["RFCs"]="markdown-frontmatter"
|
|
85
|
+
SECTION_ID_PATTERN["RFCs"]="RFC-[0-9]+"
|
|
86
|
+
|
|
87
|
+
SECTION_SOURCE_DIR["Story Maps"]="docs/story-maps"
|
|
88
|
+
SECTION_GLOB["Story Maps"]="docs/story-maps/*/STORY-MAP-*.html"
|
|
89
|
+
SECTION_MODE["Story Maps"]="html-data-attribute"
|
|
90
|
+
SECTION_ID_PATTERN["Story Maps"]="STORY-MAP-[0-9]+"
|
|
91
|
+
|
|
92
|
+
SECTION_SOURCE_DIR["Stories"]="docs/stories"
|
|
93
|
+
SECTION_GLOB["Stories"]="docs/stories/*/STORY-*.md"
|
|
94
|
+
SECTION_MODE["Stories"]="markdown-frontmatter"
|
|
95
|
+
SECTION_ID_PATTERN["Stories"]="STORY-[0-9]+"
|
|
96
|
+
|
|
97
|
+
source_dir="${SECTION_SOURCE_DIR[$SECTION_NAME]:-}"
|
|
98
|
+
glob_pattern="${SECTION_GLOB[$SECTION_NAME]:-}"
|
|
99
|
+
extraction_mode="${SECTION_MODE[$SECTION_NAME]:-}"
|
|
100
|
+
id_pattern="${SECTION_ID_PATTERN[$SECTION_NAME]:-}"
|
|
101
|
+
|
|
102
|
+
if [ -z "$source_dir" ]; then
|
|
103
|
+
echo "ERROR: unknown section-name '$SECTION_NAME'. Supported: RFCs, Story Maps, Stories" >&2
|
|
104
|
+
exit 1
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# Extract problem ID from filename (NNN portion of NNN-<slug>.md or NNN-<slug>.<state>.md)
|
|
108
|
+
problem_basename=$(basename "$PROBLEM_FILE")
|
|
109
|
+
problem_id_num=$(echo "$problem_basename" | grep -oE '^[0-9]+')
|
|
110
|
+
if [ -z "$problem_id_num" ]; then
|
|
111
|
+
echo "ERROR: cannot extract problem ID from filename: $problem_basename" >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
problem_id="P${problem_id_num}"
|
|
115
|
+
|
|
116
|
+
# Collect matching artefact IDs via the configured extraction mode.
|
|
117
|
+
# Mode dispatch (the only branch in the body) is keyed on the
|
|
118
|
+
# extraction-mode value pulled from the lookup table, NOT on the
|
|
119
|
+
# section-name. Adding a new mode is a lookup-table extension; the
|
|
120
|
+
# branch grows by one case.
|
|
121
|
+
declare -a matched_ids=()
|
|
122
|
+
declare -a matched_titles=()
|
|
123
|
+
declare -a matched_statuses=()
|
|
124
|
+
|
|
125
|
+
extract_from_markdown_frontmatter() {
|
|
126
|
+
local file="$1"
|
|
127
|
+
# Read frontmatter problems: list. Match formats:
|
|
128
|
+
# problems: [P200, P201]
|
|
129
|
+
# problems: ["P200", "P201"]
|
|
130
|
+
local problems_line
|
|
131
|
+
problems_line=$(awk '/^---$/{f=!f;next} f && /^problems:/' "$file" | head -1)
|
|
132
|
+
if [ -z "$problems_line" ]; then
|
|
133
|
+
return 1
|
|
134
|
+
fi
|
|
135
|
+
echo "$problems_line" | grep -qE "\\bP${problem_id_num}\\b"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
extract_from_html_data_attribute() {
|
|
139
|
+
local file="$1"
|
|
140
|
+
# Read <meta name="problems" content="P200,P201">
|
|
141
|
+
local problems_line
|
|
142
|
+
problems_line=$(grep -E '<meta[[:space:]]+name="problems"[[:space:]]+content="[^"]+"' "$file" | head -1)
|
|
143
|
+
if [ -z "$problems_line" ]; then
|
|
144
|
+
return 1
|
|
145
|
+
fi
|
|
146
|
+
echo "$problems_line" | grep -qE "P${problem_id_num}\\b"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
extract_id_from_filename() {
|
|
150
|
+
local file="$1"
|
|
151
|
+
basename "$file" | grep -oE "$id_pattern" | head -1
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
extract_title_from_markdown() {
|
|
155
|
+
local file="$1"
|
|
156
|
+
awk '/^# / { sub(/^# /, ""); print; exit }' "$file"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
extract_title_from_html() {
|
|
160
|
+
local file="$1"
|
|
161
|
+
grep -oE '<title>[^<]+</title>' "$file" | head -1 | sed -E 's|<title>([^<]+)</title>|\1|'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
extract_status_from_markdown_frontmatter() {
|
|
165
|
+
local file="$1"
|
|
166
|
+
awk '/^---$/{f=!f;next} f && /^status:/{ sub(/^status:[[:space:]]*/, ""); gsub(/"/, ""); print; exit }' "$file"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
extract_status_from_html_meta() {
|
|
170
|
+
local file="$1"
|
|
171
|
+
grep -oE '<meta[[:space:]]+name="status"[[:space:]]+content="[^"]+"' "$file" | head -1 | sed -E 's|.*content="([^"]+)".*|\1|'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Per-mode extraction dispatch (the ONLY branch in the body — keyed on
|
|
175
|
+
# extraction-mode pulled from the lookup table, not on section-name).
|
|
176
|
+
case "$extraction_mode" in
|
|
177
|
+
markdown-frontmatter)
|
|
178
|
+
extract_match=extract_from_markdown_frontmatter
|
|
179
|
+
extract_title=extract_title_from_markdown
|
|
180
|
+
extract_status=extract_status_from_markdown_frontmatter
|
|
181
|
+
;;
|
|
182
|
+
html-data-attribute)
|
|
183
|
+
extract_match=extract_from_html_data_attribute
|
|
184
|
+
extract_title=extract_title_from_html
|
|
185
|
+
extract_status=extract_status_from_html_meta
|
|
186
|
+
;;
|
|
187
|
+
*)
|
|
188
|
+
echo "ERROR: unknown extraction-mode '$extraction_mode'" >&2
|
|
189
|
+
exit 1
|
|
190
|
+
;;
|
|
191
|
+
esac
|
|
192
|
+
|
|
193
|
+
shopt -s nullglob
|
|
194
|
+
for artefact in $glob_pattern; do
|
|
195
|
+
[ -e "$artefact" ] || continue
|
|
196
|
+
if "$extract_match" "$artefact"; then
|
|
197
|
+
aid=$(extract_id_from_filename "$artefact")
|
|
198
|
+
[ -n "$aid" ] || continue
|
|
199
|
+
title=$("$extract_title" "$artefact" 2>/dev/null || echo "")
|
|
200
|
+
status=$("$extract_status" "$artefact" 2>/dev/null || echo "unknown")
|
|
201
|
+
matched_ids+=("$aid")
|
|
202
|
+
matched_titles+=("$title")
|
|
203
|
+
matched_statuses+=("$status")
|
|
204
|
+
fi
|
|
205
|
+
done
|
|
206
|
+
shopt -u nullglob
|
|
207
|
+
|
|
208
|
+
# Render new section body (markdown table) when matches present;
|
|
209
|
+
# lazy-empty when not. Leading `\n` is intentionally omitted — the
|
|
210
|
+
# boundary is normalised at insertion time to exactly one blank line.
|
|
211
|
+
new_section=""
|
|
212
|
+
if [ ${#matched_ids[@]} -gt 0 ]; then
|
|
213
|
+
new_section="## ${SECTION_NAME}"$'\n\n'
|
|
214
|
+
new_section+=$'| ID | Title | Status |\n'
|
|
215
|
+
new_section+=$'|----|-------|--------|\n'
|
|
216
|
+
for i in "${!matched_ids[@]}"; do
|
|
217
|
+
new_section+="| ${matched_ids[$i]} | ${matched_titles[$i]} | ${matched_statuses[$i]} |"
|
|
218
|
+
new_section+=$'\n'
|
|
219
|
+
done
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
# Rewrite the problem file: strip existing ## <SECTION_NAME> section
|
|
223
|
+
# (if present) AND any preceding blank-line run that abutted it, then
|
|
224
|
+
# normalise trailing whitespace to exactly one final newline, then
|
|
225
|
+
# insert new_section at the canonical placement (before ## Fix
|
|
226
|
+
# Released; else at EOF separated by exactly one blank line).
|
|
227
|
+
tmp_file="$(mktemp)"
|
|
228
|
+
awk -v sec="## $SECTION_NAME" '
|
|
229
|
+
BEGIN { in_target=0; blank_buffer="" }
|
|
230
|
+
$0 == sec {
|
|
231
|
+
in_target=1
|
|
232
|
+
blank_buffer="" # discard buffered blanks that preceded this section
|
|
233
|
+
next
|
|
234
|
+
}
|
|
235
|
+
in_target && /^## / && $0 != sec { in_target=0 }
|
|
236
|
+
!in_target {
|
|
237
|
+
if ($0 ~ /^[[:space:]]*$/) {
|
|
238
|
+
if (blank_buffer == "") {
|
|
239
|
+
blank_buffer = "\n" # remember a single blank
|
|
240
|
+
}
|
|
241
|
+
# collapse runs of blanks into one
|
|
242
|
+
next
|
|
243
|
+
}
|
|
244
|
+
if (blank_buffer != "") {
|
|
245
|
+
printf "%s", blank_buffer
|
|
246
|
+
blank_buffer=""
|
|
247
|
+
}
|
|
248
|
+
print
|
|
249
|
+
}
|
|
250
|
+
END {
|
|
251
|
+
if (blank_buffer != "") {
|
|
252
|
+
printf "%s", blank_buffer
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
' "$PROBLEM_FILE" > "$tmp_file"
|
|
256
|
+
|
|
257
|
+
# Normalise trailing whitespace to single newline
|
|
258
|
+
tmp_file2="$(mktemp)"
|
|
259
|
+
awk 'BEGIN{c=0} /^[[:space:]]*$/{c++; next} {for(i=0;i<c;i++)print ""; c=0; print} END{print ""}' "$tmp_file" > "$tmp_file2"
|
|
260
|
+
mv "$tmp_file2" "$tmp_file"
|
|
261
|
+
|
|
262
|
+
# Append new section before ## Fix Released, or at EOF
|
|
263
|
+
if [ -n "$new_section" ]; then
|
|
264
|
+
if grep -q '^## Fix Released' "$tmp_file"; then
|
|
265
|
+
tmp_file2="$(mktemp)"
|
|
266
|
+
awk -v section="$new_section" '
|
|
267
|
+
/^## Fix Released/ { printf "%s\n", section; print; next }
|
|
268
|
+
{ print }
|
|
269
|
+
' "$tmp_file" > "$tmp_file2"
|
|
270
|
+
mv "$tmp_file2" "$tmp_file"
|
|
271
|
+
else
|
|
272
|
+
# Ensure file ends with exactly one blank line before section
|
|
273
|
+
printf '\n%s' "$new_section" >> "$tmp_file"
|
|
274
|
+
fi
|
|
275
|
+
fi
|
|
276
|
+
|
|
277
|
+
# Idempotent write: only update if content changed
|
|
278
|
+
if ! cmp -s "$tmp_file" "$PROBLEM_FILE"; then
|
|
279
|
+
mv "$tmp_file" "$PROBLEM_FILE"
|
|
280
|
+
else
|
|
281
|
+
rm -f "$tmp_file"
|
|
282
|
+
fi
|
|
283
|
+
|
|
284
|
+
exit 0
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# packages/itil/scripts/update-rfc-references-section.sh
|
|
3
|
+
#
|
|
4
|
+
# Generalised reverse-trace section updater for RFC files. Mirror of
|
|
5
|
+
# update-problem-references-section.sh with the lookup table tuned for
|
|
6
|
+
# RFC-on-RFC reverse traces:
|
|
7
|
+
# - ## Story Maps : sources docs/story-maps/*/*.html via data attributes
|
|
8
|
+
# - ## Stories : forward-trace from RFC's own frontmatter `stories:`
|
|
9
|
+
#
|
|
10
|
+
# Per ADR-060 § Phase 2 encoding amendment 2026-05-12 architect finding 4:
|
|
11
|
+
# no per-section-name branching in body; lookup-table-driven dispatch.
|
|
12
|
+
#
|
|
13
|
+
# Usage: update-rfc-references-section.sh <rfc-file> <section-name>
|
|
14
|
+
#
|
|
15
|
+
# @adr ADR-060 (Phase 2 encoding amendment 2026-05-12)
|
|
16
|
+
# @problem P170 (Phase 2 Slice 2b)
|
|
17
|
+
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
RFC_FILE="${1:-}"
|
|
21
|
+
SECTION_NAME="${2:-}"
|
|
22
|
+
|
|
23
|
+
if [ -z "$RFC_FILE" ]; then
|
|
24
|
+
echo "ERROR: missing rfc-file argument" >&2
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
if [ -z "$SECTION_NAME" ]; then
|
|
28
|
+
echo "ERROR: missing section-name argument" >&2
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
if [ ! -f "$RFC_FILE" ]; then
|
|
32
|
+
echo "ERROR: rfc file not found: $RFC_FILE" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
declare -A SECTION_GLOB SECTION_MODE SECTION_ID_PATTERN
|
|
37
|
+
|
|
38
|
+
SECTION_GLOB["Story Maps"]="docs/story-maps/*/STORY-MAP-*.html"
|
|
39
|
+
SECTION_MODE["Story Maps"]="html-data-attribute-rfc"
|
|
40
|
+
SECTION_ID_PATTERN["Story Maps"]="STORY-MAP-[0-9]+"
|
|
41
|
+
|
|
42
|
+
SECTION_GLOB["Stories"]="docs/stories/*/STORY-*.md"
|
|
43
|
+
SECTION_MODE["Stories"]="markdown-frontmatter-rfc"
|
|
44
|
+
SECTION_ID_PATTERN["Stories"]="STORY-[0-9]+"
|
|
45
|
+
|
|
46
|
+
glob_pattern="${SECTION_GLOB[$SECTION_NAME]:-}"
|
|
47
|
+
extraction_mode="${SECTION_MODE[$SECTION_NAME]:-}"
|
|
48
|
+
id_pattern="${SECTION_ID_PATTERN[$SECTION_NAME]:-}"
|
|
49
|
+
|
|
50
|
+
if [ -z "$glob_pattern" ]; then
|
|
51
|
+
echo "ERROR: unknown section-name '$SECTION_NAME'. Supported: Story Maps, Stories" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Extract RFC ID from filename: RFC-NNN-slug.<status>.md or RFC-NNN-slug.md
|
|
56
|
+
rfc_basename=$(basename "$RFC_FILE")
|
|
57
|
+
rfc_id=$(echo "$rfc_basename" | grep -oE '^RFC-[0-9]+' | head -1)
|
|
58
|
+
if [ -z "$rfc_id" ]; then
|
|
59
|
+
echo "ERROR: cannot extract RFC ID from filename: $rfc_basename" >&2
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
declare -a matched_ids=() matched_titles=() matched_statuses=()
|
|
64
|
+
|
|
65
|
+
extract_from_html_rfcs_meta() {
|
|
66
|
+
local file="$1"
|
|
67
|
+
local rfcs_line
|
|
68
|
+
rfcs_line=$(grep -E '<meta[[:space:]]+name="rfcs"[[:space:]]+content="[^"]+"' "$file" | head -1)
|
|
69
|
+
[ -n "$rfcs_line" ] || return 1
|
|
70
|
+
echo "$rfcs_line" | grep -qE "\\b${rfc_id}\\b"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
extract_from_markdown_frontmatter_rfcs() {
|
|
74
|
+
local file="$1"
|
|
75
|
+
local rfcs_line
|
|
76
|
+
rfcs_line=$(awk '/^---$/{f=!f;next} f && /^rfcs:/' "$file" | head -1)
|
|
77
|
+
[ -n "$rfcs_line" ] || return 1
|
|
78
|
+
echo "$rfcs_line" | grep -qE "\\b${rfc_id}\\b"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
extract_id_from_filename() { basename "$1" | grep -oE "$id_pattern" | head -1; }
|
|
82
|
+
extract_title_from_markdown() { awk '/^# / { sub(/^# /, ""); print; exit }' "$1"; }
|
|
83
|
+
extract_title_from_html() { grep -oE '<title>[^<]+</title>' "$1" | head -1 | sed -E 's|<title>([^<]+)</title>|\1|'; }
|
|
84
|
+
extract_status_from_markdown() { awk '/^---$/{f=!f;next} f && /^status:/{ sub(/^status:[[:space:]]*/, ""); gsub(/"/, ""); print; exit }' "$1"; }
|
|
85
|
+
extract_status_from_html() { grep -oE '<meta[[:space:]]+name="status"[[:space:]]+content="[^"]+"' "$1" | head -1 | sed -E 's|.*content="([^"]+)".*|\1|'; }
|
|
86
|
+
|
|
87
|
+
case "$extraction_mode" in
|
|
88
|
+
html-data-attribute-rfc)
|
|
89
|
+
extract_match=extract_from_html_rfcs_meta
|
|
90
|
+
extract_title=extract_title_from_html
|
|
91
|
+
extract_status=extract_status_from_html
|
|
92
|
+
;;
|
|
93
|
+
markdown-frontmatter-rfc)
|
|
94
|
+
extract_match=extract_from_markdown_frontmatter_rfcs
|
|
95
|
+
extract_title=extract_title_from_markdown
|
|
96
|
+
extract_status=extract_status_from_markdown
|
|
97
|
+
;;
|
|
98
|
+
*)
|
|
99
|
+
echo "ERROR: unknown extraction-mode '$extraction_mode'" >&2
|
|
100
|
+
exit 1
|
|
101
|
+
;;
|
|
102
|
+
esac
|
|
103
|
+
|
|
104
|
+
shopt -s nullglob
|
|
105
|
+
for artefact in $glob_pattern; do
|
|
106
|
+
[ -e "$artefact" ] || continue
|
|
107
|
+
if "$extract_match" "$artefact"; then
|
|
108
|
+
aid=$(extract_id_from_filename "$artefact")
|
|
109
|
+
[ -n "$aid" ] || continue
|
|
110
|
+
matched_ids+=("$aid")
|
|
111
|
+
matched_titles+=("$("$extract_title" "$artefact" 2>/dev/null || echo "")")
|
|
112
|
+
matched_statuses+=("$("$extract_status" "$artefact" 2>/dev/null || echo "unknown")")
|
|
113
|
+
fi
|
|
114
|
+
done
|
|
115
|
+
shopt -u nullglob
|
|
116
|
+
|
|
117
|
+
new_section=""
|
|
118
|
+
if [ ${#matched_ids[@]} -gt 0 ]; then
|
|
119
|
+
new_section="## ${SECTION_NAME}"$'\n\n| ID | Title | Status |\n|----|-------|--------|\n'
|
|
120
|
+
for i in "${!matched_ids[@]}"; do
|
|
121
|
+
new_section+="| ${matched_ids[$i]} | ${matched_titles[$i]} | ${matched_statuses[$i]} |"$'\n'
|
|
122
|
+
done
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
tmp_file="$(mktemp)"
|
|
126
|
+
awk -v sec="## $SECTION_NAME" '
|
|
127
|
+
BEGIN { in_target=0; blank_buffer="" }
|
|
128
|
+
$0 == sec { in_target=1; blank_buffer=""; next }
|
|
129
|
+
in_target && /^## / && $0 != sec { in_target=0 }
|
|
130
|
+
!in_target {
|
|
131
|
+
if ($0 ~ /^[[:space:]]*$/) { if (blank_buffer == "") blank_buffer="\n"; next }
|
|
132
|
+
if (blank_buffer != "") { printf "%s", blank_buffer; blank_buffer="" }
|
|
133
|
+
print
|
|
134
|
+
}
|
|
135
|
+
END { if (blank_buffer != "") printf "%s", blank_buffer }
|
|
136
|
+
' "$RFC_FILE" > "$tmp_file"
|
|
137
|
+
|
|
138
|
+
tmp_file2="$(mktemp)"
|
|
139
|
+
awk 'BEGIN{c=0} /^[[:space:]]*$/{c++; next} {for(i=0;i<c;i++)print ""; c=0; print} END{print ""}' "$tmp_file" > "$tmp_file2"
|
|
140
|
+
mv "$tmp_file2" "$tmp_file"
|
|
141
|
+
|
|
142
|
+
if [ -n "$new_section" ]; then
|
|
143
|
+
printf '\n%s' "$new_section" >> "$tmp_file"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
if ! cmp -s "$tmp_file" "$RFC_FILE"; then
|
|
147
|
+
mv "$tmp_file" "$RFC_FILE"
|
|
148
|
+
else
|
|
149
|
+
rm -f "$tmp_file"
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
exit 0
|