bigpowers 2.86.3 → 2.86.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/package.json +1 -1
- package/CHANGELOG.md +7 -0
- package/package.json +1 -1
- package/scripts/golden-g12-status-consistency.sh +232 -0
- package/scripts/lib/__pycache__/link_utils.cpython-312.pyc +0 -0
- package/scripts/lib/__pycache__/srp_engine_links.cpython-312.pyc +0 -0
- package/scripts/lib/golden-suite-gates.sh +5 -0
- package/scripts/sync-status-from-epics.sh +8 -0
package/.pi/package.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [2.86.4](https://github.com/danielvm-git/bigpowers/compare/v2.86.3...v2.86.4) (2026-07-26)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **specs:** reconcile planning SoT drift and gate it with G-12 ([d8553a7](https://github.com/danielvm-git/bigpowers/commit/d8553a748d4cdc81fa612564f84600a62c4789ce))
|
|
7
|
+
|
|
1
8
|
## [2.86.3](https://github.com/danielvm-git/bigpowers/compare/v2.86.2...v2.86.3) (2026-07-26)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e51s04
|
|
3
|
+
# golden-g12-status-consistency.sh — Planning SoT Consistency Gate (G-12)
|
|
4
|
+
#
|
|
5
|
+
# CONVENTIONS.md § specs/ declares specs/execution-status.yaml the sole SoT for
|
|
6
|
+
# story state. Nothing enforced that. sync-status-from-epics.sh is a *seeder*:
|
|
7
|
+
# it preloads keys from execution-status.yaml itself and uses setdefault(), so
|
|
8
|
+
# an existing key keeps its old value and epic.yaml's stories[].status is never
|
|
9
|
+
# read. Story status written by build-epic step 8 into a capsule could therefore
|
|
10
|
+
# disagree with the SoT forever, undetected
|
|
11
|
+
# (BUG-2026-07-26-planning-sot-drift).
|
|
12
|
+
#
|
|
13
|
+
# This gate makes the two agree or fails.
|
|
14
|
+
#
|
|
15
|
+
# Authority model (CONVENTIONS.md § specs/): execution-status.yaml is the sole
|
|
16
|
+
# SoT for story state; a capsule's stories[].status is derived. Capsules under
|
|
17
|
+
# specs/epics/archive/ are frozen historical records — an archived shard that
|
|
18
|
+
# still reads 'todo' while the SoT reads 'done' is normal history, not drift, so
|
|
19
|
+
# archive/ is excluded. Status vocabulary is normalised before comparison
|
|
20
|
+
# ('passing' == 'done'; 'todo'/'planned'/'not-started' == 'backlog').
|
|
21
|
+
#
|
|
22
|
+
# Checks:
|
|
23
|
+
# 1. A LIVE capsule must not disagree with execution-status.yaml (both the
|
|
24
|
+
# stories: and development_status: sections). In practice this catches a
|
|
25
|
+
# capsule marked done by build-epic step 8 whose SoT was never updated.
|
|
26
|
+
# 2. Every capsule_dir in release-plan.yaml must resolve to a real directory.
|
|
27
|
+
# 3. A live capsule must not also exist under archive/ — two shards for one
|
|
28
|
+
# epic makes "which status is authoritative" undecidable.
|
|
29
|
+
#
|
|
30
|
+
# Usage: bash scripts/golden-g12-status-consistency.sh [--self-test]
|
|
31
|
+
# Exit 0: SoT is consistent
|
|
32
|
+
# Exit 1: drift detected (or, under --self-test, the gate failed to detect
|
|
33
|
+
# deliberately injected drift — a vacuous gate, G-08 bug class)
|
|
34
|
+
|
|
35
|
+
set -euo pipefail
|
|
36
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/python-env.sh"
|
|
37
|
+
source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
|
|
38
|
+
resolve_repo_root
|
|
39
|
+
|
|
40
|
+
RED='\033[0;31m'
|
|
41
|
+
GREEN='\033[0;32m'
|
|
42
|
+
NC='\033[0m'
|
|
43
|
+
|
|
44
|
+
SELF_TEST=0
|
|
45
|
+
[[ "${1:-}" == "--self-test" ]] && SELF_TEST=1
|
|
46
|
+
|
|
47
|
+
check_tree() {
|
|
48
|
+
# $1 = specs dir to check
|
|
49
|
+
$PYTHON - "$1" <<'PY'
|
|
50
|
+
import sys, re
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
import yaml
|
|
53
|
+
|
|
54
|
+
specs = Path(sys.argv[1])
|
|
55
|
+
epics_dir = specs / "epics"
|
|
56
|
+
exec_path = specs / "execution-status.yaml"
|
|
57
|
+
rp_path = specs / "release-plan.yaml"
|
|
58
|
+
|
|
59
|
+
problems = []
|
|
60
|
+
archived_skipped = 0
|
|
61
|
+
|
|
62
|
+
# Status vocabulary normalisation — different skills have written different
|
|
63
|
+
# words for the same state over the repo's history.
|
|
64
|
+
ALIASES = {
|
|
65
|
+
"passing": "done",
|
|
66
|
+
"complete": "done",
|
|
67
|
+
"completed": "done",
|
|
68
|
+
"todo": "backlog",
|
|
69
|
+
"planned": "backlog",
|
|
70
|
+
"not-started": "backlog",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def norm(v):
|
|
75
|
+
return ALIASES.get(str(v).strip().lower(), str(v).strip().lower())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
exec_data = yaml.safe_load(exec_path.read_text(encoding="utf-8")) or {}
|
|
79
|
+
dev = exec_data.get("development_status") or {}
|
|
80
|
+
stories = exec_data.get("stories") or {}
|
|
81
|
+
|
|
82
|
+
# --- Check 1: LIVE epic.yaml story status vs execution-status.yaml ---
|
|
83
|
+
for epic_file in sorted(epics_dir.rglob("epic.yaml")):
|
|
84
|
+
rel = epic_file.relative_to(specs)
|
|
85
|
+
if "archive" in epic_file.relative_to(epics_dir).parts:
|
|
86
|
+
archived_skipped += 1
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
ed = yaml.safe_load(epic_file.read_text(encoding="utf-8")) or {}
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
problems.append(f"unparseable epic shard {rel}: {exc}")
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
for story in ed.get("stories") or []:
|
|
96
|
+
if not isinstance(story, dict):
|
|
97
|
+
continue
|
|
98
|
+
sid = story.get("id")
|
|
99
|
+
declared = story.get("status")
|
|
100
|
+
if not sid or not declared:
|
|
101
|
+
continue
|
|
102
|
+
if not re.match(r"^e\d+s\d+$", str(sid)):
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
sot_detail = stories.get(sid, {})
|
|
106
|
+
sot_detail = sot_detail.get("status") if isinstance(sot_detail, dict) else None
|
|
107
|
+
sot_summary = dev.get(sid)
|
|
108
|
+
|
|
109
|
+
if sot_detail is not None and norm(sot_detail) != norm(declared):
|
|
110
|
+
problems.append(
|
|
111
|
+
f"{sid}: {rel} says '{declared}' but execution-status.yaml "
|
|
112
|
+
f"stories.{sid}.status says '{sot_detail}'"
|
|
113
|
+
)
|
|
114
|
+
if sot_summary is not None and norm(sot_summary) != norm(declared):
|
|
115
|
+
problems.append(
|
|
116
|
+
f"{sid}: {rel} says '{declared}' but execution-status.yaml "
|
|
117
|
+
f"development_status.{sid} says '{sot_summary}'"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# --- Check 2: release-plan.yaml capsule_dir resolves ---
|
|
121
|
+
if rp_path.exists():
|
|
122
|
+
rp = yaml.safe_load(rp_path.read_text(encoding="utf-8")) or {}
|
|
123
|
+
for ep in rp.get("epics") or []:
|
|
124
|
+
if not isinstance(ep, dict):
|
|
125
|
+
continue
|
|
126
|
+
cd = ep.get("capsule_dir")
|
|
127
|
+
if cd and not (specs / cd).is_dir():
|
|
128
|
+
problems.append(
|
|
129
|
+
f"{ep.get('id')}: release-plan.yaml capsule_dir '{cd}' does not exist"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# --- Check 3: no epic has both a live and an archived capsule ---
|
|
133
|
+
archive_dir = epics_dir / "archive"
|
|
134
|
+
if archive_dir.is_dir():
|
|
135
|
+
live = {p.name for p in epics_dir.iterdir() if p.is_dir() and p.name != "archive"}
|
|
136
|
+
for p in archive_dir.iterdir():
|
|
137
|
+
if p.is_dir() and p.name in live:
|
|
138
|
+
problems.append(
|
|
139
|
+
f"{p.name}: capsule exists both live and under archive/ — "
|
|
140
|
+
f"authoritative status is undecidable"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if archived_skipped:
|
|
144
|
+
print(f" info: {archived_skipped} archived capsule(s) skipped (frozen history)")
|
|
145
|
+
|
|
146
|
+
if problems:
|
|
147
|
+
for p in problems:
|
|
148
|
+
print(f" DRIFT {p}")
|
|
149
|
+
print(f"\n{len(problems)} inconsistencies")
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
|
|
152
|
+
print(" ok : epic shards, execution-status.yaml and release-plan.yaml agree")
|
|
153
|
+
sys.exit(0)
|
|
154
|
+
PY
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if [[ "$SELF_TEST" -eq 1 ]]; then
|
|
158
|
+
echo "=== G-12 self-test: prove the gate can fail ==="
|
|
159
|
+
TMP=$(mktemp -d)
|
|
160
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
161
|
+
# Copy only what check_tree reads — specs/ as a whole is ~28MB and the copy
|
|
162
|
+
# would dominate the gate's runtime.
|
|
163
|
+
mkdir -p "$TMP/specs"
|
|
164
|
+
cp -R "$REPO_ROOT/specs/epics" "$TMP/specs/epics"
|
|
165
|
+
cp "$REPO_ROOT/specs/execution-status.yaml" "$TMP/specs/"
|
|
166
|
+
cp "$REPO_ROOT/specs/release-plan.yaml" "$TMP/specs/"
|
|
167
|
+
|
|
168
|
+
# Inject drift into a LIVE capsule. Archived shards are excluded from the
|
|
169
|
+
# check, so mutating one would prove nothing — the gate would rightly stay
|
|
170
|
+
# silent and the self-test would report a false vacuity failure.
|
|
171
|
+
$PYTHON - "$TMP/specs" <<'PY'
|
|
172
|
+
import sys, re
|
|
173
|
+
from pathlib import Path
|
|
174
|
+
import yaml
|
|
175
|
+
|
|
176
|
+
specs = Path(sys.argv[1])
|
|
177
|
+
epics_dir = specs / "epics"
|
|
178
|
+
exec_path = specs / "execution-status.yaml"
|
|
179
|
+
data = yaml.safe_load(exec_path.read_text(encoding="utf-8")) or {}
|
|
180
|
+
|
|
181
|
+
for epic_file in sorted(epics_dir.rglob("epic.yaml")):
|
|
182
|
+
if "archive" in epic_file.relative_to(epics_dir).parts:
|
|
183
|
+
continue
|
|
184
|
+
ed = yaml.safe_load(epic_file.read_text(encoding="utf-8")) or {}
|
|
185
|
+
for story in ed.get("stories") or []:
|
|
186
|
+
if not isinstance(story, dict):
|
|
187
|
+
continue
|
|
188
|
+
sid, st = story.get("id"), story.get("status")
|
|
189
|
+
if sid and st and re.match(r"^e\d+s\d+$", str(sid)):
|
|
190
|
+
flipped = "backlog" if st != "backlog" else "done"
|
|
191
|
+
data.setdefault("stories", {}).setdefault(sid, {})["status"] = flipped
|
|
192
|
+
data.setdefault("development_status", {})[sid] = flipped
|
|
193
|
+
exec_path.write_text(
|
|
194
|
+
yaml.dump(data, default_flow_style=False, sort_keys=False),
|
|
195
|
+
encoding="utf-8",
|
|
196
|
+
)
|
|
197
|
+
print(f"injected: {sid} {st} -> {flipped} (live capsule)")
|
|
198
|
+
sys.exit(0)
|
|
199
|
+
print("ERROR: no live story with a status found to mutate", file=sys.stderr)
|
|
200
|
+
sys.exit(2)
|
|
201
|
+
PY
|
|
202
|
+
|
|
203
|
+
set +e
|
|
204
|
+
check_tree "$TMP/specs" >/dev/null 2>&1
|
|
205
|
+
injected_exit=$?
|
|
206
|
+
set -e
|
|
207
|
+
|
|
208
|
+
if [[ $injected_exit -ne 0 ]]; then
|
|
209
|
+
echo -e "${GREEN}PASS${NC} G-12 detected injected drift (exit $injected_exit)"
|
|
210
|
+
echo "G-12 self-test: PASS"
|
|
211
|
+
exit 0
|
|
212
|
+
fi
|
|
213
|
+
echo -e "${RED}FAIL${NC} G-12 passed on deliberately drifted specs — vacuous gate"
|
|
214
|
+
echo "G-12 self-test: FAIL — same bug class as G-08 / BUG-2026-07-26-planning-sot-drift"
|
|
215
|
+
exit 1
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
echo "=== G-12: Planning SoT Consistency ==="
|
|
219
|
+
set +e
|
|
220
|
+
output=$(check_tree "$REPO_ROOT/specs" 2>&1)
|
|
221
|
+
actual_exit=$?
|
|
222
|
+
set -e
|
|
223
|
+
echo "$output"
|
|
224
|
+
|
|
225
|
+
if [[ $actual_exit -eq 0 ]]; then
|
|
226
|
+
echo -e "${GREEN}PASS${NC} planning SoT is consistent"
|
|
227
|
+
echo "G-12: PASS"
|
|
228
|
+
exit 0
|
|
229
|
+
fi
|
|
230
|
+
echo -e "${RED}FAIL${NC} planning SoT has drifted"
|
|
231
|
+
echo "G-12: FAIL — see BUG-2026-07-26-planning-sot-drift"
|
|
232
|
+
exit 1
|
|
Binary file
|
|
Binary file
|
|
@@ -14,6 +14,11 @@ GOLDEN_GATES=(
|
|
|
14
14
|
"g09-yaml-roundtrip:bash scripts/golden-g09-yaml-roundtrip.sh:false"
|
|
15
15
|
"g10-trace-anti-vacuity:bash scripts/golden-g10-trace-anti-vacuity.sh:false"
|
|
16
16
|
"g11-gitignore-venv:bash scripts/golden-g11-gitignore-venv.sh:false"
|
|
17
|
+
# Planning SoT consistency (BUG-2026-07-26-planning-sot-drift): live epic
|
|
18
|
+
# shards, execution-status.yaml and release-plan.yaml must agree. Self-test
|
|
19
|
+
# first so a vacuous gate is caught before its verdict is trusted.
|
|
20
|
+
"g12-status-selftest:bash scripts/golden-g12-status-consistency.sh --self-test:false"
|
|
21
|
+
"g12-status-consistency:bash scripts/golden-g12-status-consistency.sh:false"
|
|
17
22
|
"install-helpers:bash scripts/test-install-helpers.sh:false"
|
|
18
23
|
"import-boundaries:bash scripts/check-import-boundaries.sh:false"
|
|
19
24
|
"skill-catalog:bash scripts/validate-skill-catalog.sh:false"
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# sync-status-from-epics.sh — seed execution-status.yaml keys from epic shards
|
|
3
|
+
#
|
|
4
|
+
# SEEDER, NOT A RECONCILER. `keys` is preloaded from execution-status.yaml
|
|
5
|
+
# itself and applied with setdefault(), so an already-present key keeps its
|
|
6
|
+
# existing value; epic.yaml's stories[].status is never read. Running this does
|
|
7
|
+
# NOT pull a capsule's status into the SoT — it only adds missing keys and
|
|
8
|
+
# refreshes task counters. Drift between a capsule and the SoT is detected by
|
|
9
|
+
# scripts/golden-g12-status-consistency.sh (G-12), not corrected here.
|
|
10
|
+
# See specs/bugs/BUG-2026-07-26-planning-sot-drift.md.
|
|
3
11
|
set -euo pipefail
|
|
4
12
|
source "$(dirname "${BASH_SOURCE[0]}")/lib/python-env.sh"
|
|
5
13
|
source "$(dirname "${BASH_SOURCE[0]}")/lib/skill-common.sh"
|