bigpowers 2.58.1 → 2.58.3

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.
@@ -1,59 +1,58 @@
1
1
  #!/usr/bin/env bash
2
- #
3
- # validate-okf.sh — provenance gate for OKF story-metrics bundles (e40/e31 style).
4
- #
5
- # A bundle is valid iff:
6
- # 1. The generator ran (generator field matches scripts/record-cycle-time.sh).
7
- # 2. commit_range resolves to real commits in the repo.
8
- # 3. Source enum is valid (measured|estimated|backfilled).
9
- # 4. Required keys are present with correct aggregation tags.
10
- #
11
- # This script gates on PROVENANCE, NEVER on a specific metric value.
12
- # Gate on freshness (did the right generator run?) not on outcomes.
13
- #
14
- # Usage:
15
- # scripts/validate-okf.sh [--dir <path>] [--help]
16
- # scripts/validate-okf.sh --bundle <file> # validate a single bundle
17
- #
2
+ # story: e38s09
3
+ # validate-okf.sh — kind-aware, fail-closed provenance gate for OKF bundles.
4
+ # Validates story-metrics, spec-migration, and migration-registry bundles.
5
+ # Multi-dir default: specs/metrics/ + specs/migrations/.
18
6
  set -euo pipefail
19
7
 
20
8
  ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "validate-okf: not inside a git repo"; exit 1; }
21
- METRICS_DIR="$ROOT/specs/metrics"
22
9
  EXIT_CODE=0
23
- REQUIRED_KEYS="id epic bcps commit_range source generated_at generator"
24
- VALID_SOURCES="measured estimated backfilled"
25
10
 
26
11
  RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m'
27
12
 
