bigpowers 2.64.2 → 2.65.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.64.2",
3
+ "version": "2.65.1",
4
4
  "description": "74 skills — 73 agent skills synthesizing 17 years of software engineering discipline into a prescriptive methodology for solo developers",
5
5
  "keywords": [
6
6
  "pi-package"
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [2.65.1](https://github.com/danielvm-git/bigpowers/compare/v2.65.0...v2.65.1) (2026-07-05)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **ci:** add workflow_dispatch trigger to docs-site workflow ([eaf2b0b](https://github.com/danielvm-git/bigpowers/commit/eaf2b0b4210b14d3ee6e24a44a208cf4280eaa5e))
7
+
8
+ # [2.65.0](https://github.com/danielvm-git/bigpowers/compare/v2.64.2...v2.65.0) (2026-07-05)
9
+
10
+
11
+ ### Features
12
+
13
+ * OKF Completion — wikis, verification reports, viz graph (e45) ([e04d209](https://github.com/danielvm-git/bigpowers/commit/e04d209c3e70c7e3c9aa2d19606294fde9b4cb23))
14
+
1
15
  ## [2.64.2](https://github.com/danielvm-git/bigpowers/compare/v2.64.1...v2.64.2) (2026-07-05)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bigpowers",
3
- "version": "2.64.2",
3
+ "version": "2.65.1",
4
4
  "description": "73 agent skills synthesizing 17 years of software engineering discipline into a prescriptive methodology for solo developers",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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
- # sync-bugs-registry.sh — rebuild specs/bugs/registry.yaml from BUG-*.md frontmatter
2
+ # story: e45s03
3
+ # sync-bugs-registry.sh — rebuild specs/bugs/registry.yaml + OKF concept bundles
3
4
  set -euo pipefail
4
- REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
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
@@ -1,10 +1,35 @@
1
1
  #!/usr/bin/env bash
2
- # story: e13s03 e28s03
3
- # sync-skills.sh — generate Cursor, Gemini CLI, and pi artifacts from SKILL.md source files
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
- parse_frontmatter "$skill_md" || continue
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/"/\\"/g')
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
@@ -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, and migration-registry bundles.
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 <<'EOF'
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
- EOF
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
- # ---- Receipts validation (e41s04) ----------------------------------------
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
- if [ ! -f "$BUNDLE" ]; then
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"