bigpowers 2.73.6 → 2.73.7

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,7 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  # story: e44s03 e44s04
3
3
  # migrate-version.sh — one-shot ordered migration engine with triple safety net.
4
- # Safety: (1) backup, (2) dry-run diff, (3) auto-commit.
5
4
  set -euo pipefail
6
5
  source "$(dirname "${BASH_SOURCE[0]}")/lib/python-env.sh"
7
6
 
@@ -23,705 +22,34 @@ FAILED=()
23
22
  SKIPPED=()
24
23
  STALE_FILES=()
25
24
  UNCERTAINTY_COUNT=0
26
-
27
- # ── usage ────────────────────────────────────────────────────────────────
28
- usage() {
29
- cat <<'EOF'
30
- Usage: scripts/migrate-version.sh [flags]
31
-
32
- Flags:
33
- --help Show this message
34
- --dry-run Compose migration plan and show diff, but make no changes
35
- --force Skip active-work block detection
36
- --no-commit Skip auto-commit (still applies transforms and stamps)
37
- --verbose Show detailed progress for each transform step
38
- EOF
39
- exit 0
40
- }
41
-
42
- # ── helpers ───────────────────────────────────────────────────────────────
43
- installed_version() {
44
- node -e "console.log(require('$BIGPOWERS_ROOT/package.json').version)" 2>/dev/null || echo "unknown"
45
- }
46
-
47
- yaml_get() {
48
- local file="$1" key="$2"
49
- $PYTHON -c "
50
- import yaml, sys
51
- try:
52
- d = yaml.safe_load(open('$file'))
53
- val = d
54
- for k in '$key'.split('.'):
55
- if not isinstance(val, dict): sys.exit(1)
56
- val = val.get(k)
57
- if val is None: sys.exit(1)
58
- print(val)
59
- except: sys.exit(1)
60
- " 2>/dev/null
61
- }
62
-
63
- log_info() { echo " [INFO] $*"; }
64
- log_warn() { echo " [WARN] $*"; }
65
- log_done() { echo " [OK] $*"; }
66
- log_fail() { echo " [FAIL] $*"; }
67
- log_skip() { echo " [SKIP] $*"; }
68
-
69
- # ── argument parsing ──────────────────────────────────────────────────────
70
25
  PROJECT_DIR=""
26
+ PREV_ARG=""
71
27
 
72
28
  for arg in "$@"; do
73
29
  case "$arg" in
74
- --help|-h) usage ;;
30
+ --help|-h) source "$SCRIPT_DIR/lib/migrate-version-common.sh"; migrate_usage ;;
75
31
  --dry-run) DRY_RUN=true ;;
76
32
  --force) FORCE=true ;;
77
33
  --no-commit) SKIP_COMMIT=true ;;
78
34
  --verbose) VERBOSE=true ;;
79
- --project)
80
- # Next argument is the project dir
81
- ;;
35
+ --project) ;;
82
36
  *)
83
37
  if [ "${PREV_ARG:-}" = "--project" ]; then
84
38
  PROJECT_DIR="$arg"
85
39
  else
86
- echo "migrate-version: unknown flag: $arg" >&2; exit 4
40
+ echo "migrate-version: unknown flag: $arg" >&2
41
+ exit 4
87
42
  fi
88
43
  ;;
89
44
  esac
90
45
  PREV_ARG="$arg"
91
46
  done
92
47
 
93
- # If --project given, override paths to target project; migrations remain in bigpowers
94
48
  if [ -n "$PROJECT_DIR" ]; then
95
49
  REPO_ROOT="$PROJECT_DIR"
96
50
  SPECS_DIR="$REPO_ROOT/specs"
97
51
  BACKUP_DIR="$SPECS_DIR/.pre-upgrade-backup"
98
52
  fi
99
53
 