28
- usage() {
13
+ usage_okf() {
29
14
  cat <<'EOF'
30
15
  Usage: scripts/validate-okf.sh [flags]
31
16
 
32
17
  Flags:
33
- --dir <path> Validate all OKF bundles in directory (default: specs/metrics/)
18
+ --dir <path> Validate all OKF bundles in directory
34
19
  --bundle <file> Validate a single OKF bundle
35
20
  --help Show this message
36
21
 
37
22
  Description:
38
- Provenance gate for OKF story-metrics bundles. Validates that:
39
- - Required keys are present (id, epic, bcps, commit_range, source, generated_at, generator)
40
- - Source enum is valid (measured|estimated|backfilled)
41
- - Generator matches scripts/record-cycle-time.sh
42
- - commit_range resolves to real commits
43
- - Aggregation tags are present (Σ, ⌀, %, •)
44
-
45
- NEVER gates on a specific metric value — only on provenance and freshness.
23
+ Kind-aware provenance gate for OKF bundles. Without flags, validates all
24
+ bundles in specs/metrics/ AND specs/migrations/. NEVER gates on a specific
25
+ metric value — only on provenance and schema conformance.
26
+
27
+ OKF kinds validated:
28
+ story-metrics — effort/lead-time provenance (e40)
29
+ spec-migration — migration bundle schema (e44)
30
+ migration-registry — registry index integrity (e44)
46
31
  EOF
47
32
  exit 0
48
33
  }
49
34
 
50
- # Parse a single YAML frontmatter block from an OKF bundle .md file.
51
- # Returns the frontmatter as JSON via Python (which handles YAML safely).
35
+ # ---- Helpers ------------------------------------------------------------
36
+
37
+ # True if value is empty, null JSON literal, or Python None string.
38
+ val_nullish() { [ -z "$1" ] || [ "$1" = "null" ] || [ "$1" = "None" ]; }
39
+
40
+ # True if value is nullish OR the JSON empty array literal.
41
+ val_nullish_or_empty() { val_nullish "$1" || [ "$1" = "[]" ]; }
42
+
43
+ # ---- YAML frontmatter parser (datetime-safe) ---------------------------
52
44
  parse_frontmatter() {
53
45
  local file="$1"
54
- # Extract YAML frontmatter between --- markers
55
46
  python3 -c "
56
- import yaml, sys
47
+ import yaml, sys, json
48
+
49
+ class SafeEncoder(json.JSONEncoder):
50
+ def default(self, obj):
51
+ try:
52
+ return super().default(obj)
53
+ except TypeError:
54
+ return str(obj)
55
+
57
56
  content = open('$file').read()
58
57
  parts = content.split('---')
59
58
  if len(parts) < 3:
@@ -62,137 +61,223 @@ try:
62
61
  fm = yaml.safe_load(parts[1])
63
62
  if fm is None:
64
63
  sys.exit(1)
65
- # Emit as JSON for bash
66
- import json
67
- print(json.dumps(fm))
64
+ print(json.dumps(fm, cls=SafeEncoder))
68
65
  except Exception as e:
69
- print(f'validate-okf: YAML parse error in $file: {e}', file=sys.stderr)
66
+ print(f'YAML parse error: {e}', file=sys.stderr)
70
67
  sys.exit(1)
71
68
  " 2>/dev/null
72
69
  }
73
70
 
74
- validate_bundle() {
75
- local file="$1"
76
- local bundle_name; bundle_name="$(basename "$file")"
77
-
78
- # Parse frontmatter
79
- local fm
80
- fm="$(parse_frontmatter "$file")" || {
81
- printf "${RED}FAIL${NC} %s: cannot parse YAML frontmatter\n" "$bundle_name"
82
- EXIT_CODE=1
83
- return
84
- }
71
+ # ---- Kind-specific validators ------------------------------------------
85
72
 
86
- # Check okf_kind
87
- local kind
88
- kind="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('okf_kind',''))" 2>/dev/null)"
89
- if [ "$kind" != "story-metrics" ]; then
90
- printf "${YELLOW}SKIP${NC} %s: not a story-metrics bundle (kind=%s)\n" "$bundle_name" "${kind:-none}"
91
- return
92
- fi
73
+ validate_story_metrics() {
74
+ local file="$1" fm="$2" name="$3"
75
+ local errs=0
93
76
 
94
- # Check required keys
95
- for key in $REQUIRED_KEYS; do
77
+ for key in id epic bcps commit_range source generated_at generator; do
96
78
  local val
97
79
  val="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null)"
98
- if [ -z "$val" ] || [ "$val" = "null" ] || [ "$val" = "None" ]; then
99
- printf "${RED}FAIL${NC} %s: missing required key '%s'\n" "$bundle_name" "$key"
100
- EXIT_CODE=1
101
- return
80
+ if val_nullish "$val"; then
81
+ printf "${RED}FAIL${NC} %s: missing required key '%s'\n" "$name" "$key"
82
+ errs=$((errs + 1))
102
83
  fi
103
84
  done
85
+ [ "$errs" -gt 0 ] && { EXIT_CODE=1; return; }
104
86
 
105
- # Check source enum
87
+ # Source enum
106
88
  local source
107
- source="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['source'])" 2>/dev/null)"
108
- if ! echo "$VALID_SOURCES" | grep -qw "$source"; then
109
- printf "${RED}FAIL${NC} %s: invalid source '%s' — must be one of: %s\n" "$bundle_name" "$source" "$VALID_SOURCES"
110
- EXIT_CODE=1
111
- return
89
+ source="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('source',''))" 2>/dev/null)"
90
+ if ! echo "measured estimated backfilled" | grep -qw "$source"; then
91
+ printf "${RED}FAIL${NC} %s: invalid source '%s' — must be measured|estimated|backfilled\n" "$name" "$source"
92
+ EXIT_CODE=1; return
112
93
  fi
113
94
 
114
- # Check generator
95
+ # Generator
115
96
  local generator
116
- generator="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['generator'])" 2>/dev/null)"
97
+ generator="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('generator',''))" 2>/dev/null)"
117
98
  if [ "$generator" != "scripts/record-cycle-time.sh" ]; then
