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.
- package/.pi/package.json +1 -1
- package/.pi/prompts/gate-trace.md +1 -1
- package/.pi/prompts/maintain-wiki.md +1 -1
- package/.pi/prompts/security-review.md +1 -1
- package/.pi/skills/gate-trace/SKILL.md +1 -1
- package/.pi/skills/maintain-wiki/SKILL.md +1 -1
- package/.pi/skills/security-review/SKILL.md +1 -1
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/scripts/lib/golden-suite-agent.sh +80 -0
- package/scripts/lib/golden-suite-gates.sh +105 -0
- package/scripts/lib/golden-suite-report.sh +157 -0
- package/scripts/lib/golden-suite-run.sh +79 -0
- package/scripts/lib/skill-common.sh +21 -0
- package/scripts/lib/sync-post.sh +162 -0
- package/scripts/lib/sync-render.sh +126 -0
- package/scripts/lib/validate-okf-kinds.sh +272 -0
- package/scripts/okf-add-references.py +79 -0
- package/scripts/run-golden-suite.sh +4 -488
- package/scripts/run-verification-gates.sh +4 -488
- package/scripts/sync-skills.sh +12 -612
- package/scripts/validate-okf.sh +2 -318
|
@@ -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())
|