100
- # ── pre-flight: active-work block ─────────────────────────────────────────
101
- if [ "$FORCE" != "true" ]; then
102
- branch="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null)" || true
103
- if [ -n "${branch:-}" ] && [ "$branch" != "main" ] && [ "$branch" != "master" ]; then
104
- echo "migrate-version: BLOCKED — on non-main branch '$branch'. Use --force to override." >&2
105
- exit 3
106
- fi
107
- if ! git -C "$REPO_ROOT" diff --quiet -- specs/ 2>/dev/null || \
108
- ! git -C "$REPO_ROOT" diff --cached --quiet -- specs/ 2>/dev/null; then
109
- echo "migrate-version: BLOCKED — uncommitted changes in specs/. Commit or stash first." >&2
110
- exit 3
111
- fi
112
- if [ -f "$SPECS_DIR/state.yaml" ]; then
113
- af="$(yaml_get "$SPECS_DIR/state.yaml" "active_flow" 2>/dev/null)" || true
114
- if [ -n "${af:-}" ] && [ "$af" != "null" ]; then
115
- echo "migrate-version: BLOCKED — active_flow '$af' is set (mid-epic work). Use --force." >&2
116
- exit 3
117
- fi
118
- fi
119
- fi
120
-
121
- # ── verify pre-conditions ─────────────────────────────────────────────────
122
- if [ ! -d "$SPECS_DIR" ]; then
123
- echo "migrate-version: no specs/ directory found" >&2
124
- exit 2
125
- fi
126
- if [ ! -f "$REGISTRY" ]; then
127
- echo "migrate-version: migration registry not found at $REGISTRY" >&2
128
- exit 4
129
- fi
130
-
131
- INSTALLED="$(installed_version)"
132
-
133
- # Detect version
134
- DETECTED=""
135
- if [ -f "$SPECS_DIR/state.yaml" ]; then
136
- stamped="$(yaml_get "$SPECS_DIR/state.yaml" "bigpowers_version" 2>/dev/null)" || true
137
- if [ -n "${stamped:-}" ] && [ "$stamped" != "null" ]; then
138
- DETECTED="$stamped"
139
- fi
140
- fi
141
- if [ -z "$DETECTED" ]; then
142
- if [ -f "$SPECS_DIR/STATE.md" ] && [ ! -f "$SPECS_DIR/state.yaml" ]; then
143
- DETECTED="1.0.0"
144
- elif [ -f "$SPECS_DIR/state.yaml" ]; then
145
- if yaml_get "$SPECS_DIR/state.yaml" "epic_cycle" >/dev/null 2>&1; then
146
- DETECTED="2.20.0"
147
- else
148
- DETECTED="2.0.0"
149
- fi
150
- else
151
- echo "migrate-version: cannot determine spec version" >&2
152
- exit 4
153
- fi
154
- fi
155
-
156
- echo "migrate-version: detected=$DETECTED installed=$INSTALLED"
157
-
158
- if [ "$DETECTED" = "$INSTALLED" ]; then
159
- echo "migrate-version: no version gap — specs are current"
160
- exit 0
161
- fi
162
-
163
- # ── build migration plan ──────────────────────────────────────────────────
164
- echo "migrate-version: building migration plan from $REGISTRY …"
165
-
166
- MIGRATION_PLAN="$($PYTHON -c "
167
- import yaml, json, sys
168
-
169
- with open('$REGISTRY') as f:
170
- parts = f.read().split('---')
171
- fm = yaml.safe_load(parts[1])
172
-
173
- detected = '$DETECTED'
174
- installed = '$INSTALLED'
175
-
176
- plan = []
177
- for m in sorted(fm.get('migrations', []), key=lambda x: x.get('order', 99)):
178
- sid = m['id']
179
- since = m.get('since_version', '0')
180
- order = m.get('order', 99)
181
- deps = m.get('depends_on', [])
182
- plan.append({'id': sid, 'file': m.get('file', sid + '.okf.md'),
183
- 'since_version': since, 'order': order,
184
- 'depends_on': deps, 'title': m.get('title', sid)})
185
-
186
- applicable = [m for m in plan if m['since_version'] > detected and m['since_version'] <= installed]
187
- print(json.dumps(applicable))
188
- " 2>/dev/null)"
189
-
190
- MIGRATION_COUNT="$(echo "$MIGRATION_PLAN" | $PYTHON -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null)"
191
-
192
- if [ "${MIGRATION_COUNT:-0}" -eq 0 ]; then
193
- echo "migrate-version: no applicable migrations for $DETECTED → $INSTALLED — stamping version only"
194
- fi
195
-
196
- echo "migrate-version: $MIGRATION_COUNT migration(s) to apply"
197
-
198
- if [ "$VERBOSE" = "true" ]; then
199
- echo "$MIGRATION_PLAN" | $PYTHON -c "
200
- import json,sys
201
- for m in json.load(sys.stdin):
202
- print(f\" {m['order']}. {m['id']}: {m['title']} (since {m['since_version']})\")
203
- "
204
- fi
205
-
206
- # ── Safety Layer 1: backup ────────────────────────────────────────────────
207
- if [ "$DRY_RUN" != "true" ]; then
208
- log_info "Creating backup: $BACKUP_DIR"
209
- rm -rf "$BACKUP_DIR"
210
- mkdir -p "$BACKUP_DIR"
211
- cp -r "$SPECS_DIR"/* "$BACKUP_DIR"/ 2>/dev/null || true
212
- rm -rf "$BACKUP_DIR/.pre-upgrade-backup" 2>/dev/null || true
213
- fi
214
-
215
- # ── Safety Layer 2: dry-run ───────────────────────────────────────────────
216
- if [ "$DRY_RUN" = "true" ]; then
217
- echo ""
218
- echo "=========================================="
219
- echo " DRY RUN — no changes will be made"
220
- echo "=========================================="
221
- echo ""
222
- echo "Detected version: $DETECTED"
223
- echo "Target version: $INSTALLED"
224
- echo "Migrations:"
225
- echo "$MIGRATION_PLAN" | $PYTHON -c "
226
- import json,sys
227
- for m in json.load(sys.stdin):
228
- deps = m.get('depends_on', [])
229
- dep_str = ' (depends on: ' + ', '.join(deps) + ')' if deps else ''
230
- print(f\" [{m['order']}] {m['id']}: {m['title']}{dep_str}\")
231
- "
232
-
233
- echo ""
234
- echo "Transforms preview:"
235
- echo "$MIGRATION_PLAN" | $PYTHON -c "
236
- import json, sys, os
237
- for m in json.load(sys.stdin):
238
- bundle_path = os.path.join('$MIGRATIONS_DIR', m['file'])
239
- if not os.path.exists(bundle_path):
240
- print(f\" WARN {m['id']}: bundle file not found ({m['file']})\")
241
- continue
242
- with open(bundle_path) as f:
243
- parts = f.read().split('---')
244
- fm = __import__('yaml').safe_load(parts[1])
245
- transforms = fm.get('transforms', [])
246
- print(f\"\n -- {m['id']}: {m['title']} --\")
247
- for t in transforms:
248
- action = t.get('action', '?')
249
- note = t.get('note', '')
250
- note_str = f' ({note})' if note else ''
251
- if action == 'convert_md_to_yaml':
252
- src = t.get('source', '?')
253
- tgt = t.get('target', '?')
254
- unc = t.get('uncertainty', [])
255
- print(f\" {action}: {src} -> {tgt}{note_str}\")
256
- for u in unc:
257
- print(f\" GUESS: {u.get('field','?')} - {u.get('reason','?')}\")
258
- elif action in ('rename_file', 'move_file'):
259
- print(f\" {action}: {t.get('source','?')} -> {t.get('target','?')}{note_str}\")
260
- elif action == 'delete_file':
261
- print(f\" {action}: {t.get('path','?')}{note_str}\")
262
- elif action in ('set_yaml_key', 'rename_yaml_key', 'delete_yaml_key'):
263
- print(f\" {action}: {t.get('file','?')} {t.get('key','?') or t.get('old_key','?')} = {t.get('value','?')}{note_str}\")
264
- elif action == 'create_file_from_template':
265
- print(f\" {action}: {t.get('source','?')} -> {t.get('target','?')}{note_str}\")
266
- else:
267
- print(f\" {action}{note_str}\")
268
- for a in fm.get('actions_needed', []):
269
- v = a.get('verify', '')
270
- if v: print(f\" verify: {v}\")
271
- " 2>/dev/null
272
-
273
- echo ""
274
- echo "Run without --dry-run to apply these migrations."
275
- exit 0
276
- fi
277
-
278
- # ── Apply phase ───────────────────────────────────────────────────────────
279
- echo ""
280
- echo "Applying migrations …"
281
- SUCCEEDED=()
282
- FAILED=()
283
- SKIPPED=()
284
- # Default empty values for subshell-safe access
285
- SUCCEEDED_COUNT=0
286
- FAILED_COUNT=0
287
- SKIPPED_COUNT=0
288
- UNCERTAINTY_COUNT=0
289
-
290
- apply_transform() {
291
- local mig_id="$1" action="$2" tjson="$3"
292
-
293
- case "$action" in
294
- convert_md_to_yaml)
295
- local src tgt hm unc_count arch
296
- src="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('source',''))")"
297
- tgt="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('target',''))")"
298
- arch="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('archive_source','false'))" 2>/dev/null)" || arch="false"
299
- if [ ! -f "$REPO_ROOT/$src" ]; then
300
- log_skip "$action: source '$src' not found"
301
- SKIPPED+=("$action:$src"); return
302
- fi
303
- hm="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.dumps(json.load(sys.stdin).get('heuristic_map',{})))")"
304
- $PYTHON -c "
305
- import json, re, yaml, sys, os
306
- src = '$REPO_ROOT/$src'
307
- tgt = '$REPO_ROOT/$tgt'
308
- heuristic_map = json.loads('''$hm''')
309
- content = open(src).read()
310
- result = {}
311
- for md_key, yaml_key in heuristic_map.items():
312
- pat = re.compile(r'^' + re.escape(md_key.rstrip(':')) + r'\\s*[:=]\\s*(.+)$', re.MULTILINE | re.IGNORECASE)
313
- m = pat.search(content)
314
- if m:
315
- val = m.group(1).strip()
316
- try: val = yaml.safe_load(val) or val
317
- except: pass
318
- parts = yaml_key.split('.')
319
- cur = result
320
- for p in parts[:-1]:
321
- if p not in cur or not isinstance(cur[p], dict):
322
- cur[p] = {}
323
- cur = cur[p]
324
- cur[parts[-1]] = val
325
- os.makedirs(os.path.dirname(tgt), exist_ok=True)
326
- with open(tgt, 'w') as f:
327
- yaml.safe_dump(result, f, sort_keys=False, allow_unicode=True, width=100)
328
- "
329
- unc_count="$(echo "$tjson" | $PYTHON -c "import json,sys; print(len(json.load(sys.stdin).get('uncertainty',[])))")"
330
- UNCERTAINTY_COUNT=$((UNCERTAINTY_COUNT + unc_count))
331
- # Archive source file if requested
332
- if [ "$arch" = "true" ] || [ "$arch" = "True" ]; then
333
- local archive_dir="$REPO_ROOT/specs/archive"
334
- mkdir -p "$archive_dir"
335
- mv "$REPO_ROOT/$src" "$archive_dir/"
336
- log_done "$action: $src -> $tgt (source archived to specs/archive/)"
337
- else
338
- log_done "$action: $src -> $tgt"
339
- fi
340
- ;;
341
-
342
- rename_file)
343
- local src tgt
344
- src="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('source',''))")"
345
- tgt="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('target',''))")"
346
- if [ ! -e "$REPO_ROOT/$src" ]; then
347
- log_skip "$action: source '$src' not found — idempotent"
348
- SKIPPED+=("$action:$src"); return
349
- fi
350
- if [ -e "$REPO_ROOT/$tgt" ]; then
351
- log_skip "$action: target '$tgt' already exists — idempotent"
352
- SKIPPED+=("$action:$src"); return
353
- fi
354
- mkdir -p "$(dirname "$REPO_ROOT/$tgt")"
355
- mv "$REPO_ROOT/$src" "$REPO_ROOT/$tgt"
356
- log_done "$action: $src -> $tgt"
357
- ;;
358
-
359
- move_file)
360
- local src tgt
361
- src="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('source',''))")"
362
- tgt="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('target',''))")"
363
- if [ ! -e "$REPO_ROOT/$src" ]; then
364
- log_skip "$action: source '$src' not found"
365
- SKIPPED+=("$action:$src"); return
366
- fi
367
- if [ -e "$REPO_ROOT/$tgt" ]; then
368
- log_skip "$action: target '$tgt' already exists — idempotent"
369
- SKIPPED+=("$action:$src"); return
370
- fi
371
- mkdir -p "$(dirname "$REPO_ROOT/$tgt")"
372
- mv "$REPO_ROOT/$src" "$REPO_ROOT/$tgt"
373
- log_done "$action: $src -> $tgt"
374
- ;;
375
-
376
- delete_file)
377
- local path
378
- path="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('path',''))")"
379
- if [ ! -e "$REPO_ROOT/$path" ]; then
380
- log_skip "$action: '$path' not found — idempotent"
381
- SKIPPED+=("$action:$path"); return
382
- fi
383
- rm -f "$REPO_ROOT/$path"
384
- log_done "$action: $path"
385
- ;;
386
-
387
- set_yaml_key)
388
- local file key val if_missing
389
- file="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('file',''))")"
390
- key="$(echo "$tjson" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('path') or d.get('key',''))")"
391
- val="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('value',''))")"
392
- if_missing="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('if_missing','false'))" 2>/dev/null)" || if_missing="false"
393
- if [ "$if_missing" = "true" ] || [ "$if_missing" = "True" ]; then
394
- if yaml_get "$REPO_ROOT/$file" "$key" >/dev/null 2>&1; then
395
- log_skip "$action: $file $key already set (if_missing=true) — idempotent"
396
- SKIPPED+=("$action:$key")
397
- return
398
- fi
399
- fi
400
- $PYTHON "$YAML_TOOLS" set "$REPO_ROOT/$file" "$key" "$val"
401
- log_done "$action: $file $key = $val"
402
- ;;
403
-
404
- rename_yaml_key)
405
- local file old_key new_key old_val
406
- file="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('file',''))")"
407
- old_key="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('old_key',''))")"
408
- new_key="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('new_key',''))")"
409
- old_val="$(yaml_get "$REPO_ROOT/$file" "$old_key" 2>/dev/null)" || true
410
- if [ -z "${old_val:-}" ]; then
411
- log_skip "$action: key '$old_key' not found in $file"
412
- SKIPPED+=("$action:$old_key"); return
413
- fi
414
- $PYTHON "$YAML_TOOLS" set "$REPO_ROOT/$file" "$new_key" "$old_val"
415
- $PYTHON -c "
416
- import yaml
417
- data = yaml.safe_load(open('$REPO_ROOT/$file'))
418
- parts = '$old_key'.split('.')
419
- cur = data
420
- for p in parts[:-1]:
421
- if not isinstance(cur, dict): break
422
- cur = cur.get(p, {})
423
- if isinstance(cur, dict) and parts[-1] in cur:
424
- del cur[parts[-1]]
425
- with open('$REPO_ROOT/$file', 'w') as f:
426
- yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True, width=100)
427
- " 2>/dev/null
428
- log_done "$action: $file $old_key -> $new_key = $old_val"
429
- ;;
430
-
431
- delete_yaml_key)
432
- local file key
433
- file="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('file',''))")"
434
- key="$(echo "$tjson" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('path') or d.get('key',''))")"
435
- if ! yaml_get "$REPO_ROOT/$file" "$key" >/dev/null 2>&1; then
436
- log_skip "$action: key '$key' not found in $file — idempotent"
437
- SKIPPED+=("$action:$key"); return
438
- fi
439
- $PYTHON -c "
440
- import yaml
441
- data = yaml.safe_load(open('$REPO_ROOT/$file'))
442
- parts = '$key'.split('.')
443
- cur = data
444
- for p in parts[:-1]:
445
- if not isinstance(cur, dict): break
446
- cur = cur.get(p, {})
447
- if isinstance(cur, dict) and parts[-1] in cur:
448
- del cur[parts[-1]]
449
- with open('$REPO_ROOT/$file', 'w') as f:
450
- yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True, width=100)
451
- " 2>/dev/null
452
- log_done "$action: $file $key"
453
- ;;
454
-
455
- create_file_from_template)
456
- local src tgt subs inline_tmpl
457
- src="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('source',''))")"
458
- tgt="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('target',''))")"
459
- subs="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.dumps(json.load(sys.stdin).get('substitutions',{})))")"
460
- inline_tmpl="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('template',''))")"
461
- if [ -e "$REPO_ROOT/$tgt" ]; then
462
- log_skip "$action: target '$tgt' already exists — idempotent"
463
- SKIPPED+=("$action:$tgt"); return
464
- fi
465
- mkdir -p "$(dirname "$REPO_ROOT/$tgt")"
466
- # Inline template (content embedded in OKF bundle)
467
- if [ -n "$inline_tmpl" ]; then
468
- echo "$inline_tmpl" > "$REPO_ROOT/$tgt"
469
- if [ "$subs" != "{}" ]; then
470
- $PYTHON -c "
471
- import json
472
- subs = json.loads('''$subs''')
473
- content = open('$REPO_ROOT/$tgt').read()
474
- for k, v in subs.items():
475
- content = content.replace(k, v)
476
- with open('$REPO_ROOT/$tgt', 'w') as f:
477
- f.write(content)
478
- " 2>/dev/null
479
- fi
480
- log_done "$action: inline template -> $tgt"
481
- return
482
- fi
483
- # File-based template resolution
484
- template_path=""
485
- if [ -n "$src" ]; then
486
- # Try source as relative path first, then as template name
487
- if [ -f "$REPO_ROOT/$src" ]; then
488
- template_path="$REPO_ROOT/$src"
489
- elif [ -f "$REPO_ROOT/specs/templates/${src##*/}" ]; then
490
- template_path="$REPO_ROOT/specs/templates/${src##*/}"
491
- fi
492
- fi
493
- if [ -z "$template_path" ] || [ ! -f "$template_path" ]; then
494
- log_skip "$action: template not found (src='$src')"
495
- SKIPPED+=("$action:$src"); return
496
- fi
497
- cp "$template_path" "$REPO_ROOT/$tgt"
498
- $PYTHON -c "
499
- import json
500
- subs = json.loads('''$subs''')
501
- content = open('$REPO_ROOT/$tgt').read()
502
- for k, v in subs.items():
503
- content = content.replace(k, v)
504
- with open('$REPO_ROOT/$tgt', 'w') as f:
505
- f.write(content)
506
- " 2>/dev/null
507
- log_done "$action: $src -> $tgt"
508
- ;;
509
-
510
- *)
511
- log_skip "$action: unknown transform type"
512
- SKIPPED+=("$action")
513
- ;;
514
- esac
515
- }
516
-
517
- # Execute each migration in order
518
- MIG_LIST="$(echo "$MIGRATION_PLAN" | $PYTHON -c "import json,sys; [print(json.dumps(m)) for m in json.load(sys.stdin)]")"
519
- while IFS= read -r mig_json; do
520
- [ -z "$mig_json" ] && continue
521
- MIG_ID="$(echo "$mig_json" | $PYTHON -c "import json,sys; print(json.load(sys.stdin)['id'])")"
522
- MIG_FILE="$(echo "$mig_json" | $PYTHON -c "import json,sys; print(json.load(sys.stdin)['file'])")"
523
- MIG_TITLE="$(echo "$mig_json" | $PYTHON -c "import json,sys; print(json.load(sys.stdin)['title'])")"
524
-
525
- BUNDLE_PATH="$MIGRATIONS_DIR/$MIG_FILE"
526
- if [ ! -f "$BUNDLE_PATH" ]; then
527
- log_warn "$MIG_ID: bundle file not found — skipping"
528
- SKIPPED+=("$MIG_ID:bundle_missing")
529
- continue
530
- fi
531
-
532
- echo ""
533
- echo "── $MIG_ID: $MIG_TITLE ──"
534
-
535
- TRANSFORMS="$($PYTHON -c "
536
- import yaml, json
537
- with open('$BUNDLE_PATH') as f:
538
- parts = f.read().split('---')
539
- fm = yaml.safe_load(parts[1])
540
- print(json.dumps(fm.get('transforms', []), default=str))
541
- " 2>/dev/null)"
542
-
543
- T_LIST="$(echo "$TRANSFORMS" | $PYTHON -c "import json,sys; [print(json.dumps(t, default=str)) for t in json.load(sys.stdin)]")"
544
- while IFS= read -r tjson; do
545
- [ -z "$tjson" ] && continue
546
- action="$(echo "$tjson" | $PYTHON -c "import json,sys; print(json.load(sys.stdin).get('action',''))")"
547
- apply_transform "$MIG_ID" "$action" "$tjson"
548
- done <<< "$T_LIST"
549
-
550
- # Run verify commands
551
- VERIFY_CMDS="$($PYTHON -c "
552
- import yaml, json
553
- with open('$BUNDLE_PATH') as f:
554
- parts = f.read().split('---')
555
- fm = yaml.safe_load(parts[1])
556
- for a in fm.get('actions_needed', []):
557
- v = a.get('verify', '')
558
- if v: print(v)
559
- " 2>/dev/null)"
560
-
561
- VERIFY_OK=true
562
- while IFS= read -r vcmd; do
563
- [ -z "$vcmd" ] && continue
564
- log_info "verify: $vcmd"
565
- if eval "$vcmd" 2>&1; then
566
- log_done "verify passed"
567
- else
568
- log_fail "verify failed"
569
- VERIFY_OK=false
570
- fi
571
- done <<< "$VERIFY_CMDS"
572
-
573
- if [ "$VERIFY_OK" = "true" ]; then
574
- SUCCEEDED+=("$MIG_ID")
575
- else
576
- FAILED+=("$MIG_ID")
577
- fi
578
- done <<< "$MIG_LIST"
579
-
580
- # ── stamp ─────────────────────────────────────────────────────────────────
581
- if [ -f "$SPECS_DIR/state.yaml" ]; then
582
- log_info "Stamping bigpowers_version=$INSTALLED in state.yaml"
583
- $PYTHON "$YAML_TOOLS" set "$SPECS_DIR/state.yaml" "bigpowers_version" "$INSTALLED"
584
- log_done "stamp applied"
585
- fi
586
-
587
- # ── validate-okf soft gate ────────────────────────────────────────────────
588
- if [ -f "$SCRIPT_DIR/validate-okf.sh" ]; then
589
- log_info "Running validate-okf.sh (soft gate) …"
590
- if bash "$SCRIPT_DIR/validate-okf.sh" --dir "$MIGRATIONS_DIR" 2>&1; then
591
- log_done "validate-okf: PASS"
592
- else
593
- log_warn "validate-okf: some checks failed (soft gate — non-blocking)"
594
- fi
595
- else
596
- log_info "validate-okf.sh not found — skipping soft gate"
597
- fi
598
-
599
- # ── global validate-specs-yaml gate ───────────────────────────────────────
600
- if [ -f "$SCRIPT_DIR/validate-specs-yaml.sh" ]; then
601
- log_info "Running global validate-specs-yaml.sh …"
602
- if bash "$SCRIPT_DIR/validate-specs-yaml.sh" 2>&1; then
603
- log_done "validate-specs-yaml: PASS"
604
- else
605
- log_fail "validate-specs-yaml: FAIL"
606
- FAILED+=("validate-specs-yaml")
607
- fi
608
- fi
609
-
610
- # ── CLAUDE.md / CONVENTIONS.md staleness check ────────────────────────────
611
- STALE_FILES=()
612
- BIGPOWERS_CLAUDE="$SCRIPT_DIR/../CLAUDE.md"
613
- if [ -f "$REPO_ROOT/CLAUDE.md" ] && [ -f "$BIGPOWERS_CLAUDE" ]; then
614
- proj_lines="$(wc -l < "$REPO_ROOT/CLAUDE.md")"
615
- bp_lines="$(wc -l < "$BIGPOWERS_CLAUDE")"
616
- delta=$((bp_lines - proj_lines))
617
- if [ $delta -gt 20 ]; then
618
- STALE_FILES+=("CLAUDE.md")
619
- log_warn "CLAUDE.md: $delta lines behind bigpowers template — consider updating"
620
- fi
621
- fi
622
- if [ -f "$REPO_ROOT/CONVENTIONS.md" ] && [ -f "$SCRIPT_DIR/../CONVENTIONS.md" ]; then
623
- proj_lines="$(wc -l < "$REPO_ROOT/CONVENTIONS.md")"
624
- bp_lines="$(wc -l < "$SCRIPT_DIR/../CONVENTIONS.md")"
625
- delta=$((bp_lines - proj_lines))
626
- if [ $delta -gt 20 ]; then
627
- STALE_FILES+=("CONVENTIONS.md")
628
- log_warn "CONVENTIONS.md: $delta lines behind bigpowers template — consider updating"
629
- fi
630
- fi
631
-
632
- # ── generate migration report ─────────────────────────────────────────────
633
- ISO_DATE="$(date -u +%Y-%m-%dT%H%M%SZ)"
634
- REPORT_FILE="$SPECS_DIR/migration-report-$ISO_DATE.md"
635
-
636
- cat > "$REPORT_FILE" <<REPORT
637
- # Migration Report — $ISO_DATE
638
-
639
- ## Gap Summary
640
-
641
- | Field | Value |
642
- |-------|-------|
643
- | Detected version | $DETECTED |
644
- | Installed version | $INSTALLED |
645
- | Migration count | ${MIGRATION_COUNT:-0} |
646
-
647
- ## Migrations Applied
648
-
649
- REPORT
650
-
651
- for m in "${SUCCEEDED[@]:-}"; do
652
- [ -z "$m" ] && continue
653
- echo "- $m PASS" >> "$REPORT_FILE"
654
- done
655
- FAILED_COUNT="${#FAILED[@]}"
656
- if [ "${FAILED_COUNT}" -gt 0 ]; then
657
- echo "" >> "$REPORT_FILE"
658
- echo "## Migrations Failed" >> "$REPORT_FILE"
659
- for m in "${FAILED[@]}"; do
660
- echo "- $m FAIL" >> "$REPORT_FILE"
661
- done
662
- fi
663
- if [ ${#SKIPPED[@]} -gt 0 ]; then
664
- echo "" >> "$REPORT_FILE"
665
- echo "## Skipped" >> "$REPORT_FILE"
666
- for m in "${SKIPPED[@]}"; do
667
- echo "- $m" >> "$REPORT_FILE"
668
- done
669
- fi
670
- echo "" >> "$REPORT_FILE"
671
- echo "## Uncertainties" >> "$REPORT_FILE"
672
- echo "WARN count: $UNCERTAINTY_COUNT — review specs/ for GUESS markers and adjust if needed." >> "$REPORT_FILE"
673
- echo "" >> "$REPORT_FILE"
674
- echo "## Verification" >> "$REPORT_FILE"
675
- echo "- validate-specs-yaml: ran as final gate" >> "$REPORT_FILE"
676
-
677
- if [ ${#STALE_FILES[@]} -gt 0 ]; then
678
- echo "" >> "$REPORT_FILE"
679
- echo "## Staleness Notice" >> "$REPORT_FILE"
680
- for f in "${STALE_FILES[@]:-}"; do
681
- [ -z "$f" ] && continue
682
- echo "- $f is behind the current bigpowers template" >> "$REPORT_FILE"
683
- done
684
- fi
685
-
686
- echo "" >> "$REPORT_FILE"
687
- echo "## Next Steps" >> "$REPORT_FILE"
688
- echo "1. Review the migration report and verify spec correctness" >> "$REPORT_FILE"
689
- echo "2. Run \`bash scripts/sync-skills.sh\` to regenerate agent artifacts" >> "$REPORT_FILE"
690
- echo "3. Commit changes if not auto-committed" >> "$REPORT_FILE"
691
- echo "" >> "$REPORT_FILE"
692
- echo "## Rollback" >> "$REPORT_FILE"
693
- echo "To rollback, restore from backup:" >> "$REPORT_FILE"
694
- echo "\`\`\`bash" >> "$REPORT_FILE"
695
- echo "rm -rf specs/ && cp -r $BACKUP_DIR/ specs/" >> "$REPORT_FILE"
696
- echo "\`\`\`" >> "$REPORT_FILE"
697
-
698
- log_done "Migration report: $REPORT_FILE"
699
-
700
- # ── terminal summary ──────────────────────────────────────────────────────
701
- echo ""
702
- echo "=========================================="
703
- echo " Migration complete: $DETECTED -> $INSTALLED"
704
- echo "=========================================="
705
- echo " Applied: ${#SUCCEEDED[@]}"
706
- echo " Failed: ${#FAILED[@]}"
707
- echo " Skipped: ${#SKIPPED[@]}"
708
- echo " WARN: $UNCERTAINTY_COUNT"
709
- [ ${#STALE_FILES[@]} -gt 0 ] && echo " Stale: ${STALE_FILES[*]:-}"
710
- echo " Report: $REPORT_FILE"
711
- echo "=========================================="
712
- echo " Remember: run 'bash scripts/sync-skills.sh'"
713
- echo "=========================================="
714
-
715
- # ── Safety Layer 3: auto-commit ───────────────────────────────────────────
716
- if [ "$SKIP_COMMIT" != "true" ] && [ "${MIGRATION_COUNT:-0}" -gt 0 ]; then
717
- log_info "Auto-committing migration …"
718
- git -C "$REPO_ROOT" add specs/ 2>/dev/null || true
719
- COMMIT_MSG="migrate-version: $DETECTED -> $INSTALLED (${MIGRATION_COUNT:-0} migration(s))"
720
- if git -C "$REPO_ROOT" commit -m "$COMMIT_MSG" 2>/dev/null; then
721
- log_done "committed: $COMMIT_MSG"
722
- else
723
- log_info "nothing to commit (already clean)"
724
- fi
725
- fi
726
-
727
- exit 0
54
+ source "$SCRIPT_DIR/lib/migrate-version-run.sh"
55
+ migrate_version_main