118
- printf "${RED}FAIL${NC} %s: wrong generator '%s' — expected scripts/record-cycle-time.sh\n" "$bundle_name" "$generator"
119
- EXIT_CODE=1
120
- return
99
+ printf "${RED}FAIL${NC} %s: wrong generator '%s' — expected scripts/record-cycle-time.sh\n" "$name" "$generator"
100
+ EXIT_CODE=1; return
121
101
  fi
122
102
 
123
- # Check commit_range resolves
103
+ # commit_range resolves
124
104
  local commit_range
125
- commit_range="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['commit_range'])" 2>/dev/null)"
105
+ commit_range="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('commit_range',''))" 2>/dev/null)"
126
106
  if ! git -C "$ROOT" log --oneline -1 "$commit_range" >/dev/null 2>&1; then
127
- printf "${RED}FAIL${NC} %s: commit_range '%s' does not resolve\n" "$bundle_name" "$commit_range"
128
- EXIT_CODE=1
129
- return
107
+ printf "${RED}FAIL${NC} %s: commit_range '%s' does not resolve\n" "$name" "$commit_range"
108
+ EXIT_CODE=1; return
130
109
  fi
131
110
 
132
- # Check aggregation tags exist (not empty, but don't validate correctness)
133
- local effort_hours
134
- effort_hours="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); e=d.get('effort',{}); print(e.get('effort_hours',''))" 2>/dev/null)"
135
- if [ -z "$effort_hours" ] || [ "$effort_hours" = "null" ]; then
136
- printf "${RED}FAIL${NC} %s: effort.effort_hours missing or null\n" "$bundle_name"
137
- EXIT_CODE=1
138
- return
111
+ # effort.effort_hours present and non-negative
112
+ local eff
113
+ eff="$(echo "$fm" | python3 -c "
114
+ import json,sys
115
+ d=json.load(sys.stdin)
116
+ e=d.get('effort',{}).get('effort_hours')
117
+ if e is None: sys.exit(1)
118
+ if e < 0: sys.exit(2)
119
+ print(e)
120
+ " 2>/dev/null)" || {
121
+ local rc=$?
122
+ if [ "$rc" -eq 1 ]; then
123
+ printf "${RED}FAIL${NC} %s: effort.effort_hours missing or null\n" "$name"
124
+ else
125
+ printf "${RED}FAIL${NC} %s: effort_hours out of bounds\n" "$name"
126
+ fi
127
+ EXIT_CODE=1; return
128
+ }
129
+
130
+ printf "${GREEN}PASS${NC} %s (source=%s, generator=%s, commit_range resolves)\n" "$name" "$source" "$generator"
131
+ }
132
+
133
+ validate_spec_migration() {
134
+ local file="$1" fm="$2" name="$3"
135
+ local errs=0
136
+
137
+ for key in id title since_version order actions_needed; do
138
+ local val
139
+ val="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null)"
140
+ if val_nullish_or_empty "$val"; then
141
+ printf "${RED}FAIL${NC} %s: missing required key '%s'\n" "$name" "$key"
142
+ errs=$((errs + 1))
143
+ fi
144
+ done
145
+ [ "$errs" -gt 0 ] && { EXIT_CODE=1; return; }
146
+
147
+ # fingerprint.any has at least one check
148
+ local fp_count
149
+ fp_count="$(echo "$fm" | python3 -c "
150
+ import json,sys
151
+ d=json.load(sys.stdin)
152
+ fp=d.get('fingerprint',{}).get('any',[])
153
+ print(len(fp))
154
+ " 2>/dev/null)"
155
+ if [ "${fp_count:-0}" -eq 0 ]; then
156
+ printf "${RED}FAIL${NC} %s: fingerprint.any is empty — no detection criteria\n" "$name"
157
+ EXIT_CODE=1; return
139
158
  fi
140
159
 
141
- # Bounds sanity (effort can't be negative)
142
- python3 -c "
143
- import json, sys
144
- d = json.load(sys.stdin)
145
- eff = d.get('effort', {}).get('effort_hours')
146
- if eff is not None and eff < 0:
147
- sys.exit(1)
148
- " <<< "$fm" 2>/dev/null || {
149
- printf "${RED}FAIL${NC} %s: effort_hours out of bounds\n" "$bundle_name"
160
+ printf "${GREEN}PASS${NC} %s (actions=%s, fingerprint has %s check(s))\n" \
161
+ "$name" \
162
+ "$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('actions_needed',[])))" 2>/dev/null)" \
163
+ "$fp_count"
164
+ }
165
+
166
+ validate_migration_registry() {
167
+ local file="$1" fm="$2" name="$3"
168
+
169
+ local count
170
+ count="$(echo "$fm" | python3 -c "
171
+ import json,sys
172
+ d=json.load(sys.stdin)
173
+ migrations=d.get('migrations',[])
174
+ print(len(migrations))
175
+ " 2>/dev/null)"
176
+ if [ "${count:-0}" -eq 0 ]; then
177
+ printf "${RED}FAIL${NC} %s: migrations list is empty\n" "$name"
178
+ EXIT_CODE=1; return
179
+ fi
180
+
181
+ # Check each migration has id + file + status
182
+ local bad
183
+ bad="$(echo "$fm" | python3 -c "
184
+ import json,sys
185
+ d=json.load(sys.stdin)
186
+ for m in d.get('migrations',[]):
187
+ if not all(k in m for k in ('id','file','status')):
188
+ print(m.get('id','?')); sys.exit(1)
189
+ " 2>/dev/null)" || {
190
+ printf "${RED}FAIL${NC} %s: migration '%s' missing id/file/status\n" "$name" "${bad:-?}"
191
+ EXIT_CODE=1; return
192
+ }
193
+
194
+ printf "${GREEN}PASS${NC} %s (%s migrations indexed)\n" "$name" "$count"
195
+ }
196
+
197
+ # ---- Main validation dispatcher ----------------------------------------
198
+ validate_bundle() {
199
+ local file="$1"
200
+ local name; name="$(basename "$file")"
201
+
202
+ local fm
203
+ fm="$(parse_frontmatter "$file")" || {
204
+ printf "${RED}FAIL${NC} %s: cannot parse YAML frontmatter\n" "$name"
150
205
  EXIT_CODE=1
151
206
  return
152
207
  }
153
208
 
154
- printf "${GREEN}PASS${NC} %s (source=%s, generator=%s, commit_range resolves)\n" "$bundle_name" "$source" "$generator"
209
+ local kind
210
+ kind="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('okf_kind',''))" 2>/dev/null)"
211
+
212
+ case "$kind" in
213
+ story-metrics) validate_story_metrics "$file" "$fm" "$name" ;;
214
+ spec-migration) validate_spec_migration "$file" "$fm" "$name" ;;
215
+ migration-registry) validate_migration_registry "$file" "$fm" "$name" ;;
216
+ "")
217
+ printf "${YELLOW}SKIP${NC} %s: no okf_kind — not an OKF bundle\n" "$name"
218
+ ;;
219
+ *)
220
+ printf "${YELLOW}SKIP${NC} %s: unknown okf_kind '%s'\n" "$name" "$kind"
221
+ ;;
222
+ esac
223
+ }
224
+
225
+ # ---- Scan directory ----------------------------------------------------
226
+ scan_dir() {
227
+ local dir="$1"
228
+ if [ ! -d "$dir" ]; then
229
+ printf "${RED}FAIL${NC} directory not found: %s\n" "$dir"
230
+ EXIT_CODE=1
231
+ return
232
+ fi
233
+ local count=0
234
+ while IFS= read -r -d '' f; do
235
+ if head -1 "$f" 2>/dev/null | grep -q '^---$'; then
236
+ validate_bundle "$f"
237
+ count=$((count + 1))
238
+ fi
239
+ done < <(find "$dir" -maxdepth 1 -name '*.md' -print0 2>/dev/null || true)
240
+ if [ "$count" -eq 0 ]; then
241
+ printf "${YELLOW}SKIP${NC} no OKF bundles found in %s\n" "$dir"
242
+ fi
155
243
  }
