bigpowers 2.64.2 → 2.65.0
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/.pi/package.json +1 -1
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/scripts/audit-compliance.sh +34 -0
- package/scripts/generate-adr-wiki.sh +86 -0
- package/scripts/generate-epics-wiki.sh +84 -0
- package/scripts/generate-viz-graph.sh +147 -0
- package/scripts/run-golden-suite.sh +33 -0
- package/scripts/sync-bugs-registry.sh +43 -3
- package/scripts/sync-skills.sh +387 -12
- package/scripts/validate-okf.sh +91 -40
package/.pi/package.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [2.65.0](https://github.com/danielvm-git/bigpowers/compare/v2.64.2...v2.65.0) (2026-07-05)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* OKF Completion — wikis, verification reports, viz graph (e45) ([e04d209](https://github.com/danielvm-git/bigpowers/commit/e04d209c3e70c7e3c9aa2d19606294fde9b4cb23))
|
|
7
|
+
|
|
1
8
|
## [2.64.2](https://github.com/danielvm-git/bigpowers/compare/v2.64.1...v2.64.2) (2026-07-05)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -286,6 +286,40 @@ if [[ $TOTAL_GLOBAL_ALL -gt 0 ]]; then
|
|
|
286
286
|
else
|
|
287
287
|
SCORE=0
|
|
288
288
|
fi
|
|
289
|
+
|
|
290
|
+
# ── OKF verification-report (e45s02) ────────────────────────────────────
|
|
291
|
+
OKF_DIR="specs/verifications/reports"
|
|
292
|
+
mkdir -p "$OKF_DIR"
|
|
293
|
+
OKF_FILE="$OKF_DIR/audit-$(date +%Y-%m-%d).okf.md"
|
|
294
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
295
|
+
GIT_COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
|
296
|
+
GATE=$(if [[ $SCORE -ge 94 ]]; then echo "pass"; else echo "fail"; fi)
|
|
297
|
+
|
|
298
|
+
cat > "$OKF_FILE" << OKF_EOF
|
|
299
|
+
---
|
|
300
|
+
okf_kind: verification-report
|
|
301
|
+
okf_version: "0.1"
|
|
302
|
+
score: ${SCORE}
|
|
303
|
+
gate_status: "${GATE}"
|
|
304
|
+
threshold: 94
|
|
305
|
+
total_pass: ${TOTAL_GLOBAL_PASS}
|
|
306
|
+
total_fail: ${TOTAL_GLOBAL_FAIL}
|
|
307
|
+
total_waived: ${TOTAL_GLOBAL_WAIVED}
|
|
308
|
+
generated_by: scripts/audit-compliance.sh
|
|
309
|
+
generated_at: "${TIMESTAMP}"
|
|
310
|
+
git_commit: "${GIT_COMMIT}"
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
# Compliance Audit — $(date +%Y-%m-%d)
|
|
314
|
+
|
|
315
|
+
**Score:** ${SCORE}% (${TOTAL_GLOBAL_PASS}/${TOTAL_GLOBAL_ALL} passed, ${TOTAL_GLOBAL_WAIVED} waived)
|
|
316
|
+
**Gate:** ${GATE} | **Threshold:** 94%
|
|
317
|
+
|
|
318
|
+
See \`specs/verifications/reports/audit-*.md\` for detailed step-by-step reports.
|
|
319
|
+
OKF_EOF
|
|
320
|
+
|
|
321
|
+
echo "OKF report: $OKF_FILE"
|
|
322
|
+
|
|
289
323
|
if [[ $TOTAL_GLOBAL_WAIVED -gt 0 ]]; then
|
|
290
324
|
echo " SCORE: ${SCORE}% (of ${TOTAL_GLOBAL_ALL} unwaived checks, threshold 94%)"
|
|
291
325
|
else
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e45s01 e45s06
|
|
3
|
+
# generate-adr-wiki.sh — emit OKF concept bundles from specs/adr/*.md
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
7
|
+
ADRS="$ROOT/specs/adr"
|
|
8
|
+
WIKI="$ROOT/specs/adr-wiki"
|
|
9
|
+
mkdir -p "$WIKI"
|
|
10
|
+
|
|
11
|
+
if [ ! -d "$ADRS" ]; then
|
|
12
|
+
echo "generate-adr-wiki: no ADR directory at $ADRS"
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
count=0
|
|
17
|
+
|
|
18
|
+
assign_tier() {
|
|
19
|
+
local adr_id="$1"
|
|
20
|
+
case "$adr_id" in
|
|
21
|
+
ADR-0001|ADR-0002|ADR-0003|0001-*|0002-*|0003-*) echo "core" ;;
|
|
22
|
+
*) echo "extended" ;;
|
|
23
|
+
esac
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for adr_file in "$ADRS"/*.md; do
|
|
27
|
+
[ -f "$adr_file" ] || continue
|
|
28
|
+
adr_name=$(basename "$adr_file" .md)
|
|
29
|
+
|
|
30
|
+
# Parse YAML frontmatter if present
|
|
31
|
+
fm="{}"
|
|
32
|
+
if head -1 "$adr_file" 2>/dev/null | grep -q '^---$'; then
|
|
33
|
+
fm=$(python3 -c "
|
|
34
|
+
import sys, yaml, json
|
|
35
|
+
content = open('$adr_file').read()
|
|
36
|
+
parts = content.split('---')
|
|
37
|
+
if len(parts) >= 3:
|
|
38
|
+
try:
|
|
39
|
+
d = yaml.safe_load(parts[1])
|
|
40
|
+
if d:
|
|
41
|
+
print(json.dumps(d))
|
|
42
|
+
except: pass
|
|
43
|
+
" 2>/dev/null) || fm="{}"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
title=$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('title',''))" 2>/dev/null || echo "")
|
|
47
|
+
status=$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('status','accepted'))" 2>/dev/null || echo "accepted")
|
|
48
|
+
decision=$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('decision',''))" 2>/dev/null || echo "")
|
|
49
|
+
|
|
50
|
+
# Fallback title from first heading
|
|
51
|
+
if [ -z "$title" ]; then
|
|
52
|
+
title=$(grep -m1 '^# ' "$adr_file" 2>/dev/null | sed 's/^# //' || echo "$adr_name")
|
|
53
|
+
fi
|
|
54
|
+
[ -z "$title" ] && title="$adr_name"
|
|
55
|
+
|
|
56
|
+
tier=$(assign_tier "$adr_name")
|
|
57
|
+
|
|
58
|
+
bundle="$WIKI/${adr_name}.okf.md"
|
|
59
|
+
|
|
60
|
+
cat > "$bundle" << OKFEOF
|
|
61
|
+
---
|
|
62
|
+
okf_kind: concept
|
|
63
|
+
okf_version: "0.1"
|
|
64
|
+
id: "${adr_name}"
|
|
65
|
+
title: "${title}"
|
|
66
|
+
category: adr
|
|
67
|
+
tier: ${tier}
|
|
68
|
+
status: ${status}
|
|
69
|
+
decision: "${decision}"
|
|
70
|
+
generator: scripts/generate-adr-wiki.sh
|
|
71
|
+
references:
|
|
72
|
+
- specs/adr/${adr_name}.md
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
# ${title}
|
|
76
|
+
|
|
77
|
+
**ADR:** ${adr_name} | **Status:** ${status} | **Tier:** ${tier}
|
|
78
|
+
|
|
79
|
+
See \`specs/adr/${adr_name}.md\` for the full architectural decision record.
|
|
80
|
+
OKFEOF
|
|
81
|
+
|
|
82
|
+
count=$((count + 1))
|
|
83
|
+
echo " → ${adr_name}: ${title} [${tier}]"
|
|
84
|
+
done
|
|
85
|
+
|
|
86
|
+
echo "generate-adr-wiki: ${count} concept bundles → $WIKI/"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e45s01 e45s06
|
|
3
|
+
# generate-epics-wiki.sh — emit OKF concept bundles from specs/epics/*/epic.yaml
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
7
|
+
WIKI="$ROOT/specs/epics-wiki"
|
|
8
|
+
mkdir -p "$WIKI"
|
|
9
|
+
|
|
10
|
+
# Helper: extract YAML scalar field (graceful missing → empty)
|
|
11
|
+
extract() {
|
|
12
|
+
local file="$1" key="$2"
|
|
13
|
+
grep -m1 "^${key}:" "$file" 2>/dev/null | sed "s/^${key}: *//" | sed 's/^"//;s/"$//' | sed "s/^'//;s/'$//" || echo ""
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
count=0
|
|
17
|
+
|
|
18
|
+
assign_tier() {
|
|
19
|
+
local epic_id="$1"
|
|
20
|
+
case "$epic_id" in
|
|
21
|
+
e28|e38|e40|e41|e44|e45|e46) echo "core" ;;
|
|
22
|
+
e39|e42|e43|e47|e48|e49) echo "extended" ;;
|
|
23
|
+
e12|e18|e19|e21|e25) echo "extended" ;;
|
|
24
|
+
e33|e35|e36|e37) echo "extended" ;;
|
|
25
|
+
*) echo "specialized" ;;
|
|
26
|
+
esac
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for epic_yaml in "$ROOT"/specs/epics/*/epic.yaml; do
|
|
30
|
+
[ -f "$epic_yaml" ] || continue
|
|
31
|
+
|
|
32
|
+
epic_dir=$(dirname "$epic_yaml")
|
|
33
|
+
epic_slug=$(basename "$epic_dir")
|
|
34
|
+
|
|
35
|
+
epic_id=$(extract "$epic_yaml" "id")
|
|
36
|
+
title=$(extract "$epic_yaml" "title" | tr -d '"')
|
|
37
|
+
wsjf=$(extract "$epic_yaml" "wsjf")
|
|
38
|
+
bcps=$(extract "$epic_yaml" "bcps")
|
|
39
|
+
status=$(extract "$epic_yaml" "status")
|
|
40
|
+
release=$(extract "$epic_yaml" "release")
|
|
41
|
+
codename=$(extract "$epic_yaml" "codename" | tr -d '"')
|
|
42
|
+
|
|
43
|
+
[[ -z "$epic_id" ]] && continue
|
|
44
|
+
|
|
45
|
+
tier=$(assign_tier "$epic_id")
|
|
46
|
+
|
|
47
|
+
# Build references array from story list
|
|
48
|
+
story_count=$(grep -c '^\s\+- id:' "$epic_yaml" 2>/dev/null || echo "0")
|
|
49
|
+
refs=$(grep '^\s\+- id:' "$epic_yaml" 2>/dev/null | sed 's/.*- id: */ - /' || echo "")
|
|
50
|
+
|
|
51
|
+
bundle="$WIKI/${epic_id}.okf.md"
|
|
52
|
+
|
|
53
|
+
cat > "$bundle" << OKFEOF
|
|
54
|
+
---
|
|
55
|
+
okf_kind: concept
|
|
56
|
+
okf_version: "0.1"
|
|
57
|
+
id: "${epic_id}"
|
|
58
|
+
title: "${title:-${epic_slug}}"
|
|
59
|
+
category: epic
|
|
60
|
+
tier: ${tier}
|
|
61
|
+
wsjf: ${wsjf:-0}
|
|
62
|
+
bcps: ${bcps:-0}
|
|
63
|
+
status: ${status:-proposed}
|
|
64
|
+
release: ${release:-unknown}
|
|
65
|
+
codename: "${codename:-}"
|
|
66
|
+
story_count: ${story_count}
|
|
67
|
+
generator: scripts/generate-epics-wiki.sh
|
|
68
|
+
references:
|
|
69
|
+
${refs}
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
# ${title:-${epic_slug}}
|
|
73
|
+
|
|
74
|
+
**Epic:** ${epic_id} | **WSJF:** ${wsjf:-0} | **BCP:** ${bcps:-0} | **Status:** ${status:-proposed}
|
|
75
|
+
**Release:** ${release:-unknown} | **Tier:** ${tier}
|
|
76
|
+
|
|
77
|
+
${story_count} stories. See \`${epic_yaml}\` for full specifications.
|
|
78
|
+
OKFEOF
|
|
79
|
+
|
|
80
|
+
count=$((count + 1))
|
|
81
|
+
echo " → ${epic_id}: ${title:-${epic_slug}} [${tier}]"
|
|
82
|
+
done
|
|
83
|
+
|
|
84
|
+
echo "generate-epics-wiki: ${count} concept bundles → $WIKI/"
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e45s04
|
|
3
|
+
# generate-viz-graph.sh — build specs/viz.html from bigpowers-mcp/graph.jsonl
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
|
+
GRAPH="$ROOT/bigpowers-mcp/graph.jsonl"
|
|
7
|
+
OUT="$ROOT/specs/viz.html"
|
|
8
|
+
|
|
9
|
+
ENTITIES_JSON=$(python3 -c "
|
|
10
|
+
import json, sys
|
|
11
|
+
entities = []
|
|
12
|
+
with open('$GRAPH') as f:
|
|
13
|
+
for line in f:
|
|
14
|
+
obj = json.loads(line)
|
|
15
|
+
if obj.get('type') == 'entity':
|
|
16
|
+
entities.append(obj)
|
|
17
|
+
print(json.dumps(entities))
|
|
18
|
+
")
|
|
19
|
+
|
|
20
|
+
EDGES_JSON=$(python3 -c "
|
|
21
|
+
import json, sys
|
|
22
|
+
edges = []
|
|
23
|
+
with open('$GRAPH') as f:
|
|
24
|
+
for line in f:
|
|
25
|
+
obj = json.loads(line)
|
|
26
|
+
if obj.get('type') == 'relation':
|
|
27
|
+
edges.append({'from': obj['from'], 'to': obj['to'], 'relationType': obj.get('relationType','')})
|
|
28
|
+
print(json.dumps(edges))
|
|
29
|
+
")
|
|
30
|
+
|
|
31
|
+
python3 - "$ENTITIES_JSON" "$EDGES_JSON" "$OUT" << 'PY'
|
|
32
|
+
import sys, json
|
|
33
|
+
|
|
34
|
+
entities_json = sys.argv[1]
|
|
35
|
+
edges_json = sys.argv[2]
|
|
36
|
+
out_path = sys.argv[3]
|
|
37
|
+
|
|
38
|
+
entities = json.loads(entities_json)
|
|
39
|
+
edges = json.loads(edges_json)
|
|
40
|
+
|
|
41
|
+
html = f'''<!DOCTYPE html>
|
|
42
|
+
<html lang="en">
|
|
43
|
+
<head>
|
|
44
|
+
<meta charset="UTF-8">
|
|
45
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
46
|
+
<title>bigpowers — Skill Graph</title>
|
|
47
|
+
<style>
|
|
48
|
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
|
49
|
+
body {{ background: #0d1117; color: #c9d1d9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; overflow: hidden; }}
|
|
50
|
+
#graph {{ width: 100vw; height: 100vh; }}
|
|
51
|
+
.node {{ cursor: pointer; }}
|
|
52
|
+
.node circle {{ stroke-width: 1.5px; }}
|
|
53
|
+
.node text {{ font-size: 10px; fill: #c9d1d9; pointer-events: none; }}
|
|
54
|
+
.link {{ stroke: #30363d; stroke-opacity: 0.6; }}
|
|
55
|
+
.tooltip {{
|
|
56
|
+
position: absolute; background: #161b22; border: 1px solid #30363d;
|
|
57
|
+
border-radius: 6px; padding: 10px 14px; font-size: 12px; max-width: 320px;
|
|
58
|
+
pointer-events: none; opacity: 0; transition: opacity 0.15s;
|
|
59
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.4); z-index: 100;
|
|
60
|
+
}}
|
|
61
|
+
.tooltip .name {{ font-weight: 600; font-size: 14px; color: #58a6ff; margin-bottom: 4px; }}
|
|
62
|
+
.tooltip .meta {{ color: #8b949e; font-size: 11px; margin-bottom: 4px; }}
|
|
63
|
+
.tooltip .desc {{ color: #c9d1d9; font-size: 11px; line-height: 1.4; }}
|
|
64
|
+
.legend {{ position: absolute; top: 16px; right: 16px; background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 10px 14px; font-size: 11px; z-index: 100; }}
|
|
65
|
+
.legend-item {{ display: flex; align-items: center; gap: 6px; margin: 4px 0; }}
|
|
66
|
+
.legend-swatch {{ width: 12px; height: 12px; border-radius: 50%; }}
|
|
67
|
+
header {{ position: absolute; top: 16px; left: 16px; z-index: 100; }}
|
|
68
|
+
header h1 {{ font-size: 18px; color: #58a6ff; margin: 0; }}
|
|
69
|
+
header p {{ font-size: 11px; color: #8b949e; margin: 2px 0 0; }}
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<div id="graph"></div>
|
|
74
|
+
<div class="tooltip" id="tooltip"></div>
|
|
75
|
+
<header><h1>bigpowers Skill Graph</h1><p>{len(entities)} skills · {len(edges)} relationships</p></header>
|
|
76
|
+
<div class="legend">
|
|
77
|
+
<div class="legend-item"><span class="legend-swatch" style="background:#58a6ff"></span> light</div>
|
|
78
|
+
<div class="legend-item"><span class="legend-swatch" style="background:#3fb950"></span> standard</div>
|
|
79
|
+
<div class="legend-item"><span class="legend-swatch" style="background:#d29922"></span> heavy</div>
|
|
80
|
+
</div>
|
|
81
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
82
|
+
<script>
|
|
83
|
+
const ENTITIES = {json.dumps(entities)};
|
|
84
|
+
const EDGES = {json.dumps(edges)};
|
|
85
|
+
|
|
86
|
+
const effortColors = {{ light: '#58a6ff', standard: '#3fb950', heavy: '#d29922' }};
|
|
87
|
+
const effortRadii = {{ light: 8, standard: 12, heavy: 16 }};
|
|
88
|
+
|
|
89
|
+
const nodes = ENTITIES.map(e => {{
|
|
90
|
+
const obs = (e.observations || []).join(' ');
|
|
91
|
+
const effortMatch = obs.match(/effort:\\s*(\\w+)/);
|
|
92
|
+
const effort = effortMatch ? effortMatch[1] : 'standard';
|
|
93
|
+
const descMatch = obs.match(/description:\\s*(.+?)(?:\\]|$)/);
|
|
94
|
+
const desc = descMatch ? descMatch[1] : '';
|
|
95
|
+
const modelMatch = obs.match(/model:\\s*(\\w+)/);
|
|
96
|
+
const model = modelMatch ? modelMatch[1] : '';
|
|
97
|
+
return {{ id: e.name, type: e.entityType, effort, model, description: desc, radius: effortRadii[effort] || 12, color: effortColors[effort] || '#8b949e' }};
|
|
98
|
+
}});
|
|
99
|
+
|
|
100
|
+
const nodeIds = new Set(nodes.map(n => n.id));
|
|
101
|
+
const links = EDGES.filter(e => nodeIds.has(e.from) && nodeIds.has(e.to))
|
|
102
|
+
.map(e => ({{ source: e.from, target: e.to, type: e.relationType }}));
|
|
103
|
+
|
|
104
|
+
const width = window.innerWidth, height = window.innerHeight;
|
|
105
|
+
const svg = d3.select('#graph').append('svg').attr('width', width).attr('height', height);
|
|
106
|
+
const g = svg.append('g');
|
|
107
|
+
const tooltip = d3.select('#tooltip');
|
|
108
|
+
svg.call(d3.zoom().scaleExtent([0.2, 4]).on('zoom', (e) => g.attr('transform', e.transform)));
|
|
109
|
+
|
|
110
|
+
const simulation = d3.forceSimulation(nodes)
|
|
111
|
+
.force('link', d3.forceLink(links).id(d => d.id).distance(80))
|
|
112
|
+
.force('charge', d3.forceManyBody().strength(-200))
|
|
113
|
+
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
114
|
+
.force('collision', d3.forceCollide().radius(d => d.radius + 4));
|
|
115
|
+
|
|
116
|
+
g.append('g').selectAll('line').data(links).join('line')
|
|
117
|
+
.attr('class', 'link').attr('stroke-width', 1);
|
|
118
|
+
|
|
119
|
+
const node = g.append('g').selectAll('g').data(nodes).join('g')
|
|
120
|
+
.attr('class', 'node')
|
|
121
|
+
.call(d3.drag()
|
|
122
|
+
.on('start', (e, d) => {{ if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }})
|
|
123
|
+
.on('drag', (e, d) => {{ d.fx = e.x; d.fy = e.y; }})
|
|
124
|
+
.on('end', (e, d) => {{ if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; }}));
|
|
125
|
+
|
|
126
|
+
node.append('circle').attr('r', d => d.radius).attr('fill', d => d.color).attr('stroke', '#30363d');
|
|
127
|
+
node.append('text').attr('dy', d => d.radius + 12).attr('text-anchor', 'middle').text(d => d.id);
|
|
128
|
+
|
|
129
|
+
node.on('mouseenter', (e, d) => {{
|
|
130
|
+
tooltip.style('opacity', 1)
|
|
131
|
+
.html('<div class="name">' + d.id + '</div><div class="meta">' + d.type + ' · ' + d.effort + (d.model ? ' · model: ' + d.model : '') + '</div><div class="desc">' + d.description.slice(0, 200) + '</div>');
|
|
132
|
+
}}).on('mousemove', (e) => {{
|
|
133
|
+
tooltip.style('left', (e.pageX + 14) + 'px').style('top', (e.pageY - 10) + 'px');
|
|
134
|
+
}}).on('mouseleave', () => tooltip.style('opacity', 0));
|
|
135
|
+
|
|
136
|
+
simulation.on('tick', () => {{
|
|
137
|
+
svg.selectAll('.link').attr('x1', d => d.source.x).attr('y1', d => d.source.y).attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
|
138
|
+
node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
|
|
139
|
+
}});
|
|
140
|
+
</script>
|
|
141
|
+
</body>
|
|
142
|
+
</html>'''
|
|
143
|
+
|
|
144
|
+
with open(out_path, 'w') as f:
|
|
145
|
+
f.write(html)
|
|
146
|
+
print(f"generate-viz-graph: {len(entities)} entities + {len(edges)} edges -> {out_path}")
|
|
147
|
+
PY
|
|
@@ -31,6 +31,7 @@ done
|
|
|
31
31
|
GATES=(
|
|
32
32
|
"compliance:npm run compliance:false"
|
|
33
33
|
"g04-selftest:bash scripts/golden-g04-selftest.sh:true"
|
|
34
|
+
"g06-migrate-version:bash tests/run-golden-migrate-version.sh:true"
|
|
34
35
|
"g07-negative-path:bash scripts/golden-g07-negative-path.sh:false"
|
|
35
36
|
"g08-anti-vacuity:bash scripts/golden-g08-anti-vacuity.sh:false"
|
|
36
37
|
"g09-yaml-roundtrip:bash scripts/golden-g09-yaml-roundtrip.sh:false"
|
|
@@ -174,6 +175,38 @@ summary:
|
|
|
174
175
|
overall: "$OVERALL"
|
|
175
176
|
YAML_EOF
|
|
176
177
|
|
|
178
|
+
# ── OKF verification-report (e45s02) ────────────────────────────────────
|
|
179
|
+
OKF_DIR="specs/verifications/reports"
|
|
180
|
+
mkdir -p "$OKF_DIR"
|
|
181
|
+
OKF_FILE="$OKF_DIR/GOLDEN-${TODAY}.okf.md"
|
|
182
|
+
THRESHOLD=100
|
|
183
|
+
GATE_STATUS="$OVERALL"
|
|
184
|
+
|
|
185
|
+
cat > "$OKF_FILE" << OKF_EOF
|
|
186
|
+
---
|
|
187
|
+
okf_kind: verification-report
|
|
188
|
+
okf_version: "0.1"
|
|
189
|
+
score: ${PASS_RATE}
|
|
190
|
+
gate_status: "${GATE_STATUS}"
|
|
191
|
+
threshold: ${THRESHOLD}
|
|
192
|
+
total_pass: ${PASS_COUNT}
|
|
193
|
+
total_fail: ${FAIL_COUNT}
|
|
194
|
+
total_skip: ${SKIP_COUNT}
|
|
195
|
+
generated_by: scripts/run-golden-suite.sh
|
|
196
|
+
generated_at: "${TIMESTAMP}"
|
|
197
|
+
git_commit: "${GIT_COMMIT}"
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
# Golden Suite — ${TODAY}
|
|
201
|
+
|
|
202
|
+
**Score:** ${PASS_RATE} (${PASS_COUNT}/${TOTAL} passed)
|
|
203
|
+
**Gate:** ${GATE_STATUS} | **Threshold:** ${THRESHOLD}%
|
|
204
|
+
|
|
205
|
+
See \`${REPORT_FILE}\` for the full YAML report.
|
|
206
|
+
OKF_EOF
|
|
207
|
+
|
|
208
|
+
echo "OKF report: $OKF_FILE"
|
|
209
|
+
|
|
177
210
|
# ── Size budget (placeholder for e31s04) ──────────────────────────────
|
|
178
211
|
|
|
179
212
|
if $BASELINE_MODE; then
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
2
|
+
# story: e45s03
|
|
3
|
+
# sync-bugs-registry.sh — rebuild specs/bugs/registry.yaml + OKF concept bundles
|
|
3
4
|
set -euo pipefail
|
|
4
|
-
|
|
5
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
|
|
5
6
|
BUGS="$REPO_ROOT/specs/bugs"
|
|
6
7
|
mkdir -p "$BUGS"
|
|
7
8
|
|
|
8
|
-
python3 - "$BUGS" <<'PY'
|
|
9
|
+
python3 - "$BUGS" "$REPO_ROOT" <<'PY'
|
|
9
10
|
import re
|
|
10
11
|
import sys
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
13
14
|
bugs_dir = Path(sys.argv[1])
|
|
15
|
+
repo_root = Path(sys.argv[2])
|
|
16
|
+
|
|
17
|
+
# ---- Phase 1: Build registry.yaml ----
|
|
14
18
|
entries = []
|
|
15
19
|
for path in sorted(bugs_dir.glob("BUG-*.md")):
|
|
16
20
|
text = path.read_text(encoding="utf-8")
|
|
@@ -56,4 +60,40 @@ for e in entries:
|
|
|
56
60
|
lines.append("")
|
|
57
61
|
out.write_text("\n".join(lines), encoding="utf-8")
|
|
58
62
|
print(f"sync-bugs-registry: {len(entries)} bugs -> {out}")
|
|
63
|
+
|
|
64
|
+
# ---- Phase 2: Emit OKF concept bundles (e45s03) ----
|
|
65
|
+
okf_count = 0
|
|
66
|
+
for e in entries:
|
|
67
|
+
bug_id = e["id"]
|
|
68
|
+
slug = bug_id.replace("BUG-", "").lower()
|
|
69
|
+
bundle_path = bugs_dir / f"{bug_id}.okf.md"
|
|
70
|
+
|
|
71
|
+
title_escaped = e["title"].replace('"', "'")
|
|
72
|
+
refs = f" - specs/bugs/{bug_id}.md"
|
|
73
|
+
|
|
74
|
+
bundle = f"""---
|
|
75
|
+
okf_kind: concept
|
|
76
|
+
okf_version: "0.1"
|
|
77
|
+
id: "{bug_id}"
|
|
78
|
+
title: "{title_escaped}"
|
|
79
|
+
category: bug
|
|
80
|
+
tier: extended
|
|
81
|
+
severity: {e["severity"]}
|
|
82
|
+
status: {e["status"]}
|
|
83
|
+
generator: scripts/sync-bugs-registry.sh
|
|
84
|
+
references:
|
|
85
|
+
{refs}
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
# {title_escaped}
|
|
89
|
+
|
|
90
|
+
**Bug:** {bug_id} | **Severity:** {e["severity"]} | **Status:** {e["status"]} | **Scope:** {e.get("scope", "general")}
|
|
91
|
+
**Tier:** extended
|
|
92
|
+
|
|
93
|
+
See `specs/bugs/{bug_id}.md` for full investigation and fix details.
|
|
94
|
+
"""
|
|
95
|
+
bundle_path.write_text(bundle, encoding="utf-8")
|
|
96
|
+
okf_count += 1
|
|
97
|
+
|
|
98
|
+
print(f"sync-bugs-registry: {okf_count} OKF concept bundles -> {bugs_dir}/")
|
|
59
99
|
PY
|
package/scripts/sync-skills.sh
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# story: e13s03 e28s03
|
|
3
|
-
# sync-skills.sh — generate Cursor, Gemini CLI, and
|
|
2
|
+
# story: e13s03 e28s03 e39s04
|
|
3
|
+
# sync-skills.sh — generate Cursor, Gemini CLI, pi, and OKF artifacts from SKILL.md source files
|
|
4
4
|
# Architecture: Parse→IR→Render. Each target is its own render_<target>() function.
|
|
5
5
|
# Run this after adding or updating any skill. Symlinks carry changes through automatically.
|
|
6
|
+
#
|
|
7
|
+
# Flags:
|
|
8
|
+
# --okf Generate specs/skills-wiki/ OKF concept bundle from SKILL.md frontmatter
|
|
9
|
+
# --opencode <p> Sync to bigpowers-opencode repo
|
|
6
10
|
set -euo pipefail
|
|
7
11
|
|
|
12
|
+
# ── Parse CLI flags (must happen before source to allow early --help) ─
|
|
13
|
+
OKF_MODE=0
|
|
14
|
+
OPN_TARGET=""
|
|
15
|
+
|
|
16
|
+
for arg in "$@"; do
|
|
17
|
+
case "$arg" in
|
|
18
|
+
--okf) OKF_MODE=1 ;;
|
|
19
|
+
--opencode=*) OPN_TARGET="${arg#*=}" ;;
|
|
20
|
+
--opencode) shift; OPN_TARGET="$1" ;;
|
|
21
|
+
--help|-h)
|
|
22
|
+
echo "Usage: sync-skills.sh [--okf] [--opencode <path>]"
|
|
23
|
+
echo ""
|
|
24
|
+
echo " --okf Generate specs/skills-wiki/ OKF concept bundle"
|
|
25
|
+
echo " --opencode Sync skills to bigpowers-opencode repo"
|
|
26
|
+
echo " --help Show this message"
|
|
27
|
+
exit 0
|
|
28
|
+
;;
|
|
29
|
+
*) echo "sync-skills: unknown flag: $arg" >&2; exit 2 ;;
|
|
30
|
+
esac
|
|
31
|
+
done
|
|
32
|
+
|
|
8
33
|
source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
|
|
9
34
|
|
|
10
35
|
CURSOR_RULES="$REPO_ROOT/.cursor/rules"
|
|
@@ -120,9 +145,104 @@ render_pi_prompt() {
|
|
|
120
145
|
} > "$PI_PROMPTS/$IR_NAME.md"
|
|
121
146
|
}
|
|
122
147
|
|
|
148
|
+
# ── Render target: OKF Concept (e39s04) ──────────────────────────────
|
|
149
|
+
# Writes specs/skills-wiki/skills/<name>.md with OKF concept frontmatter.
|
|
150
|
+
# Globals used: IR_NAME, IR_MODEL, IR_DESCRIPTION, IR_DESC_ESCAPED, IR_BODY
|
|
151
|
+
# Also reads _PF_EFFORT from the extended parse_frontmatter.
|
|
152
|
+
OKF_WIKI_SKILLS="$REPO_ROOT/specs/skills-wiki/skills"
|
|
153
|
+
OKF_WIKI_DIR="$REPO_ROOT/specs/skills-wiki"
|
|
154
|
+
|
|
155
|
+
render_okf_concept() {
|
|
156
|
+
mkdir -p "$OKF_WIKI_SKILLS"
|
|
157
|
+
local okf_file="$OKF_WIKI_SKILLS/$IR_NAME.md"
|
|
158
|
+
|
|
159
|
+
# Determine phase: critical-path skills per the bigpowers core loop
|
|
160
|
+
local phase="utility"
|
|
161
|
+
case "$IR_NAME" in
|
|
162
|
+
survey-context|elaborate-spec|model-domain|define-language|\
|
|
163
|
+
design-interface|plan-work|plan-release|slice-tasks|\
|
|
164
|
+
kickoff-branch|develop-tdd|verify-work|audit-code|\
|
|
165
|
+
commit-message|release-branch|security-review|\
|
|
166
|
+
assess-impact|investigate-bug|validate-fix|dispatch-agents|\
|
|
167
|
+
request-review|respond-review|hook-commits|guard-git|\
|
|
168
|
+
wire-observability|wire-ci|smoke-test|deploy|enforce-first)
|
|
169
|
+
phase="critical-path" ;;
|
|
170
|
+
esac
|
|
171
|
+
|
|
172
|
+
# Build title from name: kebab-case → Title Case
|
|
173
|
+
local title
|
|
174
|
+
title=$(echo "$IR_NAME" | sed -E 's/-/ /g; s/\b(.)/\U\1/g')
|
|
175
|
+
|
|
176
|
+
# Escape description for YAML double-quoted string
|
|
177
|
+
local desc_escaped
|
|
178
|
+
desc_escaped=$(echo "$IR_DESCRIPTION" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
|
179
|
+
|
|
180
|
+
# Truncate description to avoid overly long lines (OKF convention)
|
|
181
|
+
local short_desc="$IR_DESCRIPTION"
|
|
182
|
+
if [[ ${#short_desc} -gt 200 ]]; then
|
|
183
|
+
short_desc="${short_desc:0:197}..."
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
local effort_val="${_PF_EFFORT:-standard}"
|
|
187
|
+
|
|
188
|
+
{
|
|
189
|
+
echo "---"
|
|
190
|
+
echo "okf_kind: concept"
|
|
191
|
+
echo "type: Skill"
|
|
192
|
+
echo "id: $IR_NAME"
|
|
193
|
+
echo "title: \"$title\""
|
|
194
|
+
echo "name: $IR_NAME"
|
|
195
|
+
echo "category: skills"
|
|
196
|
+
[[ -n "$IR_MODEL" ]] && echo "model: $IR_MODEL"
|
|
197
|
+
echo "effort: $effort_val"
|
|
198
|
+
echo "phase: $phase"
|
|
199
|
+
echo "description: \"$desc_escaped\""
|
|
200
|
+
echo "references: []"
|
|
201
|
+
echo "---"
|
|
202
|
+
echo ""
|
|
203
|
+
echo "# $title"
|
|
204
|
+
echo ""
|
|
205
|
+
echo "**Phase:** $phase"
|
|
206
|
+
[[ -n "$IR_MODEL" ]] && echo "**Model:** $IR_MODEL"
|
|
207
|
+
echo "**Effort:** $effort_val"
|
|
208
|
+
echo ""
|
|
209
|
+
echo "$IR_DESCRIPTION"
|
|
210
|
+
echo ""
|
|
211
|
+
echo "> Auto-generated by sync-skills.sh --okf (e39s04) from $SKILLS_ROOT/$IR_NAME/SKILL.md"
|
|
212
|
+
} > "$okf_file"
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# ── Parse extended frontmatter for OKF (includes effort) ────────────
|
|
216
|
+
# Extends the base parse_frontmatter with _PF_EFFORT.
|
|
217
|
+
parse_frontmatter_okf() {
|
|
218
|
+
local file="$1"
|
|
219
|
+
_PF_EFFORT=""
|
|
220
|
+
|
|
221
|
+
if [[ ! -f "$file" ]]; then
|
|
222
|
+
return 1
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
if ! grep -q '^---$' "$file"; then
|
|
226
|
+
return 1
|
|
227
|
+
fi
|
|
228
|
+
|
|
229
|
+
_PF_NAME=$(awk '/^---/{f++} f==1 && /^name:/{print; exit}' "$file" | sed 's/^name:[[:space:]]*//')
|
|
230
|
+
_PF_MODEL=$(awk '/^---/{f++} f==1 && /^model:/{print; exit}' "$file" | sed 's/^model:[[:space:]]*//')
|
|
231
|
+
_PF_EFFORT=$(awk '/^---/{f++} f==1 && /^effort:/{print; exit}' "$file" | sed 's/^effort:[[:space:]]*//')
|
|
232
|
+
_PF_DESCRIPTION=$(awk '/^---/{f++; next} f==1 && /^description:/{p=1; sub(/^description:[[:space:]]*/,""); print; next} f==1 && p && /^[a-z]+:/{exit} f==1 && p{print}' "$file" \
|
|
233
|
+
| tr -d '\n' \
|
|
234
|
+
| sed -E 's/[[:space:]]+/ /g')
|
|
235
|
+
|
|
236
|
+
[[ -z "$_PF_NAME" ]] && return 1
|
|
237
|
+
return 0
|
|
238
|
+
}
|
|
239
|
+
|
|
123
240
|
# ── Target registry ─────────────────────────────────────────────────
|
|
124
241
|
# Order matters: render functions are called in this order per skill.
|
|
125
242
|
TARGETS=(render_cursor render_gemini_skill render_gemini_command render_pi_skill render_pi_prompt)
|
|
243
|
+
if [[ "$OKF_MODE" -eq 1 ]]; then
|
|
244
|
+
TARGETS+=(render_okf_concept)
|
|
245
|
+
fi
|
|
126
246
|
|
|
127
247
|
skill_count=0
|
|
128
248
|
|
|
@@ -130,14 +250,18 @@ skill_count=0
|
|
|
130
250
|
while IFS= read -r skill_dir; do
|
|
131
251
|
skill_md="$skill_dir/SKILL.md"
|
|
132
252
|
|
|
133
|
-
|
|
253
|
+
if [[ "$OKF_MODE" -eq 1 ]]; then
|
|
254
|
+
parse_frontmatter_okf "$skill_md" || continue
|
|
255
|
+
else
|
|
256
|
+
parse_frontmatter "$skill_md" || continue
|
|
257
|
+
fi
|
|
134
258
|
|
|
135
259
|
IR_NAME="$_PF_NAME"
|
|
136
260
|
IR_MODEL="$_PF_MODEL"
|
|
137
261
|
IR_DESCRIPTION="$_PF_DESCRIPTION"
|
|
138
262
|
|
|
139
263
|
# Escape double quotes and backslashes for safe double-quoted YAML output
|
|
140
|
-
IR_DESC_ESCAPED=$(echo "$IR_DESCRIPTION" | sed 's/\\/\\\\/g; s
|
|
264
|
+
IR_DESC_ESCAPED=$(echo "$IR_DESCRIPTION" | sed 's/\\/\\\\/g; s/\"/\\"/g')
|
|
141
265
|
|
|
142
266
|
[[ -z "$IR_NAME" ]] && continue
|
|
143
267
|
|
|
@@ -185,14 +309,6 @@ jq -n --arg version "$pkg_version" \
|
|
|
185
309
|
} > "$REPO_ROOT/opencode.json"
|
|
186
310
|
|
|
187
311
|
# Sync to bigpowers-opencode repo (if --opencode path is provided)
|
|
188
|
-
OPN_TARGET=""
|
|
189
|
-
for arg in "$@"; do
|
|
190
|
-
case "$arg" in
|
|
191
|
-
--opencode=*) OPN_TARGET="${arg#*=}" ;;
|
|
192
|
-
--opencode) shift; OPN_TARGET="$1" ;;
|
|
193
|
-
esac
|
|
194
|
-
done
|
|
195
|
-
|
|
196
312
|
if [[ -n "$OPN_TARGET" ]] && [[ -d "$OPN_TARGET" ]]; then
|
|
197
313
|
echo ""
|
|
198
314
|
echo "Syncing skills to opencode repo: $OPN_TARGET"
|
|
@@ -258,6 +374,258 @@ echo " → SKILL-INDEX.md (auto-generated skill reference)"
|
|
|
258
374
|
echo " → opencode.json (CLAUDE.md + CONVENTIONS.md instructions)"
|
|
259
375
|
[[ -n "$OPN_TARGET" ]] && echo " → bigpowers-opencode: $opencode_count skills"
|
|
260
376
|
|
|
377
|
+
# ── OKF post-sync pipeline (e39s04) ──────────────────────────────────
|
|
378
|
+
if [[ "$OKF_MODE" -eq 1 ]]; then
|
|
379
|
+
echo ""
|
|
380
|
+
echo "--- OKF skills-wiki post-sync ---"
|
|
381
|
+
|
|
382
|
+
# Step 2: Run build-skill-graph.sh to get cross-references in skill-graph.json
|
|
383
|
+
GRAPH_JSON="$REPO_ROOT/specs/skill-graph.json"
|
|
384
|
+
if [[ -x "$REPO_ROOT/scripts/build-skill-graph.sh" ]]; then
|
|
385
|
+
echo "Running build-skill-graph.sh for cross-references..."
|
|
386
|
+
bash "$REPO_ROOT/scripts/build-skill-graph.sh" --json || {
|
|
387
|
+
echo "sync-skills: WARNING — build-skill-graph.sh --json failed; cross-references will be empty" >&2
|
|
388
|
+
}
|
|
389
|
+
else
|
|
390
|
+
echo "sync-skills: WARNING — build-skill-graph.sh not found; skipping cross-references" >&2
|
|
391
|
+
fi
|
|
392
|
+
|
|
393
|
+
# Step 2b: Add cross-references from skill-graph.json edges into OKF concepts
|
|
394
|
+
if [[ -f "$GRAPH_JSON" ]]; then
|
|
395
|
+
echo "Adding cross-references from skill-graph.json..."
|
|
396
|
+
python3 - "$GRAPH_JSON" "$OKF_WIKI_SKILLS" <<'PYEOF'
|
|
397
|
+
import json, sys, re
|
|
398
|
+
from pathlib import Path
|
|
399
|
+
|
|
400
|
+
graph_json = Path(sys.argv[1])
|
|
401
|
+
wiki_skills = Path(sys.argv[2])
|
|
402
|
+
|
|
403
|
+
# Load graph edges
|
|
404
|
+
with open(graph_json) as f:
|
|
405
|
+
graph = json.load(f)
|
|
406
|
+
|
|
407
|
+
edges = graph.get("edges", [])
|
|
408
|
+
nodes = {n["name"] for n in graph.get("nodes", [])}
|
|
409
|
+
|
|
410
|
+
# Build adjacency: skill_name -> list of {concept, type} refs
|
|
411
|
+
refs = {}
|
|
412
|
+
for e in edges:
|
|
413
|
+
src, tgt, rel = e["from"], e["to"], e.get("relationType", "references")
|
|
414
|
+
# Only include edges where both ends are known skills
|
|
415
|
+
if src in nodes and tgt in nodes:
|
|
416
|
+
refs.setdefault(src, []).append({"concept": tgt, "type": rel})
|
|
417
|
+
|
|
418
|
+
# Also detect intra-description references: scan description for skill names
|
|
419
|
+
skill_names = set()
|
|
420
|
+
for f in wiki_skills.glob("*.md"):
|
|
421
|
+
skill_names.add(f.stem)
|
|
422
|
+
|
|
423
|
+
# Update each concept file with references
|
|
424
|
+
count_updated = 0
|
|
425
|
+
for f in wiki_skills.glob("*.md"):
|
|
426
|
+
name = f.stem
|
|
427
|
+
content = f.read_text()
|
|
428
|
+
|
|
429
|
+
# Collect references for this skill
|
|
430
|
+
concept_refs = refs.get(name, [])
|
|
431
|
+
|
|
432
|
+
# Also scan description for mentions of other skill names
|
|
433
|
+
desc_match = re.search(r'description:\s*"(.*?)"', content, re.DOTALL)
|
|
434
|
+
if desc_match:
|
|
435
|
+
desc = desc_match.group(1)
|
|
436
|
+
for sn in skill_names:
|
|
437
|
+
if sn != name and sn in desc and not any(r["concept"] == sn for r in concept_refs):
|
|
438
|
+
concept_refs.append({"concept": sn, "type": "references"})
|
|
439
|
+
|
|
440
|
+
# Deduplicate
|
|
441
|
+
seen = set()
|
|
442
|
+
unique_refs = []
|
|
443
|
+
for r in concept_refs:
|
|
444
|
+
key = (r["concept"], r["type"])
|
|
445
|
+
if key not in seen:
|
|
446
|
+
seen.add(key)
|
|
447
|
+
unique_refs.append(r)
|
|
448
|
+
|
|
449
|
+
# Build YAML references block
|
|
450
|
+
if unique_refs:
|
|
451
|
+
ref_lines = []
|
|
452
|
+
for r in unique_refs:
|
|
453
|
+
ref_lines.append(f' - concept: {r["concept"]}')
|
|
454
|
+
ref_lines.append(f' type: {r["type"]}')
|
|
455
|
+
ref_block = "\n".join(ref_lines)
|
|
456
|
+
content = re.sub(r'references:\s*\[\]', f'references:\n{ref_block}', content)
|
|
457
|
+
count_updated += 1
|
|
458
|
+
else:
|
|
459
|
+
# No edges and no description mentions → self-reference as fallback
|
|
460
|
+
content = re.sub(
|
|
461
|
+
r'references:\s*\[\]',
|
|
462
|
+
f'references:\n - concept: skills-wiki\n type: belongs_to',
|
|
463
|
+
content
|
|
464
|
+
)
|
|
465
|
+
count_updated += 1
|
|
466
|
+
|
|
467
|
+
f.write_text(content)
|
|
468
|
+
|
|
469
|
+
print(f" concepts with cross-references: {count_updated}", file=sys.stderr)
|
|
470
|
+
PYEOF
|
|
471
|
+
else
|
|
472
|
+
echo "sync-skills: WARNING — $GRAPH_JSON not found; adding fallback references" >&2
|
|
473
|
+
# Add fallback references to all concepts
|
|
474
|
+
for f in "$OKF_WIKI_SKILLS"/*.md; do
|
|
475
|
+
[[ -f "$f" ]] || continue
|
|
476
|
+
if command -v python3 &>/dev/null; then
|
|
477
|
+
python3 -c "
|
|
478
|
+
import re, sys
|
|
479
|
+
content = open('$f').read()
|
|
480
|
+
content = re.sub(r'references:\s*\[\]', 'references:\n - concept: skills-wiki\n type: belongs_to', content)
|
|
481
|
+
open('$f', 'w').write(content)
|
|
482
|
+
"
|
|
483
|
+
fi
|
|
484
|
+
done
|
|
485
|
+
fi
|
|
486
|
+
|
|
487
|
+
# Step 3: Generate specs/skills-wiki/index.md with progressive disclosure
|
|
488
|
+
echo "Generating index.md..."
|
|
489
|
+
OKF_INDEX="$OKF_WIKI_DIR/index.md"
|
|
490
|
+
python3 - "$OKF_WIKI_SKILLS" "$OKF_WIKI_DIR" "$OKF_INDEX" <<'PYEOF'
|
|
491
|
+
import json, sys, re
|
|
492
|
+
from pathlib import Path
|
|
493
|
+
from collections import defaultdict
|
|
494
|
+
|
|
495
|
+
wiki_skills = Path(sys.argv[1])
|
|
496
|
+
wiki_dir = Path(sys.argv[2])
|
|
497
|
+
index_path = Path(sys.argv[3])
|
|
498
|
+
|
|
499
|
+
# Collect all concepts
|
|
500
|
+
concepts = []
|
|
501
|
+
for f in sorted(wiki_skills.glob("*.md")):
|
|
502
|
+
content = f.read_text()
|
|
503
|
+
fm_match = re.search(r'^---\n(.*?)\n---', content, re.DOTALL)
|
|
504
|
+
if not fm_match:
|
|
505
|
+
continue
|
|
506
|
+
fm_text = fm_match.group(1)
|
|
507
|
+
# Simple YAML extraction
|
|
508
|
+
name = ""
|
|
509
|
+
title = ""
|
|
510
|
+
phase = "utility"
|
|
511
|
+
model = ""
|
|
512
|
+
effort = ""
|
|
513
|
+
description = ""
|
|
514
|
+
for line in fm_text.split('\n'):
|
|
515
|
+
line = line.strip()
|
|
516
|
+
if line.startswith('name:'):
|
|
517
|
+
name = line.split(':', 1)[1].strip()
|
|
518
|
+
elif line.startswith('title:'):
|
|
519
|
+
title = line.split(':', 1)[1].strip().strip('"')
|
|
520
|
+
elif line.startswith('phase:'):
|
|
521
|
+
phase = line.split(':', 1)[1].strip()
|
|
522
|
+
elif line.startswith('model:'):
|
|
523
|
+
model = line.split(':', 1)[1].strip()
|
|
524
|
+
elif line.startswith('effort:'):
|
|
525
|
+
effort = line.split(':', 1)[1].strip()
|
|
526
|
+
elif line.startswith('description:'):
|
|
527
|
+
description = line.split(':', 1)[1].strip().strip('"')
|
|
528
|
+
if not name:
|
|
529
|
+
continue
|
|
530
|
+
concepts.append({
|
|
531
|
+
"name": name,
|
|
532
|
+
"title": title or name.replace('-', ' ').title(),
|
|
533
|
+
"phase": phase,
|
|
534
|
+
"model": model,
|
|
535
|
+
"effort": effort,
|
|
536
|
+
"description": description[:120] + "..." if len(description) > 120 else description,
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
# Group by phase
|
|
540
|
+
phases = defaultdict(list)
|
|
541
|
+
for c in concepts:
|
|
542
|
+
phases[c["phase"]].append(c)
|
|
543
|
+
|
|
544
|
+
# Count
|
|
545
|
+
total = len(concepts)
|
|
546
|
+
cp_count = len(phases.get("critical-path", []))
|
|
547
|
+
util_count = len(phases.get("utility", []))
|
|
548
|
+
|
|
549
|
+
# Generate index.md
|
|
550
|
+
with open(index_path, "w") as out:
|
|
551
|
+
out.write(f"""---
|
|
552
|
+
okf_kind: concept
|
|
553
|
+
type: Index
|
|
554
|
+
id: skills-wiki
|
|
555
|
+
title: "BigPowers Skills Wiki"
|
|
556
|
+
category: skills
|
|
557
|
+
description: "Progressive-disclosure index of {total} bigpowers skills ({cp_count} critical-path, {util_count} utility) — auto-generated by sync-skills.sh --okf (e39s04)."
|
|
558
|
+
references:
|
|
559
|
+
- concept: bigpowers
|
|
560
|
+
type: belongs_to
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
# BigPowers Skills Wiki
|
|
564
|
+
|
|
565
|
+
**{total} skills** ({cp_count} critical-path, {util_count} utility)
|
|
566
|
+
|
|
567
|
+
> Auto-generated by sync-skills.sh --okf (e39s04).
|
|
568
|
+
> Source of truth: skills/*/SKILL.md files.
|
|
569
|
+
|
|
570
|
+
## Critical-Path Skills ({cp_count})
|
|
571
|
+
|
|
572
|
+
These skills form the bigpowers core development loop — used on every feature.
|
|
573
|
+
|
|
574
|
+
| Skill | Model | Effort | Description |
|
|
575
|
+
|-------|-------|--------|-------------|
|
|
576
|
+
""")
|
|
577
|
+
|
|
578
|
+
for c in phases.get("critical-path", []):
|
|
579
|
+
out.write(f"| [{c['title']}](skills/{c['name']}.md) | {c['model']} | {c['effort']} | {c['description']} |\n")
|
|
580
|
+
|
|
581
|
+
out.write(f"""
|
|
582
|
+
## Utility Skills ({util_count})
|
|
583
|
+
|
|
584
|
+
Supporting skills for specialised workflows.
|
|
585
|
+
|
|
586
|
+
| Skill | Model | Effort | Description |
|
|
587
|
+
|-------|-------|--------|-------------|
|
|
588
|
+
""")
|
|
589
|
+
|
|
590
|
+
for c in phases.get("utility", []):
|
|
591
|
+
out.write(f"| [{c['title']}](skills/{c['name']}.md) | {c['model']} | {c['effort']} | {c['description']} |\n")
|
|
592
|
+
|
|
593
|
+
out.write(f"""
|
|
594
|
+
## Quick Reference
|
|
595
|
+
|
|
596
|
+
- **Total skills:** {total}
|
|
597
|
+
- **Critical-path:** {cp_count}
|
|
598
|
+
- **Utility:** {util_count}
|
|
599
|
+
- **Format:** OKF concept bundles (okf_kind: concept)
|
|
600
|
+
- **Validation:** `bash scripts/validate-okf.sh --dir specs/skills-wiki/skills`
|
|
601
|
+
""")
|
|
602
|
+
|
|
603
|
+
print(f" index.md written: {total} skills indexed", file=sys.stderr)
|
|
604
|
+
PYEOF
|
|
605
|
+
|
|
606
|
+
# Step 4: Validate OKF conformance via validate-okf.sh
|
|
607
|
+
echo ""
|
|
608
|
+
echo "Validating OKF conformance..."
|
|
609
|
+
VALIDATE_SCRIPT="$REPO_ROOT/scripts/validate-okf.sh"
|
|
610
|
+
if [[ -x "$VALIDATE_SCRIPT" ]]; then
|
|
611
|
+
if bash "$VALIDATE_SCRIPT" --dir "$OKF_WIKI_SKILLS" 2>&1; then
|
|
612
|
+
echo "sync-skills: OKF validation PASSED"
|
|
613
|
+
else
|
|
614
|
+
OKF_EXIT=$?
|
|
615
|
+
echo "sync-skills: OKF validation returned exit code $OKF_EXIT (non-zero — check output above)" >&2
|
|
616
|
+
# Don't hard-fail on validation warnings; the bundle is still generated
|
|
617
|
+
fi
|
|
618
|
+
else
|
|
619
|
+
echo "sync-skills: WARNING — validate-okf.sh not found; skipping validation" >&2
|
|
620
|
+
fi
|
|
621
|
+
|
|
622
|
+
OKF_CONCEPT_COUNT=$(find "$OKF_WIKI_SKILLS" -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
|
|
623
|
+
echo ""
|
|
624
|
+
echo "sync-skills (--okf): $OKF_CONCEPT_COUNT OKF concepts generated"
|
|
625
|
+
echo " → specs/skills-wiki/skills/ ($OKF_CONCEPT_COUNT .md files)"
|
|
626
|
+
echo " → specs/skills-wiki/index.md (progressive-disclosure index)"
|
|
627
|
+
fi
|
|
628
|
+
|
|
261
629
|
# Regression guard (BUG-2026-06-02T164500)
|
|
262
630
|
trace_mdc="$REPO_ROOT/.cursor/rules/trace-requirement.mdc"
|
|
263
631
|
if [[ -f "$trace_mdc" ]] && ! grep -q 'release-plan.yaml + epic' "$trace_mdc"; then
|
|
@@ -297,4 +665,11 @@ if [[ -d "$REPO_ROOT/.git" ]]; then
|
|
|
297
665
|
bash "$REPO_ROOT/scripts/generate-reference-tables.sh"
|
|
298
666
|
fi
|
|
299
667
|
|
|
668
|
+
# ── OKF wiki generation (e45s01) ──────────────────────────────────────
|
|
669
|
+
echo ""
|
|
670
|
+
echo "Generating OKF wikis..."
|
|
671
|
+
bash "$REPO_ROOT/scripts/generate-epics-wiki.sh"
|
|
672
|
+
bash "$REPO_ROOT/scripts/generate-adr-wiki.sh"
|
|
673
|
+
echo "sync-skills: OKF wikis regenerated"
|
|
674
|
+
|
|
300
675
|
exit 0
|
package/scripts/validate-okf.sh
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# story: e38s09
|
|
2
|
+
# story: e38s09 e45s02
|
|
3
3
|
# validate-okf.sh — kind-aware, fail-closed provenance gate for OKF bundles.
|
|
4
|
-
# Validates story-metrics, spec-migration,
|
|
4
|
+
# Validates story-metrics, spec-migration, migration-registry, concept,
|
|
5
|
+
# and verification-report bundles.
|
|
5
6
|
# Multi-dir default: specs/metrics/ + specs/migrations/.
|
|
6
7
|
set -euo pipefail
|
|
7
8
|
|
|
@@ -11,7 +12,7 @@ EXIT_CODE=0
|
|
|
11
12
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m'
|
|
12
13
|
|
|
13
14
|
usage_okf() {
|
|
14
|
-
cat <<'
|
|
15
|
+
cat <<'USAGEEOF'
|
|
15
16
|
Usage: scripts/validate-okf.sh [flags]
|
|
16
17
|
|
|
17
18
|
Flags:
|
|
@@ -28,19 +29,17 @@ Description:
|
|
|
28
29
|
story-metrics — effort/lead-time provenance (e40)
|
|
29
30
|
spec-migration — migration bundle schema (e44)
|
|
30
31
|
migration-registry — registry index integrity (e44)
|
|
31
|
-
|
|
32
|
+
concept — domain concept / wiki entry (e45)
|
|
33
|
+
verification-report — compliance/golden suite gate report (e45)
|
|
34
|
+
USAGEEOF
|
|
32
35
|
exit 0
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
# ---- Helpers ------------------------------------------------------------
|
|
36
39
|
|
|
37
|
-
# True if value is empty, null JSON literal, or Python None string.
|
|
38
40
|
val_nullish() { [ -z "$1" ] || [ "$1" = "null" ] || [ "$1" = "None" ]; }
|
|
39
|
-
|
|
40
|
-
# True if value is nullish OR the JSON empty array literal.
|
|
41
41
|
val_nullish_or_empty() { val_nullish "$1" || [ "$1" = "[]" ]; }
|
|
42
42
|
|
|
43
|
-
# ---- YAML frontmatter parser (datetime-safe) ---------------------------
|
|
44
43
|
parse_frontmatter() {
|
|
45
44
|
local file="$1"
|
|
46
45
|
python3 -c "
|
|
@@ -73,7 +72,6 @@ except Exception as e:
|
|
|
73
72
|
validate_story_metrics() {
|
|
74
73
|
local file="$1" fm="$2" name="$3"
|
|
75
74
|
local errs=0
|
|
76
|
-
|
|
77
75
|
for key in id epic bcps commit_range source generated_at generator; do
|
|
78
76
|
local val
|
|
79
77
|
val="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null)"
|
|
@@ -84,7 +82,6 @@ validate_story_metrics() {
|
|
|
84
82
|
done
|
|
85
83
|
[ "$errs" -gt 0 ] && { EXIT_CODE=1; return; }
|
|
86
84
|
|
|
87
|
-
# Source enum
|
|
88
85
|
local source
|
|
89
86
|
source="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('source',''))" 2>/dev/null)"
|
|
90
87
|
if ! echo "measured estimated backfilled" | grep -qw "$source"; then
|
|
@@ -92,7 +89,6 @@ validate_story_metrics() {
|
|
|
92
89
|
EXIT_CODE=1; return
|
|
93
90
|
fi
|
|
94
91
|
|
|
95
|
-
# Generator
|
|
96
92
|
local generator
|
|
97
93
|
generator="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('generator',''))" 2>/dev/null)"
|
|
98
94
|
if [ "$generator" != "scripts/record-cycle-time.sh" ]; then
|
|
@@ -100,7 +96,6 @@ validate_story_metrics() {
|
|
|
100
96
|
EXIT_CODE=1; return
|
|
101
97
|
fi
|
|
102
98
|
|
|
103
|
-
# commit_range resolves
|
|
104
99
|
local commit_range
|
|
105
100
|
commit_range="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('commit_range',''))" 2>/dev/null)"
|
|
106
101
|
if ! git -C "$ROOT" log --oneline -1 "$commit_range" >/dev/null 2>&1; then
|
|
@@ -108,7 +103,6 @@ validate_story_metrics() {
|
|
|
108
103
|
EXIT_CODE=1; return
|
|
109
104
|
fi
|
|
110
105
|
|
|
111
|
-
# effort.effort_hours present and non-negative
|
|
112
106
|
local eff
|
|
113
107
|
eff="$(echo "$fm" | python3 -c "
|
|
114
108
|
import json,sys
|
|
@@ -126,14 +120,12 @@ print(e)
|
|
|
126
120
|
fi
|
|
127
121
|
EXIT_CODE=1; return
|
|
128
122
|
}
|
|
129
|
-
|
|
130
123
|
printf "${GREEN}PASS${NC} %s (source=%s, generator=%s, commit_range resolves)\n" "$name" "$source" "$generator"
|
|
131
124
|
}
|
|
132
125
|
|
|
133
126
|
validate_spec_migration() {
|
|
134
127
|
local file="$1" fm="$2" name="$3"
|
|
135
128
|
local errs=0
|
|
136
|
-
|
|
137
129
|
for key in id title since_version order actions_needed; do
|
|
138
130
|
local val
|
|
139
131
|
val="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null)"
|
|
@@ -144,7 +136,6 @@ validate_spec_migration() {
|
|
|
144
136
|
done
|
|
145
137
|
[ "$errs" -gt 0 ] && { EXIT_CODE=1; return; }
|
|
146
138
|
|
|
147
|
-
# fingerprint.any has at least one check
|
|
148
139
|
local fp_count
|
|
149
140
|
fp_count="$(echo "$fm" | python3 -c "
|
|
150
141
|
import json,sys
|
|
@@ -165,7 +156,6 @@ print(len(fp))
|
|
|
165
156
|
|
|
166
157
|
validate_migration_registry() {
|
|
167
158
|
local file="$1" fm="$2" name="$3"
|
|
168
|
-
|
|
169
159
|
local count
|
|
170
160
|
count="$(echo "$fm" | python3 -c "
|
|
171
161
|
import json,sys
|
|
@@ -178,7 +168,6 @@ print(len(migrations))
|
|
|
178
168
|
EXIT_CODE=1; return
|
|
179
169
|
fi
|
|
180
170
|
|
|
181
|
-
# Check each migration has id + file + status
|
|
182
171
|
local bad
|
|
183
172
|
bad="$(echo "$fm" | python3 -c "
|
|
184
173
|
import json,sys
|
|
@@ -190,11 +179,85 @@ for m in d.get('migrations',[]):
|
|
|
190
179
|
printf "${RED}FAIL${NC} %s: migration '%s' missing id/file/status\n" "$name" "${bad:-?}"
|
|
191
180
|
EXIT_CODE=1; return
|
|
192
181
|
}
|
|
193
|
-
|
|
194
182
|
printf "${GREEN}PASS${NC} %s (%s migrations indexed)\n" "$name" "$count"
|
|
195
183
|
}
|
|
196
184
|
|
|
197
|
-
# ----
|
|
185
|
+
# ---- Concept validator (e45s02) ----
|
|
186
|
+
validate_concept() {
|
|
187
|
+
local file="$1" fm="$2" name="$3"
|
|
188
|
+
local errs=0
|
|
189
|
+
for key in id title category; do
|
|
190
|
+
local val
|
|
191
|
+
val="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null)"
|
|
192
|
+
if val_nullish "$val"; then
|
|
193
|
+
printf "${RED}FAIL${NC} %s: missing required key '%s'\n" "$name" "$key"
|
|
194
|
+
errs=$((errs + 1))
|
|
195
|
+
fi
|
|
196
|
+
done
|
|
197
|
+
local ref_count
|
|
198
|
+
ref_count="$(echo "$fm" | python3 -c "
|
|
199
|
+
import json,sys
|
|
200
|
+
d=json.load(sys.stdin)
|
|
201
|
+
r=d.get('references',[])
|
|
202
|
+
print(len(r) if isinstance(r,list) else 0)
|
|
203
|
+
" 2>/dev/null)"
|
|
204
|
+
if [ "${ref_count:-0}" -eq 0 ]; then
|
|
205
|
+
printf "${RED}FAIL${NC} %s: references[] is empty or missing\n" "$name"
|
|
206
|
+
errs=$((errs + 1))
|
|
207
|
+
fi
|
|
208
|
+
[ "$errs" -gt 0 ] && { EXIT_CODE=1; return; }
|
|
209
|
+
local cat
|
|
210
|
+
cat="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('category',''))" 2>/dev/null)"
|
|
211
|
+
printf "${GREEN}PASS${NC} %s (category=%s, %s reference(s))\n" "$name" "$cat" "$ref_count"
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# ---- Verification-report validator (e45s02) ----
|
|
215
|
+
validate_verification_report() {
|
|
216
|
+
local file="$1" fm="$2" name="$3"
|
|
217
|
+
local errs=0
|
|
218
|
+
for key in score gate_status threshold total_pass total_fail generated_by; do
|
|
219
|
+
local val
|
|
220
|
+
val="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null)"
|
|
221
|
+
if val_nullish "$val"; then
|
|
222
|
+
printf "${RED}FAIL${NC} %s: missing required key '%s'\n" "$name" "$key"
|
|
223
|
+
errs=$((errs + 1))
|
|
224
|
+
fi
|
|
225
|
+
done
|
|
226
|
+
[ "$errs" -gt 0 ] && { EXIT_CODE=1; return; }
|
|
227
|
+
|
|
228
|
+
local gs
|
|
229
|
+
gs="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('gate_status',''))" 2>/dev/null)"
|
|
230
|
+
if ! echo "pass fail concerns waived" | grep -qw "$gs"; then
|
|
231
|
+
printf "${RED}FAIL${NC} %s: invalid gate_status '%s' — must be pass|fail|concerns|waived\n" "$name" "$gs"
|
|
232
|
+
EXIT_CODE=1; return
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
local score
|
|
236
|
+
score="$(echo "$fm" | python3 -c "
|
|
237
|
+
import json,sys
|
|
238
|
+
d=json.load(sys.stdin)
|
|
239
|
+
s=d.get('score')
|
|
240
|
+
if s is None or not isinstance(s,(int,float)):
|
|
241
|
+
sys.exit(1)
|
|
242
|
+
if s < 0 or s > 100:
|
|
243
|
+
sys.exit(2)
|
|
244
|
+
print(s)
|
|
245
|
+
" 2>/dev/null)" || {
|
|
246
|
+
local rc=$?
|
|
247
|
+
if [ "$rc" -eq 1 ]; then
|
|
248
|
+
printf "${RED}FAIL${NC} %s: score missing or not numeric\n" "$name"
|
|
249
|
+
else
|
|
250
|
+
printf "${RED}FAIL${NC} %s: score out of bounds (0-100)\n" "$name"
|
|
251
|
+
fi
|
|
252
|
+
EXIT_CODE=1; return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
printf "${GREEN}PASS${NC} %s (score=%.1f, gate=%s, threshold=%.1f)\n" \
|
|
256
|
+
"$name" "$score" "$gs" \
|
|
257
|
+
"$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('threshold',0))" 2>/dev/null)"
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
# ---- Receipts validation (e41s04) ----
|
|
198
261
|
validate_receipts() {
|
|
199
262
|
local f="$ROOT/specs/receipts.json"
|
|
200
263
|
if [ ! -f "$f" ]; then printf "${YELLOW}SKIP${NC} receipts.json not found\n"; return; fi
|
|
@@ -219,25 +282,24 @@ sys.exit(errs)
|
|
|
219
282
|
" || { EXIT_CODE=1; return; }
|
|
220
283
|
}
|
|
221
284
|
|
|
222
|
-
# ---- Main validation dispatcher
|
|
285
|
+
# ---- Main validation dispatcher ----
|
|
223
286
|
validate_bundle() {
|
|
224
287
|
local file="$1"
|
|
225
288
|
local name; name="$(basename "$file")"
|
|
226
|
-
|
|
227
289
|
local fm
|
|
228
290
|
fm="$(parse_frontmatter "$file")" || {
|
|
229
291
|
printf "${RED}FAIL${NC} %s: cannot parse YAML frontmatter\n" "$name"
|
|
230
292
|
EXIT_CODE=1
|
|
231
293
|
return
|
|
232
294
|
}
|
|
233
|
-
|
|
234
295
|
local kind
|
|
235
296
|
kind="$(echo "$fm" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('okf_kind',''))" 2>/dev/null)"
|
|
236
|
-
|
|
237
297
|
case "$kind" in
|
|
238
298
|
story-metrics) validate_story_metrics "$file" "$fm" "$name" ;;
|
|
239
299
|
spec-migration) validate_spec_migration "$file" "$fm" "$name" ;;
|
|
240
300
|
migration-registry) validate_migration_registry "$file" "$fm" "$name" ;;
|
|
301
|
+
concept) validate_concept "$file" "$fm" "$name" ;;
|
|
302
|
+
verification-report) validate_verification_report "$file" "$fm" "$name" ;;
|
|
241
303
|
"")
|
|
242
304
|
printf "${YELLOW}SKIP${NC} %s: no okf_kind — not an OKF bundle\n" "$name"
|
|
243
305
|
;;
|
|
@@ -247,7 +309,6 @@ validate_bundle() {
|
|
|
247
309
|
esac
|
|
248
310
|
}
|
|
249
311
|
|
|
250
|
-
# ---- Scan directory ----------------------------------------------------
|
|
251
312
|
scan_dir() {
|
|
252
313
|
local dir="$1"
|
|
253
314
|
if [ ! -d "$dir" ]; then
|
|
@@ -267,9 +328,10 @@ scan_dir() {
|
|
|
267
328
|
fi
|
|
268
329
|
}
|
|
269
330
|
|
|
270
|
-
# ---- main
|
|
331
|
+
# ---- main ----
|
|
271
332
|
DIRS=()
|
|
272
333
|
BUNDLE=""
|
|
334
|
+
STAMPLINE="# e45s02 stamp -- concept and verification-report validators added"
|
|
273
335
|
|
|
274
336
|
if [ $# -eq 0 ]; then
|
|
275
337
|
DIRS=("$ROOT/specs/metrics" "$ROOT/specs/migrations")
|
|
@@ -280,32 +342,21 @@ while [ $# -gt 0 ]; do
|
|
|
280
342
|
--dir) DIRS+=("$2"); shift 2 ;;
|
|
281
343
|
--bundle) BUNDLE="$2"; shift 2 ;;
|
|
282
344
|
--help|-h) usage_okf ;;
|
|
283
|
-
*)
|
|
284
|
-
echo "validate-okf: unknown flag: $1" >&2
|
|
285
|
-
echo "Run 'scripts/validate-okf.sh --help' for help." >&2
|
|
286
|
-
exit 2
|
|
287
|
-
;;
|
|
345
|
+
*) echo "validate-okf: unknown flag: $1" >&2; exit 2 ;;
|
|
288
346
|
esac
|
|
289
347
|
done
|
|
290
348
|
|
|
291
349
|
echo "validate-okf: scanning for OKF bundles..."
|
|
292
350
|
|
|
293
351
|
if [ -n "$BUNDLE" ]; then
|
|
294
|
-
|
|
295
|
-
printf "${RED}FAIL${NC} bundle not found: %s\n" "$BUNDLE"
|
|
296
|
-
exit 1
|
|
297
|
-
fi
|
|
352
|
+
[ ! -f "$BUNDLE" ] && { printf "${RED}FAIL${NC} bundle not found: %s\n" "$BUNDLE"; exit 1; }
|
|
298
353
|
validate_bundle "$BUNDLE"
|
|
299
354
|
elif [ ${#DIRS[@]} -gt 0 ]; then
|
|
300
|
-
for d in "${DIRS[@]}"; do
|
|
301
|
-
scan_dir "$d"
|
|
302
|
-
done
|
|
355
|
+
for d in "${DIRS[@]}"; do scan_dir "$d"; done
|
|
303
356
|
else
|
|
304
357
|
echo "validate-okf: nothing to validate (no --dir or --bundle)" >&2
|
|
305
358
|
exit 1
|
|
306
359
|
fi
|
|
307
360
|
|
|
308
|
-
# e41s04: always validate receipts.json
|
|
309
361
|
validate_receipts
|
|
310
|
-
|
|
311
362
|
exit "$EXIT_CODE"
|