@windyroad/itil 0.61.0 → 0.61.1-preview.1037
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/lib/story-oversight.sh
CHANGED
|
@@ -19,15 +19,43 @@
|
|
|
19
19
|
# encodings in one filter:
|
|
20
20
|
# - markdown: `human-oversight:` / `oversight-hash:` frontmatter lines
|
|
21
21
|
# - HTML: <meta name="human-oversight" ...> / <meta name="oversight-hash" ...>
|
|
22
|
+
# THE single definition of what the fingerprint ignores. Reads stdin, writes the
|
|
23
|
+
# normalised stream. Both hash functions route through this and neither carries
|
|
24
|
+
# its own copy — that duplication is why the P474 `**Status**:` mirror had to be
|
|
25
|
+
# removed from two places and why a third copy could have been missed entirely
|
|
26
|
+
# (RFC-059 / STORY-055).
|
|
27
|
+
#
|
|
28
|
+
# Excludes the marker lines plus the lifecycle `status` key in both encodings, and
|
|
29
|
+
# normalises lifecycle PROGRESS — acceptance-criterion ticks and slice
|
|
30
|
+
# `data-status` — so ONLY a SUBSTANCE change re-opens ratification. Ticking a
|
|
31
|
+
# criterion or advancing status is progress, not a revision of what the human
|
|
32
|
+
# ratified; the value statement, criterion TEXT and structure all still drift.
|
|
33
|
+
#
|
|
34
|
+
# Editing this filter changes ADR-090's Decision Outcome, not merely a mechanism.
|
|
35
|
+
# It is the surface the 2026-07-03 narrowing changed while going unrecorded for
|
|
36
|
+
# three weeks; a change here needs an ADR-090 amendment. The pattern is a
|
|
37
|
+
# LITERAL on purpose: built from a variable, an expansion that ever yielded empty
|
|
38
|
+
# would make `grep -vE ''` suppress every line and hash the empty stream to a
|
|
39
|
+
# constant, after which one artefact's stored hash would validate against ANY
|
|
40
|
+
# content — and `itil-no-implement-draft-gate` sources this lib under
|
|
41
|
+
# `2>/dev/null || exit 0`, so that failure would remove the gate silently.
|
|
42
|
+
_oversight_filter() {
|
|
43
|
+
grep -vE '^(human-oversight|oversight-hash|oversight-basis|status):|<meta[^>]*name="(human-oversight|oversight-hash|oversight-basis|status)"' \
|
|
44
|
+
| sed -E 's/- \[[ xX]\]/- [ ]/g; s/data-status="[^"]*"/data-status=""/g'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# The excluded key set, readable by tests and the corpus lint. Kept in agreement
|
|
48
|
+
# with the literal above by a bidirectional test rather than by construction, for
|
|
49
|
+
# the reason in that comment. One key per line.
|
|
50
|
+
oversight_excluded_keys() {
|
|
51
|
+
printf '%s\n' human-oversight oversight-hash oversight-basis status
|
|
52
|
+
}
|
|
53
|
+
|
|
22
54
|
oversight_content_hash() {
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
# value statement, criterion TEXT, and structure still drift the hash.
|
|
28
|
-
grep -vE '^(human-oversight|oversight-hash|oversight-basis|status):|<meta[^>]*name="(human-oversight|oversight-hash|oversight-basis|status)"' "$1" \
|
|
29
|
-
| sed -E 's/- \[[ xX]\]/- [ ]/g; s/data-status="[^"]*"/data-status=""/g' \
|
|
30
|
-
| shasum -a 256 | awk '{print $1}'
|
|
55
|
+
# Input path: the file is fed to the filter directly, so trailing blank lines
|
|
56
|
+
# are PRESERVED. Deliberately NOT unified with the map variant below — see the
|
|
57
|
+
# note there. Changing this changes every stored fingerprint.
|
|
58
|
+
_oversight_filter < "$1" | shasum -a 256 | awk '{print $1}'
|
|
31
59
|
}
|
|
32
60
|
|
|
33
61
|
# Hash a story MAP's content while EXCLUDING the single-line card elements whose
|
|
@@ -52,10 +80,16 @@ oversight_content_hash_excluding_stories() {
|
|
|
52
80
|
# Anchored on the closing quote so STORY-05 cannot strip STORY-054's card.
|
|
53
81
|
filtered="$(printf '%s\n' "$filtered" | grep -vF "data-story-id=\"${id}\"" || true)"
|
|
54
82
|
done
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
83
|
+
# Input path: `$(cat)` above stripped ALL trailing newlines and this `printf`
|
|
84
|
+
# re-adds exactly one, so trailing blank lines are COLLAPSED here where
|
|
85
|
+
# `oversight_content_hash` preserves them. That divergence is pre-existing and
|
|
86
|
+
# is preserved deliberately — unifying the two input paths would silently change
|
|
87
|
+
# one function's hash and un-ratify every stored fingerprint at once. A
|
|
88
|
+
# consequence worth knowing: with zero ids this does NOT equal
|
|
89
|
+
# `oversight_content_hash` for an artefact with trailing blank lines, which is
|
|
90
|
+
# recorded as its own P474 task because it makes ADR-101's map leg
|
|
91
|
+
# unsatisfiable for such a map even with the right card excluded.
|
|
92
|
+
printf '%s\n' "$filtered" | _oversight_filter | shasum -a 256 | awk '{print $1}'
|
|
59
93
|
}
|
|
60
94
|
|
|
61
95
|
# True (0) if this artefact's `confirmed` marker was written by the ADR-101 AFK
|
package/package.json
CHANGED
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
grep -rniE "$(mirror_pattern)" "$@" 2>/dev/null || true
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@test "lint: the excluded-key accessor is non-empty and the pattern is well-formed" {
|
|
62
|
+
# Guard against a silently-empty pattern — the whole lint would go vacuous.
|
|
63
|
+
local keys; keys="$(oversight_excluded_keys)"
|
|
64
|
+
[ -n "$keys" ]
|
|
65
|
+
[ "$(printf '%s\n' "$keys" | wc -l | tr -d ' ')" -ge 4 ]
|
|
66
|
+
[[ "$(mirror_pattern)" == *'status'* ]]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@test "lint: search roots exist and hold artefacts — fails loudly, never vacuously" {
|
|
70
|
+
# A renamed or adopter-relative path must fail here rather than let the scan
|
|
71
|
+
# below pass by matching nothing. This is the shape that reddened main once.
|
|
72
|
+
[ -d "$STORIES" ]
|
|
73
|
+
[ -d "$MAPS" ]
|
|
74
|
+
local n
|
|
75
|
+
n="$(find "$STORIES" -name 'STORY-*.md' -type f 2>/dev/null | wc -l | tr -d ' ')"
|
|
76
|
+
[ "$n" -ge 1 ] || { echo "no STORY-*.md under $STORIES — lint would be vacuous"; false; }
|
|
77
|
+
n="$(find "$MAPS" -name '*.html' -type f 2>/dev/null | wc -l | tr -d ' ')"
|
|
78
|
+
[ "$n" -ge 1 ] || { echo "no maps under $MAPS — lint would be vacuous"; false; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@test "lint: no story artefact mirrors an excluded frontmatter key in its body" {
|
|
82
|
+
local hits
|
|
83
|
+
hits="$(scan "$STORIES"/*/STORY-*.md)"
|
|
84
|
+
[ -z "$hits" ] || {
|
|
85
|
+
printf 'body mirror of an excluded key found — accepting these stories will drop their ratification:\n%s\n' "$hits"
|
|
86
|
+
printf 'remove the line; run `wr-itil-migrate-story-status-mirror docs/stories` for an existing corpus.\n'
|
|
87
|
+
false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@test "lint: no story map mirrors an excluded frontmatter key in its body" {
|
|
92
|
+
# The map's own `<meta name="status">` is the key's HOME in the HTML encoding,
|
|
93
|
+
# not a mirror of it, and the lib's filter already excludes it — so the
|
|
94
|
+
# line-anchored bold-key pattern correctly does not flag it.
|
|
95
|
+
local hits
|
|
96
|
+
hits="$(scan "$MAPS"/*/*.html)"
|
|
97
|
+
[ -z "$hits" ] || {
|
|
98
|
+
printf 'body mirror of an excluded key found in a map:\n%s\n' "$hits"
|
|
99
|
+
false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@test "lint: RED on an injected mirror — proves the pattern actually fires" {
|
|
104
|
+
mkdir -p "$TMPD/stories/draft"
|
|
105
|
+
printf -- '---\nstatus: accepted\n---\n\n# STORY-999: x\n\n**Status**: accepted\n**Reported**: 2026-07-30\n' \
|
|
106
|
+
> "$TMPD/stories/draft/STORY-999-x.md"
|
|
107
|
+
local hits; hits="$(scan "$TMPD/stories"/*/STORY-*.md)"
|
|
108
|
+
[ -n "$hits" ] || { echo "lint failed to detect an injected line-start **Status**: mirror"; false; }
|
|
109
|
+
[[ "$hits" == *"STORY-999"* ]]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@test "lint: prose ABOUT a mirror is not flagged — that is what line-anchoring buys" {
|
|
113
|
+
mkdir -p "$TMPD/prose/draft"
|
|
114
|
+
printf -- '---\nstatus: accepted\n---\n\n# STORY-998: y\n\nThe `**Status**:` line used to be hashed.\n<!-- No **Status**: body line, deliberately. -->\n' \
|
|
115
|
+
> "$TMPD/prose/draft/STORY-998-y.md"
|
|
116
|
+
[ -z "$(scan "$TMPD/prose"/*/STORY-*.md)" ]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# The behavioural half. Covers a mirror of ANY excluded key in ANY shape,
|
|
120
|
+
# including the HTML meta form the lint's pattern cannot reach.
|
|
121
|
+
@test "behavioural: a mirror of any excluded key drifts a ratified artefact" {
|
|
122
|
+
local k
|
|
123
|
+
for k in $(oversight_excluded_keys); do
|
|
124
|
+
# Skip the marker keys themselves — those are excluded BY the marker write,
|
|
125
|
+
# so a "mirror" of them is what the filter is for.
|
|
126
|
+
case "$k" in human-oversight|oversight-hash|oversight-basis) continue ;; esac
|
|
127
|
+
local f="$TMPD/mirror-$k.md"
|
|
128
|
+
printf -- '---\n%s: draft\nhuman-oversight: confirmed\noversight-hash: P\n---\n\n# STORY-997\n\nbody\n' "$k" > "$f"
|
|
129
|
+
local h; h="$(oversight_content_hash "$f")"
|
|
130
|
+
sed -i.bak "s/oversight-hash: P/oversight-hash: $h/" "$f" && rm -f "$f.bak"
|
|
131
|
+
run is_story_map_ratified "$f"
|
|
132
|
+
[ "$status" -eq 0 ]
|
|
133
|
+
# Add a body mirror of that key, then advance BOTH copies the way a
|
|
134
|
+
# transition would. The frontmatter change alone is excluded; the body
|
|
135
|
+
# mirror is not — so ratification must drop, which is P474 exactly.
|
|
136
|
+
printf -- '**%s**: accepted\n' "$k" >> "$f"
|
|
137
|
+
h="$(oversight_content_hash "$f")"
|
|
138
|
+
sed -i.bak "s/^oversight-hash: .*/oversight-hash: $h/" "$f" && rm -f "$f.bak"
|
|
139
|
+
sed -i.bak "s/^${k}: draft/${k}: accepted/; s/^\\*\\*${k}\\*\\*: accepted/**${k}**: DIFFERENT/" "$f" && rm -f "$f.bak"
|
|
140
|
+
run is_story_map_ratified "$f"
|
|
141
|
+
[ "$status" -ne 0 ] || { echo "a body mirror of '$k' did not drift the hash"; false; }
|
|
142
|
+
done
|
|
143
|
+
}
|
|
@@ -86,3 +86,105 @@ teardown() { rm -rf "$TMPD"; }
|
|
|
86
86
|
printf '<meta name="human-oversight" content="confirmed">\n<meta name="oversight-hash" content="%s">\n<h1>map</h1>\n' "$h" > "$TMPD/m.html"
|
|
87
87
|
run is_story_map_ratified "$TMPD/m.html"; [ "$status" -eq 0 ]
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
# --- P474 / STORY-055: the extraction must not move a single hash --------------
|
|
91
|
+
#
|
|
92
|
+
# `oversight_content_hash` and `oversight_content_hash_excluding_stories` shared a
|
|
93
|
+
# verbatim grep|sed filter, which is why the P474 mirror had to be removed twice
|
|
94
|
+
# and why a third copy could have been missed. The filter is now extracted.
|
|
95
|
+
#
|
|
96
|
+
# The extraction boundary is the whole risk. The two functions' INPUT paths are
|
|
97
|
+
# NOT equivalent and must stay that way: one passes the file to grep directly,
|
|
98
|
+
# the other round-trips through $(cat) — which strips all trailing newlines —
|
|
99
|
+
# then printf '%s\n' re-adds exactly one. So trailing blank lines are preserved
|
|
100
|
+
# by the first and collapsed by the second. Extract the filter only; unify the
|
|
101
|
+
# input paths "for symmetry" and the hash silently changes for any artefact with
|
|
102
|
+
# anomalous trailing whitespace, un-ratifying every stored fingerprint at once.
|
|
103
|
+
#
|
|
104
|
+
# `_ref_*` below are VERBATIM copies of the pre-extraction implementation. They
|
|
105
|
+
# exist to be compared against, so do NOT refactor them to call the new helper —
|
|
106
|
+
# that would make the equivalence assertions circular and worthless.
|
|
107
|
+
|
|
108
|
+
_ref_hash() {
|
|
109
|
+
grep -vE '^(human-oversight|oversight-hash|oversight-basis|status):|<meta[^>]*name="(human-oversight|oversight-hash|oversight-basis|status)"' "$1" \
|
|
110
|
+
| sed -E 's/- \[[ xX]\]/- [ ]/g; s/data-status="[^"]*"/data-status=""/g' \
|
|
111
|
+
| shasum -a 256 | awk '{print $1}'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_ref_hash_excluding() {
|
|
115
|
+
local f="$1"; shift
|
|
116
|
+
local filtered id
|
|
117
|
+
filtered="$(cat "$f")"
|
|
118
|
+
for id in "$@"; do
|
|
119
|
+
[ -n "$id" ] || continue
|
|
120
|
+
filtered="$(printf '%s\n' "$filtered" | grep -vF "data-story-id=\"${id}\"" || true)"
|
|
121
|
+
done
|
|
122
|
+
printf '%s\n' "$filtered" \
|
|
123
|
+
| grep -vE '^(human-oversight|oversight-hash|oversight-basis|status):|<meta[^>]*name="(human-oversight|oversight-hash|oversight-basis|status)"' \
|
|
124
|
+
| sed -E 's/- \[[ xX]\]/- [ ]/g; s/data-status="[^"]*"/data-status=""/g' \
|
|
125
|
+
| shasum -a 256 | awk '{print $1}'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@test "extraction: byte-identical to the frozen reference across edge shapes" {
|
|
129
|
+
# No final newline.
|
|
130
|
+
printf -- '---\nstatus: draft\n---\nbody' > "$TMPD/a"
|
|
131
|
+
# Multiple trailing blank lines — the shape the input-path divergence hinges on.
|
|
132
|
+
printf -- '---\nstatus: draft\n---\nbody\n\n\n\n' > "$TMPD/b"
|
|
133
|
+
# CRLF.
|
|
134
|
+
printf -- '---\r\nstatus: draft\r\n---\r\nbody\r\n' > "$TMPD/c"
|
|
135
|
+
# Empty file.
|
|
136
|
+
: > "$TMPD/d"
|
|
137
|
+
# A criterion tick and a data-status, both normalised.
|
|
138
|
+
printf -- '- [x] done\n<a data-status="done">x</a>\n' > "$TMPD/e"
|
|
139
|
+
# Every excluded key present, in both encodings.
|
|
140
|
+
printf -- 'human-oversight: confirmed\noversight-hash: deadbeef\noversight-basis: pure-decomposition\nstatus: accepted\n<meta name="status" content="draft">\nbody\n' > "$TMPD/f"
|
|
141
|
+
# Only a newline.
|
|
142
|
+
printf '\n' > "$TMPD/g"
|
|
143
|
+
|
|
144
|
+
for n in a b c d e f g; do
|
|
145
|
+
[ "$(oversight_content_hash "$TMPD/$n")" = "$(_ref_hash "$TMPD/$n")" ] || {
|
|
146
|
+
echo "oversight_content_hash diverged on fixture $n"; false
|
|
147
|
+
}
|
|
148
|
+
done
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@test "extraction: map variant byte-identical, including the id-exclusion loop" {
|
|
152
|
+
printf -- '<meta name="status" content="draft">\n<a data-story-id="STORY-05">short</a>\n<a data-story-id="STORY-054">long</a>\n<a class="slice" data-status="done">s</a>\nbody\n\n\n' \
|
|
153
|
+
> "$TMPD/m.html"
|
|
154
|
+
# Zero ids, one id, and the prefix-collision pair — STORY-05 must not strip
|
|
155
|
+
# STORY-054's card, which is what the closing-quote anchor buys.
|
|
156
|
+
[ "$(oversight_content_hash_excluding_stories "$TMPD/m.html")" = "$(_ref_hash_excluding "$TMPD/m.html")" ]
|
|
157
|
+
[ "$(oversight_content_hash_excluding_stories "$TMPD/m.html" STORY-054)" = "$(_ref_hash_excluding "$TMPD/m.html" STORY-054)" ]
|
|
158
|
+
[ "$(oversight_content_hash_excluding_stories "$TMPD/m.html" STORY-05)" = "$(_ref_hash_excluding "$TMPD/m.html" STORY-05)" ]
|
|
159
|
+
[ "$(oversight_content_hash_excluding_stories "$TMPD/m.html" STORY-05 STORY-054)" = "$(_ref_hash_excluding "$TMPD/m.html" STORY-05 STORY-054)" ]
|
|
160
|
+
# And the divergence itself is preserved rather than accidentally unified.
|
|
161
|
+
[ "$(oversight_content_hash_excluding_stories "$TMPD/m.html")" != "$(oversight_content_hash "$TMPD/m.html")" ]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Golden hashes are pinned over FIXTURES, never over live artefacts. Pinning to
|
|
165
|
+
# the corpus would redden this suite on every later legitimate artefact edit —
|
|
166
|
+
# ADR-052's refactor-blocking failure mode. Their job is to catch the frozen
|
|
167
|
+
# reference itself rotting, which the equivalence cases above cannot.
|
|
168
|
+
@test "extraction: pinned golden hashes over a canonical fixture" {
|
|
169
|
+
printf -- '---\nstatus: draft\nhuman-oversight: confirmed\n---\n\n# STORY-900: canonical\n\n**Reported**: 2026-07-30\n\n- [ ] a criterion\n' \
|
|
170
|
+
> "$TMPD/golden.md"
|
|
171
|
+
[ "$(oversight_content_hash "$TMPD/golden.md")" = "1a18f2501f7092b64316a1566795e64860f90893c2b4d0d36cd9a783ada21414" ]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@test "extraction: the excluded-key accessor agrees with the operative pattern both ways" {
|
|
175
|
+
local keys; keys="$(oversight_excluded_keys)"
|
|
176
|
+
[ -n "$keys" ]
|
|
177
|
+
# Every accessor key appears as an alternation branch in the operative filter.
|
|
178
|
+
for k in $keys; do
|
|
179
|
+
grep -qF -- "$k" "$LIB" || { echo "accessor key '$k' absent from the lib pattern"; false; }
|
|
180
|
+
done
|
|
181
|
+
# And every branch in the operative pattern appears in the accessor — the
|
|
182
|
+
# direction that catches a key ADDED to the regex but not to the accessor,
|
|
183
|
+
# which would leave the corpus lint blind to it.
|
|
184
|
+
local branch
|
|
185
|
+
for branch in $(grep -oE '\^\(([a-z-]+\|)+[a-z-]+\):' "$LIB" | head -1 | tr -d '^():' | tr '|' ' '); do
|
|
186
|
+
printf '%s\n' "$keys" | grep -qx -- "$branch" || {
|
|
187
|
+
echo "pattern branch '$branch' missing from oversight_excluded_keys()"; false
|
|
188
|
+
}
|
|
189
|
+
done
|
|
190
|
+
}
|