156
244
 
157
245
  # ---- main ----------------------------------------------------------------
158
- DIR=""
246
+ DIRS=()
159
247
  BUNDLE=""
160
248
 
161
- [ $# -ge 1 ] || set -- "--dir" "$METRICS_DIR" # default: validate specs/metrics/
249
+ if [ $# -eq 0 ]; then
250
+ DIRS=("$ROOT/specs/metrics" "$ROOT/specs/migrations")
251
+ fi
162
252
 
163
253
  while [ $# -gt 0 ]; do
164
254
  case "$1" in
165
- --dir) DIR="$2"; shift 2 ;;
166
- --bundle) BUNDLE="$2"; shift 2 ;;
167
- --help|-h) usage ;;
168
- *) echo "validate-okf: unknown flag: $1" >&2; usage ;;
255
+ --dir) DIRS+=("$2"); shift 2 ;;
256
+ --bundle) BUNDLE="$2"; shift 2 ;;
257
+ --help|-h) usage_okf ;;
258
+ *)
259
+ echo "validate-okf: unknown flag: $1" >&2
260
+ echo "Run 'scripts/validate-okf.sh --help' for help." >&2
261
+ exit 2
262
+ ;;
169
263
  esac
170
264
  done
171
265
 
172
266
  echo "validate-okf: scanning for OKF bundles..."
