bigpowers 2.65.1 → 2.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.fix_regex.sh +16 -0
- package/.hermes-verify-sed-fix.py +53 -0
- package/.pi/package.json +2 -2
- package/.pi/prompts/build-epic.md +8 -0
- package/.pi/prompts/gate-trace.md +2 -1
- package/.pi/prompts/kickoff-branch.md +19 -0
- package/.pi/prompts/maintain-wiki.md +63 -0
- package/.pi/prompts/plan-tests.md +1 -1
- package/.pi/prompts/release-branch.md +17 -13
- package/.pi/prompts/verify-work.md +9 -5
- package/.pi/skills/build-epic/SKILL.md +8 -0
- package/.pi/skills/gate-trace/SKILL.md +2 -1
- package/.pi/skills/kickoff-branch/SKILL.md +19 -0
- package/.pi/skills/maintain-wiki/SKILL.md +65 -0
- package/.pi/skills/plan-tests/SKILL.md +1 -1
- package/.pi/skills/release-branch/SKILL.md +17 -13
- package/.pi/skills/verify-work/SKILL.md +9 -5
- package/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/SKILL-INDEX.md +1 -1
- package/_test_e28s01.sh +2 -0
- package/hermes-verify-sed-fix.py +30 -0
- package/package.json +1 -1
- package/scripts/build-skill-graph.sh +151 -183
- package/scripts/check-spec-drift.sh +121 -0
- package/scripts/check-spec-version-gap.sh +143 -0
- package/scripts/decompose-conventions.sh +147 -0
- package/scripts/generate-agent-guide.sh +89 -0
- package/scripts/hermes-verify-e39s02.sh +28 -0
- package/scripts/migrate-version.sh +721 -0
- package/scripts/okf-post-sync.sh +163 -0
- package/scripts/run-golden-suite.sh +235 -27
- package/scripts/validate-okf.sh +18 -0
- package/skills/build-epic/SKILL.md +8 -0
- package/skills/gate-trace/SKILL.md +2 -1
- package/skills/kickoff-branch/SKILL.md +19 -0
- package/skills/maintain-wiki/SKILL.md +66 -0
- package/skills/plan-tests/SKILL.md +1 -1
- package/skills/release-branch/SKILL.md +17 -13
- package/skills/verify-work/SKILL.md +9 -5
- package/skills-lock.json +11 -6
- package/tests/fixtures/v1.x-project/CLAUDE.md +4 -0
- package/tests/fixtures/v1.x-project/CONVENTIONS.md +4 -0
- package/tests/fixtures/v2.0.0-project/CLAUDE.md +4 -0
- package/tests/fixtures/v2.0.0-project/CONVENTIONS.md +3 -0
- package/tests/fixtures/v2.20-project/CLAUDE.md +4 -0
- package/tests/fixtures/v2.20-project/CONVENTIONS.md +4 -0
- package/tests/run-golden-migrate-version.sh +155 -0
|
@@ -1,226 +1,194 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# story: e39s01
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# Supersedes standalone SKILL.md parsing approach (2026-07-04).
|
|
7
|
-
# Graph must be built first: run MCP tool `build_skill_graph` or
|
|
8
|
-
# `node -e "..."` to generate bigpowers-mcp/graph.jsonl.
|
|
9
|
-
|
|
3
|
+
# Consume e32 build_skill_graph MCP output (bigpowers-mcp/graph.jsonl) and emit:
|
|
4
|
+
# - specs/skill-graph.json — machine-readable dependency graph
|
|
5
|
+
# - specs/skills-wiki/ — OKF wiki with per-skill pages
|
|
10
6
|
set -euo pipefail
|
|
11
7
|
|
|
12
8
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
9
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
GRAPH_JSONL="$REPO_ROOT/bigpowers-mcp/graph.jsonl"
|
|
11
|
+
SKILL_GRAPH_JSON="$REPO_ROOT/specs/skill-graph.json"
|
|
12
|
+
SKILLS_WIKI="$REPO_ROOT/specs/skills-wiki/skills"
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
if [[ "${1:-}" == "--help" ]]; then
|
|
14
|
+
show_help() {
|
|
20
15
|
cat <<EOF
|
|
21
|
-
Usage:
|
|
16
|
+
Usage: $(basename "$0") [OPTION]
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
specs/
|
|
26
|
-
enforces, handoff_to)
|
|
27
|
-
specs/skills-wiki/ — one OKF concept markdown file per skill
|
|
18
|
+
Consume bigpowers-mcp/graph.jsonl and emit:
|
|
19
|
+
- specs/skill-graph.json — full skill dependency graph
|
|
20
|
+
- specs/skills-wiki/skills/*.md — per-skill OKF wiki pages
|
|
28
21
|
|
|
29
22
|
Options:
|
|
30
|
-
--help Show this
|
|
31
|
-
--
|
|
32
|
-
--wiki Write only specs/skills-wiki/ (skip JSON graph)
|
|
33
|
-
|
|
34
|
-
Requires: bigpowers-mcp/graph.jsonl (run build_skill_graph MCP tool first)
|
|
23
|
+
--help Show this help and exit
|
|
24
|
+
--wiki Regenerate only the skills-wiki OKF bundle (skip graph.json)
|
|
35
25
|
EOF
|
|
36
|
-
|
|
37
|
-
fi
|
|
26
|
+
}
|
|
38
27
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
both) DO_JSON=1; DO_WIKI=1 ;;
|
|
44
|
-
*) echo "Unknown option: $MODE" >&2; exit 1 ;;
|
|
45
|
-
esac
|
|
46
|
-
|
|
47
|
-
# Pre-flight: graph.jsonl must exist
|
|
48
|
-
if [[ ! -f "$GRAPH_IN" ]]; then
|
|
49
|
-
cat >&2 <<EOF
|
|
50
|
-
ERROR: $GRAPH_IN not found.
|
|
51
|
-
|
|
52
|
-
The skill graph has not been built yet. Run the bigpowers-mcp
|
|
53
|
-
build_skill_graph tool to generate it:
|
|
54
|
-
|
|
55
|
-
# Option A: via MCP inspector
|
|
56
|
-
npx @modelcontextprotocol/inspector node bigpowers-mcp/build/index.js
|
|
57
|
-
# Then call build_skill_graph
|
|
58
|
-
|
|
59
|
-
# Option B: via Node.js directly
|
|
60
|
-
node -e "
|
|
61
|
-
const {discoverSkills,readSkillFile}=require('./bigpowers-mcp/build/lib/skill-parser.js');
|
|
62
|
-
const {buildGraphFromSkills}=require('./bigpowers-mcp/build/lib/graph/builder.js');
|
|
63
|
-
const {saveGraph}=require('./bigpowers-mcp/build/lib/graph/store.js');
|
|
64
|
-
const skills=discoverSkills(process.cwd(),()=>'');
|
|
65
|
-
const parsed=skills.map(s=>readSkillFile(process.cwd(),s.name));
|
|
66
|
-
saveGraph('bigpowers-mcp/graph.jsonl', buildGraphFromSkills(parsed));
|
|
67
|
-
"
|
|
68
|
-
EOF
|
|
28
|
+
# --- Error handling ---
|
|
29
|
+
missing_graph() {
|
|
30
|
+
echo "[ERROR] MCP graph not found at: $GRAPH_JSONL" >&2
|
|
31
|
+
echo "[ERROR] Run 'cd bigpowers-mcp && npm start' or e32s02 MCP tool first" >&2
|
|
69
32
|
exit 1
|
|
70
|
-
|
|
33
|
+
}
|
|
71
34
|
|
|
72
|
-
|
|
73
|
-
|
|
35
|
+
if [ ! -f "$GRAPH_JSONL" ]; then
|
|
36
|
+
missing_graph
|
|
37
|
+
fi
|
|
74
38
|
|
|
75
|
-
|
|
76
|
-
|
|
39
|
+
# --- Read graph.jsonl into temporary JSON ---
|
|
40
|
+
TMP_NODES=$(mktemp)
|
|
41
|
+
TMP_EDGES=$(mktemp)
|
|
42
|
+
TMP_ALL=$(mktemp)
|
|
77
43
|
|
|
78
|
-
python3 - "
|
|
44
|
+
python3 -c "
|
|
79
45
|
import json, sys
|
|
80
|
-
from pathlib import Path
|
|
81
46
|
|
|
82
|
-
|
|
83
|
-
nodes_out = Path(sys.argv[2])
|
|
84
|
-
edges_out = Path(sys.argv[3])
|
|
85
|
-
|
|
86
|
-
nodes = []
|
|
47
|
+
nodes = {}
|
|
87
48
|
edges = []
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if obj.get("type") == "entity":
|
|
105
|
-
nodes.append({
|
|
106
|
-
"name": obj["name"],
|
|
107
|
-
"entityType": obj.get("entityType", "Skill"),
|
|
108
|
-
"observations": obj.get("observations", []),
|
|
49
|
+
|
|
50
|
+
with open('$GRAPH_JSONL') as f:
|
|
51
|
+
for line in f:
|
|
52
|
+
obj = json.loads(line)
|
|
53
|
+
if obj.get('type') == 'entity':
|
|
54
|
+
nodes[obj['name']] = {
|
|
55
|
+
'id': obj['name'],
|
|
56
|
+
'type': obj.get('entityType', 'Skill'),
|
|
57
|
+
'observations': obj.get('observations', [])
|
|
58
|
+
}
|
|
59
|
+
elif obj.get('type') == 'relation':
|
|
60
|
+
edges.append({
|
|
61
|
+
'source': obj.get('from', ''),
|
|
62
|
+
'target': obj.get('to', ''),
|
|
63
|
+
'relation': obj.get('relationType', 'references'),
|
|
64
|
+
'provenance': obj.get('provenance', '')
|
|
109
65
|
})
|
|
110
|
-
|
|
111
|
-
|
|
66
|
+
|
|
67
|
+
# Emit node list
|
|
68
|
+
print(json.dumps(list(nodes.values()), indent=2))
|
|
69
|
+
" > "$TMP_NODES"
|
|
70
|
+
|
|
71
|
+
python3 -c "
|
|
72
|
+
import json
|
|
73
|
+
# re-read edges into a separate file
|
|
74
|
+
edges = []
|
|
75
|
+
with open('$GRAPH_JSONL') as f:
|
|
76
|
+
for line in f:
|
|
77
|
+
obj = json.loads(line)
|
|
78
|
+
if obj.get('type') == 'relation':
|
|
112
79
|
edges.append({
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
80
|
+
'source': obj.get('from', ''),
|
|
81
|
+
'target': obj.get('to', ''),
|
|
82
|
+
'relation': obj.get('relationType', 'references'),
|
|
83
|
+
'provenance': obj.get('provenance', '')
|
|
116
84
|
})
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
with open(edges_out, "w") as f:
|
|
124
|
-
json.dump(edges, f, indent=2)
|
|
125
|
-
|
|
126
|
-
print(f" Entities: {entity_count}, Relations: {relation_count}", file=sys.stderr)
|
|
127
|
-
if parse_errors:
|
|
128
|
-
print(f" Parse errors: {parse_errors}", file=sys.stderr)
|
|
129
|
-
PYEOF
|
|
130
|
-
|
|
131
|
-
# Emit specs/skill-graph.json
|
|
132
|
-
if [[ "$DO_JSON" -eq 1 ]]; then
|
|
133
|
-
echo "Writing $GRAPH_OUT ..." >&2
|
|
134
|
-
python3 - "$NODES_JSON" "$EDGES_JSON" "$GRAPH_OUT" <<'PYEOF'
|
|
135
|
-
import json, sys
|
|
85
|
+
print(json.dumps(edges, indent=2))
|
|
86
|
+
" > "$TMP_EDGES"
|
|
87
|
+
|
|
88
|
+
# --- Write skill-graph.json ---
|
|
89
|
+
python3 -c "
|
|
90
|
+
import json
|
|
136
91
|
|
|
137
|
-
with open(
|
|
92
|
+
with open('$TMP_NODES') as f:
|
|
138
93
|
nodes = json.load(f)
|
|
139
|
-
with open(
|
|
94
|
+
with open('$TMP_EDGES') as f:
|
|
140
95
|
edges = json.load(f)
|
|
141
96
|
|
|
142
|
-
graph = {
|
|
143
|
-
|
|
97
|
+
graph = {
|
|
98
|
+
'meta': {
|
|
99
|
+
'source': 'bigpowers-mcp/graph.jsonl',
|
|
100
|
+
'generated_by': 'scripts/build-skill-graph.sh',
|
|
101
|
+
'node_count': len(nodes),
|
|
102
|
+
'edge_count': len(edges)
|
|
103
|
+
},
|
|
104
|
+
'nodes': nodes,
|
|
105
|
+
'edges': edges
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
with open('$SKILL_GRAPH_JSON', 'w') as f:
|
|
144
109
|
json.dump(graph, f, indent=2)
|
|
145
110
|
|
|
146
|
-
print(f
|
|
147
|
-
|
|
148
|
-
fi
|
|
111
|
+
print(f'OK: {len(nodes)} nodes, {len(edges)} edges written to specs/skill-graph.json')
|
|
112
|
+
"
|
|
149
113
|
|
|
150
|
-
# Emit specs/skills-wiki/ OKF
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
rm -rf "$WIKI_DIR"
|
|
154
|
-
mkdir -p "$WIKI_DIR"
|
|
114
|
+
# --- Emit specs/skills-wiki/ OKF bundle ---
|
|
115
|
+
WIKI_INDEX="$REPO_ROOT/specs/skills-wiki/index.md"
|
|
116
|
+
mkdir -p "$SKILLS_WIKI"
|
|
155
117
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
118
|
+
# Generate index page
|
|
119
|
+
cat > "$WIKI_INDEX" << WEOF
|
|
120
|
+
# Skills Wiki — OKF Bundle
|
|
159
121
|
|
|
160
|
-
|
|
161
|
-
|
|
122
|
+
---
|
|
123
|
+
type: Index
|
|
124
|
+
title: Skills Wiki
|
|
125
|
+
context: benchmark
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
**Generated by:** scripts/build-skill-graph.sh
|
|
129
|
+
**Source:** bigpowers-mcp/graph.jsonl
|
|
130
|
+
|
|
131
|
+
## Skill Index
|
|
162
132
|
|
|
163
|
-
|
|
133
|
+
WEOF
|
|
134
|
+
|
|
135
|
+
# Generate one .md per skill
|
|
136
|
+
python3 -c "
|
|
137
|
+
import json, os
|
|
138
|
+
|
|
139
|
+
# Re-read nodes
|
|
140
|
+
with open('$TMP_NODES') as f:
|
|
164
141
|
nodes = json.load(f)
|
|
165
142
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
name = node["name"]
|
|
169
|
-
observations = node.get("observations", [])
|
|
170
|
-
|
|
171
|
-
model = ""
|
|
172
|
-
effort = ""
|
|
173
|
-
description = ""
|
|
174
|
-
for obs in observations:
|
|
175
|
-
if obs.startswith("model: "):
|
|
176
|
-
model = obs[7:]
|
|
177
|
-
elif obs.startswith("effort: "):
|
|
178
|
-
effort = obs[8:]
|
|
179
|
-
elif obs.startswith("description: "):
|
|
180
|
-
description = obs[13:]
|
|
181
|
-
|
|
182
|
-
# Derive phase from name convention (simplified)
|
|
183
|
-
phase = "utility"
|
|
184
|
-
critical_path = [
|
|
185
|
-
"survey-context", "elaborate-spec", "model-domain", "define-language",
|
|
186
|
-
"design-interface", "plan-work", "plan-release", "slice-tasks",
|
|
187
|
-
"kickoff-branch", "develop-tdd", "verify-work", "audit-code",
|
|
188
|
-
"commit-message", "release-branch", "security-review",
|
|
189
|
-
"assess-impact", "investigate-bug", "validate-fix",
|
|
190
|
-
]
|
|
191
|
-
if name in critical_path:
|
|
192
|
-
phase = "critical-path"
|
|
193
|
-
|
|
194
|
-
safe_desc = description.replace('"', '\\"')
|
|
195
|
-
md = f"""---
|
|
196
|
-
type: Skill
|
|
197
|
-
name: {name}
|
|
198
|
-
phase: {phase}
|
|
199
|
-
model: {model}
|
|
200
|
-
effort: {effort}
|
|
201
|
-
description: "{safe_desc}"
|
|
202
|
-
---
|
|
143
|
+
skills_dir = '$SKILLS_WIKI'
|
|
144
|
+
index_path = '$WIKI_INDEX'
|
|
203
145
|
|
|
204
|
-
#
|
|
146
|
+
# Sort by phase then name
|
|
147
|
+
nodes.sort(key=lambda n: (n.get('type', ''), n['id']))
|
|
205
148
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
149
|
+
with open(index_path, 'a') as idx:
|
|
150
|
+
for node in nodes:
|
|
151
|
+
name = node['id']
|
|
152
|
+
ntype = node.get('type', 'Skill')
|
|
153
|
+
obs = node.get('observations', [])
|
|
209
154
|
|
|
210
|
-
|
|
155
|
+
slug = name # already kebab-case
|
|
211
156
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
157
|
+
# Extract key fields from observations
|
|
158
|
+
model = ''
|
|
159
|
+
effort = ''
|
|
160
|
+
desc = ''
|
|
161
|
+
for o in obs:
|
|
162
|
+
if o.startswith('model:'):
|
|
163
|
+
model = o.replace('model:', '').strip()
|
|
164
|
+
elif o.startswith('effort:'):
|
|
165
|
+
effort = o.replace('effort:', '').strip()
|
|
166
|
+
elif o.startswith('description:'):
|
|
167
|
+
desc = o.replace('description:', '').strip()
|
|
218
168
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
169
|
+
# Write skill page
|
|
170
|
+
page = f'''# {name}
|
|
171
|
+
|
|
172
|
+
**Type:** {ntype}
|
|
173
|
+
**Model:** {model or 'default'}
|
|
174
|
+
**Effort:** {effort or 'standard'}
|
|
175
|
+
|
|
176
|
+
{desc}
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
*Auto-generated by scripts/build-skill-graph.sh*
|
|
181
|
+
'''
|
|
182
|
+
with open(os.path.join(skills_dir, f'{slug}.md'), 'w') as pf:
|
|
183
|
+
pf.write(page)
|
|
184
|
+
|
|
185
|
+
# Add to index
|
|
186
|
+
idx.write(f'- [{name}](skills/{slug}.md) — {desc[:80] if desc else \"\"}\\n')
|
|
187
|
+
|
|
188
|
+
print(f'OK: {len(nodes)} skill pages written to specs/skills-wiki/skills/')
|
|
189
|
+
"
|
|
222
190
|
|
|
223
191
|
# Cleanup
|
|
224
|
-
rm -f "$
|
|
192
|
+
rm -f "$TMP_NODES" "$TMP_EDGES" "$TMP_ALL"
|
|
225
193
|
|
|
226
|
-
echo "Done."
|
|
194
|
+
echo "Done."
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e39s03
|
|
3
|
+
# Compare spec-file mtimes against implementing source-file mtimes,
|
|
4
|
+
# mark suspect links, write to specs/drift-report.json
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
8
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
9
|
+
TRACE_MATRIX="$REPO_ROOT/specs/traceability-matrix.json"
|
|
10
|
+
DRIFT_REPORT="$REPO_ROOT/specs/drift-report.json"
|
|
11
|
+
|
|
12
|
+
show_help() {
|
|
13
|
+
cat <<EOF
|
|
14
|
+
Usage: $(basename "$0") [OPTION]
|
|
15
|
+
|
|
16
|
+
Compare spec/story mtimes vs implementing file mtimes to detect drift.
|
|
17
|
+
Output: specs/drift-report.json with suspect and broken links.
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--help Show this help and exit
|
|
21
|
+
EOF
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if [ "$#" -eq 1 ] && [ "$1" = "--help" ]; then
|
|
25
|
+
show_help
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [ ! -f "$TRACE_MATRIX" ]; then
|
|
30
|
+
echo "[ERROR] Traceability matrix not found: $TRACE_MATRIX" >&2
|
|
31
|
+
echo "[ERROR] Run scripts/trace-stories.sh first" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
python3 -c "
|
|
36
|
+
import json, os, sys
|
|
37
|
+
|
|
38
|
+
with open('$TRACE_MATRIX') as f:
|
|
39
|
+
matrix = json.load(f)
|
|
40
|
+
|
|
41
|
+
drift = {
|
|
42
|
+
'meta': {
|
|
43
|
+
'generated_by': 'scripts/check-spec-drift.sh',
|
|
44
|
+
'source': 'specs/traceability-matrix.json',
|
|
45
|
+
'timestamp': __import__('datetime').datetime.now(__import__('datetime').timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
46
|
+
},
|
|
47
|
+
'suspect_links': [],
|
|
48
|
+
'broken_links': [],
|
|
49
|
+
'summary': {'suspect': 0, 'broken': 0, 'ok': 0}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
repo_root = '$REPO_ROOT'
|
|
53
|
+
|
|
54
|
+
stories = matrix.get('stories', [])
|
|
55
|
+
assert isinstance(stories, list), 'stories must be a list'
|
|
56
|
+
|
|
57
|
+
for entry in stories:
|
|
58
|
+
story_id = entry.get('id', '')
|
|
59
|
+
spec_path = entry.get('spec_path', '')
|
|
60
|
+
links = entry.get('links', [])
|
|
61
|
+
|
|
62
|
+
if not links:
|
|
63
|
+
drift['broken_links'].append({
|
|
64
|
+
'story_id': story_id,
|
|
65
|
+
'reason': 'no trace links',
|
|
66
|
+
'severity': 'broken'
|
|
67
|
+
})
|
|
68
|
+
drift['summary']['broken'] += 1
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
impl_files = [link['file'] for link in links if link.get('file')]
|
|
72
|
+
|
|
73
|
+
if not impl_files:
|
|
74
|
+
drift['broken_links'].append({
|
|
75
|
+
'story_id': story_id,
|
|
76
|
+
'reason': 'no implementing files in trace links',
|
|
77
|
+
'severity': 'broken'
|
|
78
|
+
})
|
|
79
|
+
drift['summary']['broken'] += 1
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
suspect_files = []
|
|
83
|
+
|
|
84
|
+
for impl_file in impl_files:
|
|
85
|
+
impl_full = os.path.join(repo_root, impl_file) if not os.path.isabs(impl_file) else impl_file
|
|
86
|
+
if os.path.exists(impl_full):
|
|
87
|
+
impl_mtime = os.path.getmtime(impl_full)
|
|
88
|
+
if spec_path and os.path.exists(spec_path):
|
|
89
|
+
spec_mtime = os.path.getmtime(spec_path)
|
|
90
|
+
if impl_mtime > spec_mtime:
|
|
91
|
+
suspect_files.append({
|
|
92
|
+
'file': impl_file,
|
|
93
|
+
'impl_mtime': impl_mtime,
|
|
94
|
+
'spec_mtime': spec_mtime,
|
|
95
|
+
'age_seconds': impl_mtime - spec_mtime
|
|
96
|
+
})
|
|
97
|
+
else:
|
|
98
|
+
drift['broken_links'].append({
|
|
99
|
+
'story_id': story_id,
|
|
100
|
+
'spec': spec_path,
|
|
101
|
+
'file': impl_file,
|
|
102
|
+
'reason': 'implementing file does not exist',
|
|
103
|
+
'severity': 'broken'
|
|
104
|
+
})
|
|
105
|
+
drift['summary']['broken'] += 1
|
|
106
|
+
|
|
107
|
+
if suspect_files:
|
|
108
|
+
drift['suspect_links'].append({
|
|
109
|
+
'story_id': story_id,
|
|
110
|
+
'spec': spec_path,
|
|
111
|
+
'files': suspect_files
|
|
112
|
+
})
|
|
113
|
+
drift['summary']['suspect'] += 1
|
|
114
|
+
else:
|
|
115
|
+
drift['summary']['ok'] += 1
|
|
116
|
+
|
|
117
|
+
with open('$DRIFT_REPORT', 'w') as f:
|
|
118
|
+
json.dump(drift, f, indent=2)
|
|
119
|
+
|
|
120
|
+
print(f'OK: {drift[\"summary\"][\"ok\"]} clean, {drift[\"summary\"][\"suspect\"]} suspect, {drift[\"summary\"][\"broken\"]} broken')
|
|
121
|
+
"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-spec-version-gap.sh — detect spec format gap (e44s02)
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
BP_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
6
|
+
|
|
7
|
+
usage() {
|
|
8
|
+
cat <<'EOF'
|
|
9
|
+
Usage: check-spec-version-gap.sh [--help] [--project <dir>] [--json]
|
|
10
|
+
Exit codes: 0=no gap, 1=gap, 2=no specs, 3=blocked, 4=error
|
|
11
|
+
EOF
|
|
12
|
+
exit 0
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
PROJECT_DIR="."
|
|
16
|
+
JSON_STDOUT=false
|
|
17
|
+
SKIP_BLOCK=false
|
|
18
|
+
while [[ $# -gt 0 ]]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--help|-h) usage ;;
|
|
21
|
+
--project) PROJECT_DIR="${2:?}"; shift 2 ;;
|
|
22
|
+
--json) JSON_STDOUT=true; shift ;;
|
|
23
|
+
--skip-block) SKIP_BLOCK=true; shift ;;
|
|
24
|
+
*) echo "unknown: $1" >&2; exit 4 ;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
PROJECT_DIR="$(cd "$PROJECT_DIR" 2>/dev/null && pwd)" || { echo "bad dir: $PROJECT_DIR" >&2; exit 4; }
|
|
29
|
+
SPECS="$PROJECT_DIR/specs"
|
|
30
|
+
|
|
31
|
+
emit_json() {
|
|
32
|
+
if $JSON_STDOUT; then echo "$1"; else echo "$1" >&2; fi
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get_installed_version() {
|
|
36
|
+
python3 -c "import json; d=json.load(open('$BP_ROOT/package.json')); print(d.get('version','unknown'))" 2>/dev/null || echo "unknown"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
yaml_get() {
|
|
40
|
+
[[ ! -f "$1" ]] && { echo ""; return; }
|
|
41
|
+
python3 -c "
|
|
42
|
+
import sys, yaml
|
|
43
|
+
d = yaml.safe_load(open('$1')) or {}
|
|
44
|
+
parts = '$2'.split('.')
|
|
45
|
+
cur = d
|
|
46
|
+
for p in parts:
|
|
47
|
+
if isinstance(cur, dict): cur = cur.get(p)
|
|
48
|
+
else: print(''); sys.exit(0)
|
|
49
|
+
print('' if cur is None else cur)
|
|
50
|
+
" 2>/dev/null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
version_lt() {
|
|
54
|
+
if [[ "$1" == "$2" ]]; then return 1; fi
|
|
55
|
+
local IFS=.; local a=($1) b=($2)
|
|
56
|
+
for i in "${!a[@]}"; do
|
|
57
|
+
local av="${a[$i]:-0}" bv="${b[$i]:-0}"
|
|
58
|
+
if (( av < bv )); then return 0; fi
|
|
59
|
+
if (( av > bv )); then return 1; fi
|
|
60
|
+
done
|
|
61
|
+
return 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
find_migrations() {
|
|
65
|
+
local ver="$1" reg="$BP_ROOT/specs/migrations/registry.okf.md"
|
|
66
|
+
[[ ! -f "$reg" ]] && { echo "[]"; return; }
|
|
67
|
+
python3 -c "
|
|
68
|
+
import json, yaml
|
|
69
|
+
def vt(v): return tuple(int(p) if p.isdigit() else 0 for p in v.lstrip('v').split('.')[:3])
|
|
70
|
+
dt = vt('$ver')
|
|
71
|
+
c = open('$reg').read()
|
|
72
|
+
parts = c.split('---')
|
|
73
|
+
fm = yaml.safe_load(parts[1]) if len(parts) >= 3 else {}
|
|
74
|
+
out = []
|
|
75
|
+
for m in fm.get('migrations', []):
|
|
76
|
+
if m.get('status') not in ('implemented','planned'): continue
|
|
77
|
+
st = vt(m.get('since_version','0'))
|
|
78
|
+
if dt < st:
|
|
79
|
+
out.append({'id': m['id'], 'since_version': m['since_version'], 'order': m.get('order',0), 'depends_on': m.get('depends_on',[]), 'actions_needed': m.get('actions_needed',[]), 'status': m.get('status','')})
|
|
80
|
+
print(json.dumps(out))
|
|
81
|
+
" 2>/dev/null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
check_active_work() {
|
|
85
|
+
local branch; branch=$(git -C "$PROJECT_DIR" branch --show-current 2>/dev/null || echo "")
|
|
86
|
+
if [[ -n "$branch" && "$branch" != "main" && "$branch" != "master" ]]; then echo "true"; return; fi
|
|
87
|
+
if [[ -d "$SPECS" ]] && git -C "$PROJECT_DIR" status --porcelain -- specs/ 2>/dev/null | grep -q .; then echo "true"; return; fi
|
|
88
|
+
local af; af=$(yaml_get "$SPECS/state.yaml" "active_flow")
|
|
89
|
+
if [[ -n "$af" && "$af" != "null" && "$af" != "None" ]]; then
|
|
90
|
+
case "$af" in build_epic|develop_tdd|fix_bug) echo "true"; return ;; esac
|
|
91
|
+
fi
|
|
92
|
+
echo "false"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Main
|
|
96
|
+
[[ ! -d "$SPECS" ]] && { emit_json '{"gap":false,"reason":"NO_SPECS"}'; exit 2; }
|
|
97
|
+
IV=$(get_installed_version)
|
|
98
|
+
if [ "$SKIP_BLOCK" != "true" ] && [[ "$(check_active_work)" == "true" ]]; then
|
|
99
|
+
emit_json '{"gap":true,"blocked":true,"reason":"ACTIVE_WORK","installed_version":"'"$IV"'"}'
|
|
100
|
+
exit 3
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
SV=$(yaml_get "$SPECS/state.yaml" "bigpowers_version")
|
|
104
|
+
|
|
105
|
+
if [[ -n "$SV" && "$SV" != "null" && "$SV" != "None" ]]; then
|
|
106
|
+
if [[ "$SV" == "$IV" ]]; then
|
|
107
|
+
emit_json '{"gap":false,"stamp":true,"detected_version":"'"$SV"'","installed_version":"'"$IV"'","detection_method":"stamp","confidence":"high"}'
|
|
108
|
+
exit 0
|
|
109
|
+
fi
|
|
110
|
+
APP=$(find_migrations "$SV")
|
|
111
|
+
CNT=$(echo "$APP" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
|
|
112
|
+
emit_json "$(python3 -c "import json; a=json.loads('''$APP'''); print(json.dumps({'gap':True,'stamp':True,'detected_version':'$SV','detection_method':'stamp','installed_version':'$IV','applicable_migrations':len(a),'migration_ids':[m['id'] for m in a],'confidence':'high','active_work_blocked':False}))")"
|
|
113
|
+
exit 1
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Fingerprint
|
|
117
|
+
DV=""; CONF="medium"; MARKERS=()
|
|
118
|
+
[[ -f "$SPECS/STATE.md" ]] && { DV="1.0.0"; CONF="high"; MARKERS+=("STATE.md"); }
|
|
119
|
+
[[ -f "$SPECS/state.yaml" ]] && { [[ -z "$DV" ]] && { DV="2.0.0"; MARKERS+=("state.yaml"); } || MARKERS+=("state.yaml (dual-era)"); }
|
|
120
|
+
for f in release-plan.yaml execution-status.yaml product/SCOPE_LATEST.yaml metrics/cycle-times.yaml; do
|
|
121
|
+
[[ -f "$SPECS/$f" ]] && { MARKERS+=("$f"); [[ -z "$DV" ]] && DV="2.0.0"; }
|
|
122
|
+
done
|
|
123
|
+
if [[ -f "$SPECS/state.yaml" ]]; then
|
|
124
|
+
ec=$(yaml_get "$SPECS/state.yaml" "epic_cycle.current_step")
|
|
125
|
+
if [[ -n "$ec" && "$ec" != "null" && "$ec" != "None" ]]; then
|
|
126
|
+
MARKERS+=("epic_cycle"); [[ "$DV" == "2.0.0" ]] && DV="2.20.0"
|
|
127
|
+
fi
|
|
128
|
+
fi
|
|
129
|
+
if ls "$SPECS"/epics/*/epic.yaml 2>/dev/null | head -1 | grep -q .; then
|
|
130
|
+
MARKERS+=("epic capsules"); [[ "$DV" == "2.0.0" ]] && DV="2.20.0"; [[ -z "$DV" ]] && DV="2.20.0"
|
|
131
|
+
fi
|
|
132
|
+
[[ -z "$DV" ]] && { DV="unknown"; CONF="low"; }
|
|
133
|
+
|
|
134
|
+
if [[ "$DV" != "unknown" ]] && ! version_lt "$DV" "$IV"; then
|
|
135
|
+
[[ "$DV" == "$IV" ]] && { emit_json '{"gap":false,"stamp":false,"detected_version":"'"$DV"'","installed_version":"'"$IV"'","detection_method":"fingerprint","confidence":"'"$CONF"'"}'; exit 0; }
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
APP=$(find_migrations "$DV")
|
|
139
|
+
CNT=$(echo "$APP" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
|
|
140
|
+
MJ=$(printf '%s\n' "${MARKERS[@]}" | python3 -c "import json,sys; print(json.dumps([l.strip() for l in sys.stdin if l.strip()]))" 2>/dev/null || echo "[]")
|
|
141
|
+
|
|
142
|
+
emit_json "$(python3 -c "import json; a=json.loads('''$APP'''); m=json.loads('''$MJ'''); print(json.dumps({'gap':True,'stamp':False,'detected_version':'$DV','detection_method':'fingerprint','installed_version':'$IV','applicable_migrations':len(a),'migration_ids':[x['id'] for x in a],'confidence':'$CONF','active_work_blocked':False,'matched_markers':m}))")"
|
|
143
|
+
exit 1
|