bigpowers 2.73.3 → 2.73.5

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.
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env bash
2
+ # story: e13s03 e28s03
3
+ # Post-sync pipeline for sync-skills.sh — manifests, lockfiles, guards, OKF wikis.
4
+ # Args: $1=skill_count $2=opencode_count (optional, default 0)
5
+
6
+ if [ -n "${SYNC_POST_LOADED:-}" ]; then
7
+ return 0
8
+ fi
9
+ SYNC_POST_LOADED=1
10
+
11
+ sync_post_run() {
12
+ local skill_count="${1:-0}"
13
+ local opencode_count="${2:-0}"
14
+ local pkg_version pkg_desc readme readme_tmp trace_mdc manifest ext_ver pkg_ver validate_script
15
+
16
+ pkg_version=$(jq -r '.version' "$REPO_ROOT/package.json")
17
+ pkg_desc=$(jq -r '.description' "$REPO_ROOT/package.json")
18
+
19
+ jq -n --arg name "bigpowers" \
20
+ --arg version "$pkg_version" \
21
+ --arg desc "${skill_count} skills — ${pkg_desc}" \
22
+ '{name: $name, version: $version, description: $desc}' > "$GEMINI_MANIFEST"
23
+
24
+ jq -n --arg version "$pkg_version" \
25
+ --arg desc "${skill_count} skills — ${pkg_desc}" \
26
+ '{
27
+ "name": "bigpowers",
28
+ "version": $version,
29
+ "description": $desc,
30
+ "keywords": ["pi-package"],
31
+ "pi": {
32
+ "skills": ["./skills"],
33
+ "prompts": ["./prompts"]
34
+ }
35
+ }' > "$PI_PACKAGE_JSON"
36
+
37
+ {
38
+ echo "{"
39
+ echo " \"\$schema\": \"https://opencode.ai/config.json\","
40
+ echo " \"instructions\": [\"CLAUDE.md\", \"CONVENTIONS.md\"]"
41
+ echo "}"
42
+ } > "$REPO_ROOT/opencode.json"
43
+
44
+ if [[ -x "$REPO_ROOT/scripts/regenerate-lockfile.sh" ]]; then
45
+ bash "$REPO_ROOT/scripts/regenerate-lockfile.sh" || {
46
+ echo "sync-skills: FAIL — lockfile regeneration failed" >&2
47
+ exit 1
48
+ }
49
+ fi
50
+
51
+ if [[ -x "$REPO_ROOT/scripts/generate-skill-index.sh" ]]; then
52
+ bash "$REPO_ROOT/scripts/generate-skill-index.sh" || {
53
+ echo "sync-skills: FAIL — SKILL-INDEX.md generation failed" >&2
54
+ exit 1
55
+ }
56
+ fi
57
+
58
+ if [[ -x "$REPO_ROOT/scripts/build-skill-index.sh" ]]; then
59
+ bash "$REPO_ROOT/scripts/build-skill-index.sh" || true
60
+ fi
61
+
62
+ local CURSOR_KEEP="context7.mdc" mdc mdc_base
63
+ for mdc in "$CURSOR_RULES"/*.mdc; do
64
+ [[ -e "$mdc" ]] || continue
65
+ mdc_base=$(basename "$mdc")
66
+ case " $CURSOR_KEEP " in *" $mdc_base "*) continue ;; esac
67
+ if [[ ! -f "$SKILLS_ROOT/${mdc_base%.mdc}/SKILL.md" ]]; then
68
+ rm "$mdc"
69
+ echo " → pruned orphan cursor rule: $mdc_base"
70
+ fi
71
+ done
72
+
73
+ readme="$REPO_ROOT/README.md"
74
+ if [[ -f "$readme" ]] && grep -q 'badge/skills-' "$readme"; then
75
+ readme_tmp=$(mktemp)
76
+ sed -E "s|(badge/skills-)[0-9]+(-brightgreen)|\1${skill_count}\2|" "$readme" > "$readme_tmp"
77
+ mv "$readme_tmp" "$readme"
78
+ fi
79
+
80
+ echo "sync-skills: $skill_count skills synced"
81
+ echo " → .cursor/rules/ ($skill_count .mdc files)"
82
+ echo " → .gemini/extensions/bigpowers/skills/ (Agent Skills)"
83
+ echo " → .gemini/extensions/bigpowers/commands/ (Slash Commands)"
84
+ echo " → .gemini/extensions/bigpowers/gemini-extension.json"
85
+ echo " → .pi/skills/ ($skill_count skill dirs — pi Agent Skills)"
86
+ echo " → .pi/prompts/ ($skill_count prompt templates — pi slash commands)"
87
+ echo " → .pi/package.json (pi package manifest)"
88
+ echo " → skills-lock.json (catalog with SHA-256 hashes)"
89
+ echo " → SKILL-INDEX.md (auto-generated skill reference)"
90
+ echo " → opencode.json (CLAUDE.md + CONVENTIONS.md instructions)"
91
+ [[ -n "${OPN_TARGET:-}" ]] && echo " → bigpowers-opencode: $opencode_count skills"
92
+
93
+ sync_post_regression_guards
94
+
95
+ if [[ -d "$REPO_ROOT/.git" ]]; then
96
+ bash "$REPO_ROOT/scripts/generate-reference-tables.sh"
97
+ fi
98
+
99
+ echo ""
100
+ echo "Generating OKF wikis..."
101
+ bash "$REPO_ROOT/scripts/generate-epics-wiki.sh"
102
+ bash "$REPO_ROOT/scripts/generate-adr-wiki.sh"
103
+ echo "sync-skills: OKF wikis regenerated"
104
+ }
105
+
106
+ sync_post_regression_guards() {
107
+ local trace_mdc manifest ext_ver pkg_ver validate_script
108
+
109
+ trace_mdc="$REPO_ROOT/.cursor/rules/trace-requirement.mdc"
110
+ if [[ -f "$trace_mdc" ]] && ! grep -q 'release-plan.yaml + epic' "$trace_mdc"; then
111
+ echo "sync-skills: FAIL — '+' missing from trace-requirement; use sed -E for whitespace collapse" >&2
112
+ exit 1
113
+ fi
114
+
115
+ manifest="$REPO_ROOT/.gemini/extensions/bigpowers/gemini-extension.json"
116
+ if [[ -f "$manifest" ]]; then
117
+ ext_ver=$(jq -r '.version // empty' "$manifest")
118
+ pkg_ver=$(jq -r '.version // empty' "$REPO_ROOT/package.json")
119
+ if [[ -n "$ext_ver" && -n "$pkg_ver" && "$ext_ver" != "$pkg_ver" ]]; then
120
+ echo "sync-skills: FAIL — gemini-extension.json version ($ext_ver) != package.json ($pkg_ver)" >&2
121
+ exit 1
122
+ fi
123
+ fi
124
+
125
+ validate_script="$REPO_ROOT/scripts/validate-skill-yaml.py"
126
+ if [[ -f "$validate_script" ]] && command -v $PYTHON &>/dev/null; then
127
+ if ! $PYTHON "$validate_script" > /dev/null 2>&1; then
128
+ echo "sync-skills: FAIL — YAML frontmatter validation failed" >&2
129
+ $PYTHON "$validate_script" >&2
130
+ exit 1
131
+ fi
132
+ fi
133
+
134
+ if grep -rn '^declare -A\|^[[:space:]]*declare -A' \
135
+ scripts/sync-skills.sh scripts/generate-skill-index.sh \
136
+ scripts/regenerate-lockfile.sh scripts/build-skill-index.sh 2>/dev/null; then
137
+ echo "sync-skills: FAIL — bash 4+ features detected in install-chain scripts (not macOS-compatible)" >&2
138
+ exit 1
139
+ fi
140
+ }
141
+
142
+ sync_opencode_copy() {
143
+ local opencode_count=0 skill_dir skill_md skill_name
144
+ SYNC_OPENCODE_COUNT=0
145
+
146
+ [[ -z "${OPN_TARGET:-}" || ! -d "$OPN_TARGET" ]] && return 0
147
+
148
+ echo ""
149
+ echo "Syncing skills to opencode repo: $OPN_TARGET"
150
+ OPN_SKILLS="$OPN_TARGET/skills"
151
+ mkdir -p "$OPN_SKILLS"
152
+ for skill_dir in "$SKILLS_ROOT"/*/; do
153
+ skill_md="$skill_dir/SKILL.md"
154
+ [[ -f "$skill_md" ]] || continue
155
+ skill_name=$(basename "$skill_dir")
156
+ mkdir -p "$OPN_SKILLS/$skill_name"
157
+ cp "$skill_md" "$OPN_SKILLS/$skill_name/SKILL.md"
158
+ opencode_count=$((opencode_count + 1))
159
+ done
160
+ echo " → $opencode_count skills copied to $OPN_SKILLS/"
161
+ SYNC_OPENCODE_COUNT=$opencode_count
162
+ }
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env bash
2
+ # story: e13s03 e39s04
3
+ # Render helpers for sync-skills.sh — build_body + per-target render_* functions.
4
+ # Source after skill-common.sh; expects CURSOR_RULES, GEMINI_*, PI_*, OKF_WIKI_* globals.
5
+
6
+ if [ -n "${SYNC_RENDER_LOADED:-}" ]; then
7
+ return 0
8
+ fi
9
+ SYNC_RENDER_LOADED=1
10
+
11
+ build_body() {
12
+ local skill_md="$1"
13
+ local skill_dir="$2"
14
+ local body extra_md
15
+
16
+ body=$(awk '/^---/{f++; next} f>=2{print}' "$skill_md")
17
+ for extra_md in $(find "$skill_dir" -maxdepth 1 -name "*.md" ! -name "SKILL.md" | LC_ALL=C sort); do
18
+ body="$body"$'\n\n'"---"$'\n\n'"$(cat "$extra_md")"
19
+ done
20
+ echo "$body" | grep -v 'disable-model-invocation'
21
+ }
22
+
23
+ render_cursor() {
24
+ local cursor_file="$CURSOR_RULES/$IR_NAME.mdc"
25
+ {
26
+ echo "---"
27
+ echo "description: \"$IR_DESC_ESCAPED\""
28
+ echo "alwaysApply: false"
29
+ echo "---"
30
+ echo ""
31
+ echo "$IR_BODY"
32
+ } > "$cursor_file"
33
+ }
34
+
35
+ render_gemini_skill() {
36
+ mkdir -p "$GEMINI_SKILLS/$IR_NAME"
37
+ {
38
+ echo "---"
39
+ echo "name: $IR_NAME"
40
+ echo "description: \"$IR_DESC_ESCAPED\""
41
+ echo "---"
42
+ echo ""
43
+ echo "$IR_BODY"
44
+ } > "$GEMINI_SKILLS/$IR_NAME/SKILL.md"
45
+ }
46
+
47
+ render_gemini_command() {
48
+ mkdir -p "$GEMINI_COMMANDS/prompts"
49
+ local prompt_file="commands/prompts/$IR_NAME.md"
50
+ echo "$IR_BODY" > "$GEMINI_EXT_DIR/$prompt_file"
51
+ {
52
+ echo "description = \"$IR_DESC_ESCAPED\""
53
+ echo "prompt = \"@{$prompt_file}\""
54
+ } > "$GEMINI_COMMANDS/$IR_NAME.toml"
55
+ }
56
+
57
+ render_pi_skill() {
58
+ mkdir -p "$PI_SKILLS/$IR_NAME"
59
+ {
60
+ echo "---"
61
+ echo "name: $IR_NAME"
62
+ echo "description: \"$IR_DESC_ESCAPED\""
63
+ [[ -n "$IR_MODEL" ]] && echo "model: $IR_MODEL"
64
+ echo "---"
65
+ echo ""
66
+ echo "$IR_BODY"
67
+ } > "$PI_SKILLS/$IR_NAME/SKILL.md"
68
+ }
69
+
70
+ render_pi_prompt() {
71
+ {
72
+ echo "---"
73
+ echo "description: $IR_DESCRIPTION"
74
+ echo "---"
75
+ echo ""
76
+ echo "$IR_BODY"
77
+ } > "$PI_PROMPTS/$IR_NAME.md"
78
+ }
79
+
80
+ render_okf_concept() {
81
+ mkdir -p "$OKF_WIKI_SKILLS"
82
+ local okf_file="$OKF_WIKI_SKILLS/$IR_NAME.md"
83
+ local phase="utility" title desc_escaped short_desc effort_val="${_PF_EFFORT:-standard}"
84
+
85
+ case "$IR_NAME" in
86
+ survey-context|elaborate-spec|model-domain|define-language|\
87
+ design-interface|plan-work|plan-release|slice-tasks|\
88
+ kickoff-branch|develop-tdd|verify-work|audit-code|\
89
+ commit-message|release-branch|security-review|\
90
+ assess-impact|investigate-bug|validate-fix|dispatch-agents|\
91
+ request-review|respond-review|hook-commits|guard-git|\
92
+ wire-observability|wire-ci|smoke-test|deploy|enforce-first)
93
+ phase="critical-path" ;;
94
+ esac
95
+
96
+ title=$(echo "$IR_NAME" | sed -E 's/-/ /g; s/\b(.)/\U\1/g')
97
+ desc_escaped=$(echo "$IR_DESCRIPTION" | sed 's/\\/\\\\/g; s/"/\\"/g')
98
+ short_desc="$IR_DESCRIPTION"
99
+ [[ ${#short_desc} -gt 200 ]] && short_desc="${short_desc:0:197}..."
100
+
101
+ {
102
+ echo "---"
103
+ echo "okf_kind: concept"
104
+ echo "type: Skill"
105
+ echo "id: $IR_NAME"
106
+ echo "title: \"$title\""
107
+ echo "name: $IR_NAME"
108
+ echo "category: skills"
109
+ [[ -n "$IR_MODEL" ]] && echo "model: $IR_MODEL"
110
+ echo "effort: $effort_val"
111
+ echo "phase: $phase"
112
+ echo "description: \"$desc_escaped\""
113
+ echo "references: []"
114
+ echo "---"
115
+ echo ""
116
+ echo "# $title"
117
+ echo ""
118
+ echo "**Phase:** $phase"
119
+ [[ -n "$IR_MODEL" ]] && echo "**Model:** $IR_MODEL"
120
+ echo "**Effort:** $effort_val"
121
+ echo ""
122
+ echo "$IR_DESCRIPTION"
123
+ echo ""
124
+ echo "> Auto-generated by sync-skills.sh --okf (e39s04) from $SKILLS_ROOT/$IR_NAME/SKILL.md"
125
+ } > "$okf_file"
126
+ }
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env bash
2
+ # story: e38s09 e41s04 e45s02
3
+ # OKF kind-specific validators — sourced by validate-okf.sh
4
+
5
+ if [ -n "${VALIDATE_OKF_KINDS_LOADED:-}" ]; then return 0; fi
6
+ VALIDATE_OKF_KINDS_LOADED=1
7
+
8
+ val_nullish() { [ -z "$1" ] || [ "$1" = "null" ] || [ "$1" = "None" ]; }
9
+ val_nullish_or_empty() { val_nullish "$1" || [ "$1" = "[]" ]; }
10
+
11
+ okf_parse_frontmatter() {
12
+ local file="$1"
13
+ $PYTHON -c "
14
+ import yaml, sys, json
15
+
16
+ class SafeEncoder(json.JSONEncoder):
17
+ def default(self, obj):
18
+ try:
19
+ return super().default(obj)
20
+ except TypeError:
21
+ return str(obj)
22
+
23
+ content = open('$file').read()
24
+ parts = content.split('---')
25
+ if len(parts) < 3:
26
+ sys.exit(1)
27
+ try:
28
+ fm = yaml.safe_load(parts[1])
29
+ if fm is None:
30
+ sys.exit(1)
31
+ print(json.dumps(fm, cls=SafeEncoder))
32
+ except Exception as e:
33
+ print(f'YAML parse error: {e}', file=sys.stderr)
34
+ sys.exit(1)
35
+ " 2>/dev/null
36
+ }
37
+
38
+ validate_story_metrics() {
39
+ local file="$1" fm="$2" name="$3" errs=0 key val source generator commit_range eff
40
+ for key in id epic bcps commit_range source generated_at generator; do
41
+ val="$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null)"
42
+ if val_nullish "$val"; then
43
+ printf "${RED}FAIL${NC} %s: missing required key '%s'\n" "$name" "$key"
44
+ errs=$((errs + 1))
45
+ fi
46
+ done
47
+ [ "$errs" -gt 0 ] && { EXIT_CODE=1; return; }
48
+
49
+ source="$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('source',''))" 2>/dev/null)"
50
+ if ! echo "measured estimated backfilled" | grep -qw "$source"; then
51
+ printf "${RED}FAIL${NC} %s: invalid source '%s' — must be measured|estimated|backfilled\n" "$name" "$source"
52
+ EXIT_CODE=1; return
53
+ fi
54
+
55
+ generator="$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('generator',''))" 2>/dev/null)"
56
+ if [ "$generator" != "scripts/record-cycle-time.sh" ]; then
57
+ printf "${RED}FAIL${NC} %s: wrong generator '%s' — expected scripts/record-cycle-time.sh\n" "$name" "$generator"
58
+ EXIT_CODE=1; return
59
+ fi
60
+
61
+ commit_range="$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('commit_range',''))" 2>/dev/null)"
62
+ if ! git -C "$ROOT" log --oneline -1 "$commit_range" >/dev/null 2>&1; then
63
+ printf "${RED}FAIL${NC} %s: commit_range '%s' does not resolve\n" "$name" "$commit_range"
64
+ EXIT_CODE=1; return
65
+ fi
66
+
67
+ eff="$(echo "$fm" | $PYTHON -c "
68
+ import json,sys
69
+ d=json.load(sys.stdin)
70
+ e=d.get('effort',{}).get('effort_hours')
71
+ if e is None: sys.exit(1)
72
+ if e < 0: sys.exit(2)
73
+ print(e)
74
+ " 2>/dev/null)" || {
75
+ local rc=$?
76
+ if [ "$rc" -eq 1 ]; then printf "${RED}FAIL${NC} %s: effort.effort_hours missing or null\n" "$name"
77
+ else printf "${RED}FAIL${NC} %s: effort_hours out of bounds\n" "$name"; fi
78
+ EXIT_CODE=1; return
79
+ }
80
+ printf "${GREEN}PASS${NC} %s (source=%s, generator=%s, commit_range resolves)\n" "$name" "$source" "$generator"
81
+ }
82
+
83
+ validate_spec_migration() {
84
+ local file="$1" fm="$2" name="$3" errs=0 key val fp_count
85
+ for key in id title since_version order actions_needed; do
86
+ val="$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null)"
87
+ if val_nullish_or_empty "$val"; then
88
+ printf "${RED}FAIL${NC} %s: missing required key '%s'\n" "$name" "$key"
89
+ errs=$((errs + 1))
90
+ fi
91
+ done
92
+ [ "$errs" -gt 0 ] && { EXIT_CODE=1; return; }
93
+
94
+ fp_count="$(echo "$fm" | $PYTHON -c "
95
+ import json,sys
96
+ d=json.load(sys.stdin)
97
+ fp=d.get('fingerprint',{}).get('any',[])
98
+ print(len(fp))
99
+ " 2>/dev/null)"
100
+ if [ "${fp_count:-0}" -eq 0 ]; then
101
+ printf "${RED}FAIL${NC} %s: fingerprint.any is empty — no detection criteria\n" "$name"
102
+ EXIT_CODE=1; return
103
+ fi
104
+
105
+ printf "${GREEN}PASS${NC} %s (actions=%s, fingerprint has %s check(s))\n" \
106
+ "$name" \
107
+ "$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('actions_needed',[])))" 2>/dev/null)" \
108
+ "$fp_count"
109
+ }
110
+
111
+ validate_migration_registry() {
112
+ local file="$1" fm="$2" name="$3" count bad
113
+ count="$(echo "$fm" | $PYTHON -c "
114
+ import json,sys
115
+ d=json.load(sys.stdin)
116
+ print(len(d.get('migrations',[])))
117
+ " 2>/dev/null)"
118
+ if [ "${count:-0}" -eq 0 ]; then
119
+ printf "${RED}FAIL${NC} %s: migrations list is empty\n" "$name"
120
+ EXIT_CODE=1; return
121
+ fi
122
+
123
+ bad="$(echo "$fm" | $PYTHON -c "
124
+ import json,sys
125
+ d=json.load(sys.stdin)
126
+ for m in d.get('migrations',[]):
127
+ if not all(k in m for k in ('id','file','status')):
128
+ print(m.get('id','?')); sys.exit(1)
129
+ " 2>/dev/null)" || {
130
+ printf "${RED}FAIL${NC} %s: migration '%s' missing id/file/status\n" "$name" "${bad:-?}"
131
+ EXIT_CODE=1; return
132
+ }
133
+ printf "${GREEN}PASS${NC} %s (%s migrations indexed)\n" "$name" "$count"
134
+ }
135
+
136
+ validate_concept() {
137
+ local file="$1" fm="$2" name="$3" errs=0 key val ref_count cat
138
+ for key in id title category; do
139
+ val="$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null)"
140
+ if val_nullish "$val"; then
141
+ printf "${RED}FAIL${NC} %s: missing required key '%s'\n" "$name" "$key"
142
+ errs=$((errs + 1))
143
+ fi
144
+ done
145
+ ref_count="$(echo "$fm" | $PYTHON -c "
146
+ import json,sys
147
+ d=json.load(sys.stdin)
148
+ r=d.get('references',[])
149
+ print(len(r) if isinstance(r,list) else 0)
150
+ " 2>/dev/null)"
151
+ if [ "${ref_count:-0}" -eq 0 ]; then
152
+ printf "${RED}FAIL${NC} %s: references[] is empty or missing\n" "$name"
153
+ errs=$((errs + 1))
154
+ fi
155
+ [ "$errs" -gt 0 ] && { EXIT_CODE=1; return; }
156
+ cat="$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('category',''))" 2>/dev/null)"
157
+ printf "${GREEN}PASS${NC} %s (category=%s, %s reference(s))\n" "$name" "$cat" "$ref_count"
158
+ }
159
+
160
+ validate_verification_report() {
161
+ local file="$1" fm="$2" name="$3" errs=0 key val gs score rc
162
+ for key in score gate_status threshold total_pass total_fail generated_by; do
163
+ val="$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('$key',''))" 2>/dev/null)"
164
+ if val_nullish "$val"; then
165
+ printf "${RED}FAIL${NC} %s: missing required key '%s'\n" "$name" "$key"
166
+ errs=$((errs + 1))
167
+ fi
168
+ done
169
+ [ "$errs" -gt 0 ] && { EXIT_CODE=1; return; }
170
+
171
+ gs="$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('gate_status',''))" 2>/dev/null)"
172
+ if ! echo "pass fail concerns waived" | grep -qw "$gs"; then
173
+ printf "${RED}FAIL${NC} %s: invalid gate_status '%s' — must be pass|fail|concerns|waived\n" "$name" "$gs"
174
+ EXIT_CODE=1; return
175
+ fi
176
+
177
+ score="$(echo "$fm" | $PYTHON -c "
178
+ import json,sys
179
+ d=json.load(sys.stdin)
180
+ s=d.get('score')
181
+ if s is None or not isinstance(s,(int,float)):
182
+ sys.exit(1)
183
+ if s < 0 or s > 100:
184
+ sys.exit(2)
185
+ print(s)
186
+ " 2>/dev/null)" || {
187
+ rc=$?
188
+ if [ "$rc" -eq 1 ]; then printf "${RED}FAIL${NC} %s: score missing or not numeric\n" "$name"
189
+ else printf "${RED}FAIL${NC} %s: score out of bounds (0-100)\n" "$name"; fi
190
+ EXIT_CODE=1; return
191
+ }
192
+
193
+ printf "${GREEN}PASS${NC} %s (score=%.1f, gate=%s, threshold=%.1f)\n" \
194
+ "$name" "$score" "$gs" \
195
+ "$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('threshold',0))" 2>/dev/null)"
196
+ }
197
+
198
+ validate_receipts() {
199
+ local f="$ROOT/specs/receipts.json"
200
+ if [ ! -f "$f" ]; then printf "${YELLOW}SKIP${NC} receipts.json not found\n"; return; fi
201
+ $PYTHON -c "
202
+ import json, sys
203
+ d=json.load(open('$f'))
204
+ vs={'measured','estimated','backfilled','absent'}
205
+ ss=('compliance','golden_suite','metrics','traceability')
206
+ errs=0
207
+ for sec in ss:
208
+ data=d.get(sec)
209
+ if data is None: continue
210
+ src=data.get('source','')
211
+ if not src: print(f'FAIL: receipts.json — {sec}: missing source tag'); errs+=1; continue
212
+ if src not in vs: print(f'FAIL: receipts.json — {sec}: unknown source \"{src}\"'); errs+=1; continue
213
+ hv='value' in data
214
+ if src=='absent' and hv: print(f'FAIL: receipts.json — {sec}: tagged absent but carries value'); errs+=1
215
+ elif src!='absent' and not hv: print(f'FAIL: receipts.json — {sec}: tagged {src} but missing value'); errs+=1
216
+ if errs==0: print('PASS receipts.json')
217
+ else: print(f'{errs} violation(s) in receipts.json')
218
+ sys.exit(errs)
219
+ " || { EXIT_CODE=1; return; }
220
+ }
221
+
222
+ validate_bundle() {
223
+ local file="$1" name fm fm_type kind
224
+ name="$(basename "$file")"
225
+ fm="$(okf_parse_frontmatter "$file")" || {
226
+ printf "${RED}FAIL${NC} %s: cannot parse YAML frontmatter\n" "$name"
227
+ EXIT_CODE=1
228
+ return
229
+ }
230
+
231
+ fm_type="$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('type',''))" 2>/dev/null)"
232
+ if [ -z "$fm_type" ]; then
233
+ printf "${RED}FAIL${NC} %s: OKF v0.1 — missing required 'type' field\n" "$name"
234
+ EXIT_CODE=1
235
+ return
236
+ fi
237
+
238
+ if [ "$name" = "index.md" ] && [ "$fm_type" != "Index" ]; then
239
+ printf "${RED}FAIL${NC} %s: OKF v0.1 — index.md must have type: Index (got '%s')\n" "$name" "$fm_type"
240
+ EXIT_CODE=1
241
+ return
242
+ fi
243
+
244
+ kind="$(echo "$fm" | $PYTHON -c "import json,sys; d=json.load(sys.stdin); print(d.get('okf_kind',''))" 2>/dev/null)"
245
+ case "$kind" in
246
+ story-metrics) validate_story_metrics "$file" "$fm" "$name" ;;
247
+ spec-migration) validate_spec_migration "$file" "$fm" "$name" ;;
248
+ migration-registry) validate_migration_registry "$file" "$fm" "$name" ;;
249
+ concept) validate_concept "$file" "$fm" "$name" ;;
250
+ verification-report) validate_verification_report "$file" "$fm" "$name" ;;
251
+ "") printf "${YELLOW}SKIP${NC} %s: no okf_kind — not an OKF bundle\n" "$name" ;;
252
+ *) printf "${YELLOW}SKIP${NC} %s: unknown okf_kind '%s'\n" "$name" "$kind" ;;
253
+ esac
254
+ }
255
+
256
+ okf_scan_dir() {
257
+ local dir="$1" count=0 f
258
+ if [ ! -d "$dir" ]; then
259
+ printf "${RED}FAIL${NC} directory not found: %s\n" "$dir"
260
+ EXIT_CODE=1
261
+ return
262
+ fi
263
+ while IFS= read -r -d '' f; do
264
+ if head -1 "$f" 2>/dev/null | grep -q '^---$'; then
265
+ validate_bundle "$f"
266
+ count=$((count + 1))
267
+ fi
268
+ done < <(find "$dir" -maxdepth 1 -name '*.md' -print0 2>/dev/null || true)
269
+ if [ "$count" -eq 0 ]; then
270
+ printf "${YELLOW}SKIP${NC} no OKF bundles found in %s\n" "$dir"
271
+ fi
272
+ }
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ # story: e39s04
3
+ """Add cross-references from skill-graph.json into OKF concept files."""
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import re
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ def main() -> int:
13
+ graph_json = Path(sys.argv[1])
14
+ wiki_skills = Path(sys.argv[2])
15
+
16
+ with open(graph_json, encoding="utf-8") as f:
17
+ graph = json.load(f)
18
+
19
+ edges = graph.get("edges", [])
20
+ nodes = {n.get("id") or n.get("name") for n in graph.get("nodes", [])}
21
+ nodes.discard(None)
22
+
23
+ refs: dict[str, list[dict[str, str]]] = {}
24
+ for edge in edges:
25
+ src = edge.get("from") or edge.get("source")
26
+ tgt = edge.get("to") or edge.get("target")
27
+ rel = edge.get("relationType") or edge.get("relation") or "references"
28
+ if src in nodes and tgt in nodes:
29
+ refs.setdefault(src, []).append({"concept": tgt, "type": rel})
30
+
31
+ skill_names = {f.stem for f in wiki_skills.glob("*.md")}
32
+ count_updated = 0
33
+
34
+ for path in wiki_skills.glob("*.md"):
35
+ name = path.stem
36
+ content = path.read_text(encoding="utf-8")
37
+ concept_refs = list(refs.get(name, []))
38
+
39
+ desc_match = re.search(r'description:\s*"(.*?)"', content, re.DOTALL)
40
+ if desc_match:
41
+ desc = desc_match.group(1)
42
+ for skill_name in skill_names:
43
+ if (
44
+ skill_name != name
45
+ and skill_name in desc
46
+ and not any(r["concept"] == skill_name for r in concept_refs)
47
+ ):
48
+ concept_refs.append({"concept": skill_name, "type": "references"})
49
+
50
+ seen: set[tuple[str, str]] = set()
51
+ unique_refs: list[dict[str, str]] = []
52
+ for ref in concept_refs:
53
+ key = (ref["concept"], ref["type"])
54
+ if key not in seen:
55
+ seen.add(key)
56
+ unique_refs.append(ref)
57
+
58
+ if unique_refs:
59
+ ref_lines = []
60
+ for ref in unique_refs:
61
+ ref_lines.append(f' - concept: {ref["concept"]}')
62
+ ref_lines.append(f' type: {ref["type"]}')
63
+ ref_block = "\n".join(ref_lines)
64
+ content = re.sub(r"references:\s*\[\]", f"references:\n{ref_block}", content)
65
+ else:
66
+ content = re.sub(
67
+ r"references:\s*\[\]",
68
+ "references:\n - concept: skills-wiki\n type: belongs_to",
69
+ content,
70
+ )
71
+ count_updated += 1
72
+ path.write_text(content, encoding="utf-8")
73
+
74
+ print(f" concepts with cross-references: {count_updated}", file=sys.stderr)
75
+ return 0
76
+
77
+
78
+ if __name__ == "__main__":
79
+ raise SystemExit(main())