173
267
 
174
268
  if [ -n "$BUNDLE" ]; then
175
- validate_bundle "$BUNDLE"
176
- elif [ -n "$DIR" ]; then
177
- if [ ! -d "$DIR" ]; then
178
- printf "${YELLOW}SKIP${NC} directory not found: %s\n" "$DIR"
179
- exit 0
180
- fi
181
- count=0
182
- while IFS= read -r -d '' f; do
183
- # Only process .md files with YAML frontmatter
184
- if head -1 "$f" 2>/dev/null | grep -q '^---$'; then
185
- validate_bundle "$f"
186
- count=$((count + 1))
187
- fi
188
- done < <(find "$DIR" -maxdepth 1 -name '*.md' -print0 2>/dev/null || true)
189
- if [ "$count" -eq 0 ]; then
190
- printf "${YELLOW}SKIP${NC} no OKF bundles found in %s\n" "$DIR"
191
- else
192
- echo "validate-okf: scanned $count bundle(s)"
269
+ if [ ! -f "$BUNDLE" ]; then
270
+ printf "${RED}FAIL${NC} bundle not found: %s\n" "$BUNDLE"
271
+ exit 1
193
272
  fi
273
+ validate_bundle "$BUNDLE"
274
+ elif [ ${#DIRS[@]} -gt 0 ]; then
275
+ for d in "${DIRS[@]}"; do
276
+ scan_dir "$d"
277
+ done
194
278
  else
195
- echo "validate-okf: nothing to validate" >&2
279
+ echo "validate-okf: nothing to validate (no --dir or --bundle)" >&2
280
+ exit 1
196
281
  fi
197
282
 
198
283
  exit "$EXIT_CODE"
@@ -18,7 +18,7 @@ BRANCH=$(git branch --show-current 2>/dev/null || echo "main")
18
18
  COMMIT=""
19
19
  OUTPUT_JSON=false
20
20
 
21
- usage() {
21
+ usage_ci() {
22
22
  sed -n '2,12p' "$0" | sed 's/^# //'
23
23
  }
24
24
 
@@ -29,8 +29,8 @@ while [[ $# -gt 0 ]]; do
29
29
  --branch) BRANCH="$2"; shift 2 ;;
30
30
  --commit) COMMIT="$2"; shift 2 ;;
31
31
  --json) OUTPUT_JSON=true; shift ;;
32
- --help) usage; exit 0 ;;
33
- *) echo "Unknown flag: $1"; usage; exit 2 ;;
32
+ --help) usage_ci; exit 0 ;;
33
+ *) echo "Unknown flag: $1"; usage_ci; exit 2 ;;
34
34
  esac
35
35
  done
36
36