bigpowers 2.73.8 → 2.74.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/.continue/rules/context7-mcp.md +0 -0
- package/.continue/rules/diagnose-stall.md +0 -0
- package/.kilocode/rules/context7-mcp.md +0 -0
- package/.kilocode/rules/diagnose-stall.md +0 -0
- package/.pi/package.json +2 -2
- package/.pi/prompts/audit-code.md +17 -13
- package/.pi/prompts/change-request.md +3 -1
- package/.pi/prompts/compose-workflow.md +15 -0
- package/.pi/prompts/context7-mcp.md +54 -0
- package/.pi/prompts/craft-skill.md +28 -0
- package/.pi/prompts/deepen-architecture.md +20 -0
- package/.pi/prompts/delegate-task.md +14 -0
- package/.pi/prompts/deploy.md +8 -0
- package/.pi/prompts/develop-tdd.md +15 -4
- package/.pi/prompts/diagnose-stall.md +53 -0
- package/.pi/prompts/dispatch-agents.md +49 -19
- package/.pi/prompts/gate-trace.md +9 -1
- package/.pi/prompts/guard-git.md +14 -0
- package/.pi/prompts/migrate-spec.md +12 -0
- package/.pi/prompts/plan-work.md +62 -2
- package/.pi/prompts/publish-package.md +11 -0
- package/.pi/prompts/release-branch.md +39 -43
- package/.pi/prompts/request-review.md +57 -17
- package/.pi/prompts/run-benchmark.md +3 -3
- package/.pi/prompts/run-evals.md +26 -6
- package/.pi/prompts/security-review.md +56 -8
- package/.pi/prompts/seed-conventions.md +61 -0
- package/.pi/prompts/session-state.md +5 -10
- package/.pi/prompts/slice-tasks.md +3 -0
- package/.pi/prompts/smoke-test.md +11 -0
- package/.pi/prompts/stocktake-skills.md +2 -1
- package/.pi/prompts/validate-contracts.md +12 -0
- package/.pi/prompts/validate-fix.md +2 -0
- package/.pi/prompts/verify-work.md +31 -20
- package/.pi/prompts/wire-ci.md +11 -0
- package/.pi/prompts/write-document.md +10 -0
- package/.pi/skills/audit-code/SKILL.md +17 -13
- package/.pi/skills/change-request/SKILL.md +3 -1
- package/.pi/skills/compose-workflow/SKILL.md +15 -0
- package/.pi/skills/context7-mcp/SKILL.md +56 -0
- package/.pi/skills/craft-skill/SKILL.md +28 -0
- package/.pi/skills/deepen-architecture/SKILL.md +20 -0
- package/.pi/skills/delegate-task/SKILL.md +14 -0
- package/.pi/skills/deploy/SKILL.md +8 -0
- package/.pi/skills/develop-tdd/SKILL.md +15 -4
- package/.pi/skills/diagnose-stall/SKILL.md +55 -0
- package/.pi/skills/dispatch-agents/SKILL.md +49 -19
- package/.pi/skills/gate-trace/SKILL.md +9 -1
- package/.pi/skills/guard-git/SKILL.md +14 -0
- package/.pi/skills/migrate-spec/SKILL.md +12 -0
- package/.pi/skills/plan-work/SKILL.md +62 -2
- package/.pi/skills/publish-package/SKILL.md +11 -0
- package/.pi/skills/release-branch/SKILL.md +39 -43
- package/.pi/skills/request-review/SKILL.md +57 -17
- package/.pi/skills/run-benchmark/SKILL.md +3 -3
- package/.pi/skills/run-evals/SKILL.md +26 -6
- package/.pi/skills/security-review/SKILL.md +56 -8
- package/.pi/skills/seed-conventions/SKILL.md +61 -0
- package/.pi/skills/session-state/SKILL.md +5 -10
- package/.pi/skills/slice-tasks/SKILL.md +3 -0
- package/.pi/skills/smoke-test/SKILL.md +11 -0
- package/.pi/skills/stocktake-skills/SKILL.md +2 -1
- package/.pi/skills/validate-contracts/SKILL.md +12 -0
- package/.pi/skills/validate-fix/SKILL.md +2 -0
- package/.pi/skills/verify-work/SKILL.md +31 -20
- package/.pi/skills/wire-ci/SKILL.md +11 -0
- package/.pi/skills/write-document/SKILL.md +10 -0
- package/CHANGELOG.md +14 -0
- package/CLAUDE.md +59 -1
- package/CONVENTIONS.md +80 -5
- package/README.md +1 -1
- package/SKILL-INDEX.md +1 -1
- package/package.json +1 -1
- package/scripts/bp-churn-rank.sh +49 -0
- package/scripts/check-import-boundaries.sh +64 -0
- package/scripts/compile-rule-matrix.sh +114 -0
- package/scripts/hooks/rtk-rewrite.sh +11 -0
- package/scripts/hooks/token-mgmt-pre-tool-use.sh +53 -0
- package/scripts/install.sh +6 -0
- package/scripts/lib/completeness-critic.sh +62 -0
- package/scripts/lib/doc-fetch-cache.sh +71 -0
- package/scripts/lib/golden-suite-gates.sh +3 -1
- package/scripts/lib/parallel-review-worktrees.sh +34 -0
- package/scripts/lib/plan-consistency-check.sh +88 -0
- package/scripts/lib/run-benchmark.py +210 -0
- package/scripts/lib/simple_yaml.py +130 -0
- package/scripts/lib/sync-context-engineering-ref.sh +19 -0
- package/scripts/lib/trace-matrix.py +10 -281
- package/scripts/lib/trace_renderer.py +147 -0
- package/scripts/references-manifest.yaml +19 -0
- package/scripts/run-benchmark.sh +6 -0
- package/scripts/sync-references.sh +25 -0
- package/scripts/test-trace-matrix-size.sh +46 -0
- package/scripts/validate-skill-catalog.sh +147 -0
- package/scripts/validate-skill-description.sh +70 -0
- package/skills/audit-code/SKILL.md +19 -13
- package/skills/change-request/SKILL.md +3 -1
- package/skills/compose-workflow/SKILL.md +15 -0
- package/skills/context7-mcp/SKILL.md +57 -0
- package/skills/craft-skill/SKILL.md +29 -0
- package/skills/deepen-architecture/SKILL.md +22 -0
- package/skills/delegate-task/SKILL.md +14 -0
- package/skills/deploy/SKILL.md +9 -0
- package/skills/develop-tdd/SKILL.md +18 -4
- package/skills/diagnose-stall/SKILL.md +56 -0
- package/skills/dispatch-agents/SKILL.md +51 -19
- package/skills/gate-trace/SKILL.md +11 -1
- package/skills/guard-git/REFERENCE.md +14 -0
- package/skills/migrate-spec/REFERENCE.md +12 -0
- package/skills/plan-work/REFERENCE.md +29 -2
- package/skills/plan-work/SKILL.md +38 -0
- package/skills/publish-package/REFERENCE.md +11 -0
- package/skills/release-branch/REFERENCE.md +14 -0
- package/skills/release-branch/SKILL.md +28 -43
- package/skills/request-review/SKILL.md +59 -17
- package/skills/run-benchmark/SKILL.md +4 -4
- package/skills/run-evals/REFERENCE.md +14 -4
- package/skills/run-evals/SKILL.md +13 -2
- package/skills/security-review/REFERENCE-false-positives.md +9 -8
- package/skills/security-review/REFERENCE-vuln-categories.md +4 -0
- package/skills/security-review/SKILL.md +45 -0
- package/skills/security-review/fixtures/CWE-79-xss-negative.js +6 -0
- package/skills/security-review/fixtures/CWE-79-xss-positive.js +6 -0
- package/skills/security-review/fixtures/CWE-89-sqli-negative.py +8 -0
- package/skills/security-review/fixtures/CWE-89-sqli-positive.py +9 -0
- package/skills/seed-conventions/REFERENCE.md +40 -0
- package/skills/seed-conventions/SKILL.md +22 -0
- package/skills/session-state/SKILL.md +5 -10
- package/skills/slice-tasks/SKILL.md +3 -0
- package/skills/smoke-test/REFERENCE.md +11 -0
- package/skills/stocktake-skills/SKILL.md +3 -1
- package/skills/validate-contracts/REFERENCE.md +12 -0
- package/skills/validate-fix/SKILL.md +3 -0
- package/skills/verify-work/REFERENCE.md +17 -0
- package/skills/verify-work/SKILL.md +18 -20
- package/skills/wire-ci/REFERENCE.md +11 -0
- package/skills/write-document/REFERENCE.md +10 -0
- package/skills-lock.json +32 -22
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# story: e45s01
|
|
3
|
+
"""Run skill benchmarks from specs/benchmarks/ with train/validation split and delta grading."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from datetime import date
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
16
|
+
BENCH_DIR = REPO_ROOT / "specs" / "benchmarks"
|
|
17
|
+
REPORTS_DIR = BENCH_DIR / "reports"
|
|
18
|
+
DEFAULT_RUNS = 3
|
|
19
|
+
GRADER_TIMEOUT = 15
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_definition(skill: str) -> dict:
|
|
23
|
+
path = BENCH_DIR / f"{skill}.yaml"
|
|
24
|
+
if not path.exists():
|
|
25
|
+
print(f"ERROR: no benchmark definition at {path}", file=sys.stderr)
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
with path.open() as f:
|
|
28
|
+
return yaml.safe_load(f)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def list_skills() -> list[str]:
|
|
32
|
+
return sorted(p.stem for p in BENCH_DIR.glob("*.yaml") if p.stem not in ("axes",))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_code_grader(command: str, cwd: Path) -> bool:
|
|
36
|
+
try:
|
|
37
|
+
result = subprocess.run(
|
|
38
|
+
["bash", "-c", command],
|
|
39
|
+
cwd=cwd,
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
timeout=GRADER_TIMEOUT,
|
|
43
|
+
)
|
|
44
|
+
out = (result.stdout + result.stderr).strip()
|
|
45
|
+
if "PASS" in out.upper() and "FAIL" not in out.upper():
|
|
46
|
+
return True
|
|
47
|
+
return result.returncode == 0
|
|
48
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def scenario_pass_rate(scenario: dict, runs: int, with_skill: bool) -> tuple[float, str]:
|
|
53
|
+
grader = scenario.get("grader", {})
|
|
54
|
+
gtype = grader.get("type", "code")
|
|
55
|
+
if gtype == "rubric":
|
|
56
|
+
return (-1.0, "pending_agent_eval")
|
|
57
|
+
if gtype != "code":
|
|
58
|
+
return (-1.0, "unsupported_grader")
|
|
59
|
+
command = grader.get("command", "")
|
|
60
|
+
if not command:
|
|
61
|
+
return (-1.0, "missing_command")
|
|
62
|
+
if not with_skill:
|
|
63
|
+
command = f"test -f /dev/null" # bare agent: code graders cannot pass without skill context
|
|
64
|
+
passes = sum(1 for _ in range(runs) if run_code_grader(command, REPO_ROOT))
|
|
65
|
+
return (passes / runs, "code")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def pass_at_k(scenarios: list[dict], runs: int, with_skill: bool) -> float:
|
|
69
|
+
total_weight = 0.0
|
|
70
|
+
weighted = 0.0
|
|
71
|
+
for sc in scenarios:
|
|
72
|
+
rate, status = scenario_pass_rate(sc, runs, with_skill)
|
|
73
|
+
if status == "pending_agent_eval":
|
|
74
|
+
continue
|
|
75
|
+
weight = float(sc.get("weight", 1.0))
|
|
76
|
+
total_weight += weight
|
|
77
|
+
weighted += weight * max(rate, 0.0)
|
|
78
|
+
if total_weight == 0:
|
|
79
|
+
return 0.0
|
|
80
|
+
return round(weighted / total_weight, 2)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def partition_scenarios(scenarios: list[dict]) -> tuple[list[dict], list[dict]]:
|
|
84
|
+
train, validation = [], []
|
|
85
|
+
for sc in scenarios:
|
|
86
|
+
split = sc.get("split", "validation")
|
|
87
|
+
if split == "train":
|
|
88
|
+
train.append(sc)
|
|
89
|
+
else:
|
|
90
|
+
validation.append(sc)
|
|
91
|
+
return train, validation
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_split_report(scenarios: list[dict], runs: int) -> dict:
|
|
95
|
+
if not scenarios:
|
|
96
|
+
return {"with_skill": 0.0, "without_skill": 0.0, "delta": 0.0, "scenarios": []}
|
|
97
|
+
with_score = pass_at_k(scenarios, runs, with_skill=True)
|
|
98
|
+
without_score = pass_at_k(scenarios, runs, with_skill=False)
|
|
99
|
+
return {
|
|
100
|
+
"with_skill": with_score,
|
|
101
|
+
"without_skill": without_score,
|
|
102
|
+
"delta": round(with_score - without_score, 2),
|
|
103
|
+
"scenarios": [sc["id"] for sc in scenarios],
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def scenario_details(scenarios: list[dict], runs: int) -> list[dict]:
|
|
108
|
+
rows = []
|
|
109
|
+
for sc in scenarios:
|
|
110
|
+
with_rate, status = scenario_pass_rate(sc, runs, with_skill=True)
|
|
111
|
+
without_rate, _ = scenario_pass_rate(sc, runs, with_skill=False)
|
|
112
|
+
if status == "pending_agent_eval":
|
|
113
|
+
rows.append(
|
|
114
|
+
{
|
|
115
|
+
"id": sc["id"],
|
|
116
|
+
"split": sc.get("split", "validation"),
|
|
117
|
+
"status": "pending_agent_eval",
|
|
118
|
+
"weight": sc.get("weight", 1.0),
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
continue
|
|
122
|
+
rows.append(
|
|
123
|
+
{
|
|
124
|
+
"id": sc["id"],
|
|
125
|
+
"split": sc.get("split", "validation"),
|
|
126
|
+
"with_pass_rate": round(max(with_rate, 0.0), 2),
|
|
127
|
+
"without_pass_rate": round(max(without_rate, 0.0), 2),
|
|
128
|
+
"delta": round(max(with_rate, 0.0) - max(without_rate, 0.0), 2),
|
|
129
|
+
"weight": sc.get("weight", 1.0),
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
return rows
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def run_benchmark(skill: str, baseline: bool = False) -> dict:
|
|
136
|
+
definition = load_definition(skill)
|
|
137
|
+
runs = int(definition.get("runs", DEFAULT_RUNS))
|
|
138
|
+
scenarios = definition.get("scenarios", [])
|
|
139
|
+
train_sc, val_sc = partition_scenarios(scenarios)
|
|
140
|
+
today = date.today().isoformat()
|
|
141
|
+
report = {
|
|
142
|
+
"skill": skill,
|
|
143
|
+
"run_date": today,
|
|
144
|
+
"runs_per_scenario": runs,
|
|
145
|
+
"train": build_split_report(train_sc, runs),
|
|
146
|
+
"validation": build_split_report(val_sc, runs),
|
|
147
|
+
"scenarios": scenario_details(scenarios, runs),
|
|
148
|
+
}
|
|
149
|
+
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
yaml_path = REPORTS_DIR / f"BENCHMARK-{skill}-{today}.yaml"
|
|
151
|
+
json_path = REPORTS_DIR / f"benchmark-{skill}.json"
|
|
152
|
+
with yaml_path.open("w") as f:
|
|
153
|
+
yaml.dump(
|
|
154
|
+
{
|
|
155
|
+
"skill": skill,
|
|
156
|
+
"run_date": today,
|
|
157
|
+
"runs_per_scenario": runs,
|
|
158
|
+
"train": {
|
|
159
|
+
"pass_at_k_with": report["train"]["with_skill"],
|
|
160
|
+
"pass_at_k_without": report["train"]["without_skill"],
|
|
161
|
+
"delta": report["train"]["delta"],
|
|
162
|
+
},
|
|
163
|
+
"validation": {
|
|
164
|
+
"pass_at_k_with": report["validation"]["with_skill"],
|
|
165
|
+
"pass_at_k_without": report["validation"]["without_skill"],
|
|
166
|
+
"delta": report["validation"]["delta"],
|
|
167
|
+
},
|
|
168
|
+
"scenarios": report["scenarios"],
|
|
169
|
+
},
|
|
170
|
+
f,
|
|
171
|
+
default_flow_style=False,
|
|
172
|
+
)
|
|
173
|
+
with json_path.open("w") as f:
|
|
174
|
+
json.dump(report, f, separators=(",", ":"))
|
|
175
|
+
print(f"Wrote {yaml_path}")
|
|
176
|
+
print(f"Wrote {json_path}")
|
|
177
|
+
if baseline:
|
|
178
|
+
import shutil
|
|
179
|
+
|
|
180
|
+
shutil.copy(yaml_path, REPORTS_DIR / f"BASELINE-{skill}.yaml")
|
|
181
|
+
shutil.copy(json_path, REPORTS_DIR / f"baseline-{skill}.json")
|
|
182
|
+
print(f"Pinned baseline for {skill}")
|
|
183
|
+
val_delta = report["validation"]["delta"]
|
|
184
|
+
if val_delta < 0:
|
|
185
|
+
print(f"REGRESSION: validation Δ {val_delta} — do NOT ship")
|
|
186
|
+
elif val_delta < 0.05:
|
|
187
|
+
print(f"WARN: validation Δ {val_delta} — marginal contribution")
|
|
188
|
+
else:
|
|
189
|
+
print(f"OK: validation Δ {val_delta}")
|
|
190
|
+
return report
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def main() -> None:
|
|
194
|
+
parser = argparse.ArgumentParser(description="Run skill quality benchmarks")
|
|
195
|
+
parser.add_argument("skill", nargs="?", help="Skill name to benchmark")
|
|
196
|
+
parser.add_argument("--all", action="store_true", help="Benchmark all skills with definitions")
|
|
197
|
+
parser.add_argument("--baseline", action="store_true", help="Pin results as baseline")
|
|
198
|
+
args = parser.parse_args()
|
|
199
|
+
if args.all:
|
|
200
|
+
for skill in list_skills():
|
|
201
|
+
run_benchmark(skill, baseline=args.baseline)
|
|
202
|
+
elif args.skill:
|
|
203
|
+
run_benchmark(args.skill, baseline=args.baseline)
|
|
204
|
+
else:
|
|
205
|
+
parser.print_help()
|
|
206
|
+
sys.exit(1)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
main()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# story: e38s01
|
|
2
|
+
# simple_yaml.py — Custom lightweight YAML parser extracted from trace-matrix.py.
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
def parse_simple_yaml(text: str) -> dict:
|
|
6
|
+
"""Parse flat and one-level-nested YAML including lists of objects."""
|
|
7
|
+
root: dict = {}
|
|
8
|
+
# Each stack entry: (indent, container, parent_dict, key_in_parent)
|
|
9
|
+
stack: list = [(0, root, None, None)]
|
|
10
|
+
skip_until_indent = None # skip block scalar continuation lines (| and >)
|
|
11
|
+
for i, raw in enumerate(text.splitlines()):
|
|
12
|
+
stripped = raw.strip()
|
|
13
|
+
if not stripped or stripped.startswith("#"):
|
|
14
|
+
# Reset block-scalar skip on blank lines
|
|
15
|
+
continue
|
|
16
|
+
indent = len(raw) - len(raw.lstrip())
|
|
17
|
+
|
|
18
|
+
# Skip block scalar continuation (| and > in YAML)
|
|
19
|
+
if skip_until_indent is not None:
|
|
20
|
+
if indent > skip_until_indent:
|
|
21
|
+
continue
|
|
22
|
+
else:
|
|
23
|
+
skip_until_indent = None
|
|
24
|
+
|
|
25
|
+
# Pop stack until we're at the right nesting level
|
|
26
|
+
# Don't pop list containers at same indent — sibling items stay in same list
|
|
27
|
+
while len(stack) > 1:
|
|
28
|
+
if indent < stack[-1][0]:
|
|
29
|
+
stack.pop()
|
|
30
|
+
elif indent == stack[-1][0]:
|
|
31
|
+
# Same indent: pop dicts (moving to sibling key), keep lists (sibling items)
|
|
32
|
+
if isinstance(stack[-1][1], list) and stripped.startswith("- "):
|
|
33
|
+
break # stay in same list for next item
|
|
34
|
+
elif isinstance(stack[-1][1], dict) and stripped.startswith("- "):
|
|
35
|
+
# dict at same indent with upcoming list item — convert to list
|
|
36
|
+
entry_container = stack[-1][1]
|
|
37
|
+
entry_parent = stack[-1][2]
|
|
38
|
+
entry_key = stack[-1][3]
|
|
39
|
+
if entry_parent is not None and entry_key is not None:
|
|
40
|
+
if not isinstance(entry_parent.get(entry_key), list):
|
|
41
|
+
entry_parent[entry_key] = []
|
|
42
|
+
stack[-1] = (stack[-1][0], entry_parent[entry_key], entry_parent, entry_key)
|
|
43
|
+
break
|
|
44
|
+
stack.pop()
|
|
45
|
+
else:
|
|
46
|
+
stack.pop() # other same-indent transitions
|
|
47
|
+
else:
|
|
48
|
+
break # indent > stack indent — staying inside
|
|
49
|
+
|
|
50
|
+
container = stack[-1][1]
|
|
51
|
+
parent_dict = stack[-1][2]
|
|
52
|
+
key_in_parent = stack[-1][3]
|
|
53
|
+
|
|
54
|
+
# List item
|
|
55
|
+
if stripped.startswith("- "):
|
|
56
|
+
inner = stripped[2:]
|
|
57
|
+
# Convert dict to list if needed (preserve original indent from dict push)
|
|
58
|
+
if isinstance(container, dict) and parent_dict is not None and key_in_parent is not None:
|
|
59
|
+
if not isinstance(parent_dict.get(key_in_parent), list):
|
|
60
|
+
orig_indent = stack[-1][0]
|
|
61
|
+
parent_dict[key_in_parent] = []
|
|
62
|
+
container = parent_dict[key_in_parent]
|
|
63
|
+
stack[-1] = (orig_indent, container, parent_dict, key_in_parent)
|
|
64
|
+
|
|
65
|
+
if ":" in inner:
|
|
66
|
+
k, _, v = inner.partition(":")
|
|
67
|
+
k = k.strip()
|
|
68
|
+
v = v.strip()
|
|
69
|
+
# Detect block scalars in list items too
|
|
70
|
+
if v in ("|", ">", "|-", ">-", "|+", ">+"):
|
|
71
|
+
item = {k: v}
|
|
72
|
+
container.append(item)
|
|
73
|
+
skip_until_indent = indent
|
|
74
|
+
continue
|
|
75
|
+
item = {k: _yaml_scalar(v) if v else None}
|
|
76
|
+
container.append(item)
|
|
77
|
+
if v == "":
|
|
78
|
+
stack.append((indent, item, container, k))
|
|
79
|
+
else:
|
|
80
|
+
container.append(_yaml_scalar(inner))
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# Key: value
|
|
84
|
+
if ":" not in stripped:
|
|
85
|
+
continue
|
|
86
|
+
key, _, val = stripped.partition(":")
|
|
87
|
+
key = key.strip()
|
|
88
|
+
val = val.strip()
|
|
89
|
+
|
|
90
|
+
# Detect block scalars (|, >, |- etc.)
|
|
91
|
+
if val in ("|", ">", "|-", ">-", "|+", ">+"):
|
|
92
|
+
if isinstance(container, list) and container:
|
|
93
|
+
container[-1][key] = val # store the marker
|
|
94
|
+
elif isinstance(container, dict):
|
|
95
|
+
container[key] = val
|
|
96
|
+
skip_until_indent = indent
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
if isinstance(container, list) and container:
|
|
100
|
+
# Inside a list item dict
|
|
101
|
+
container[-1][key] = _yaml_scalar(val) if val else None
|
|
102
|
+
if val == "":
|
|
103
|
+
nxt = {}
|
|
104
|
+
container[-1][key] = nxt
|
|
105
|
+
stack.append((indent, nxt, container[-1], key))
|
|
106
|
+
elif isinstance(container, dict):
|
|
107
|
+
if val == "":
|
|
108
|
+
# Store as dict first; will convert to list if next line is "-"
|
|
109
|
+
nxt = {}
|
|
110
|
+
container[key] = nxt
|
|
111
|
+
stack.append((indent, nxt, container, key))
|
|
112
|
+
else:
|
|
113
|
+
container[key] = _yaml_scalar(val)
|
|
114
|
+
return root
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _yaml_scalar(val: str):
|
|
118
|
+
"""Convert a YAML scalar string to Python type."""
|
|
119
|
+
val = val.strip('"').strip("'")
|
|
120
|
+
if val in ("true", "false"):
|
|
121
|
+
return val == "true"
|
|
122
|
+
if val in ("null", "~", ""):
|
|
123
|
+
return None
|
|
124
|
+
try:
|
|
125
|
+
return int(val)
|
|
126
|
+
except ValueError:
|
|
127
|
+
try:
|
|
128
|
+
return float(val)
|
|
129
|
+
except ValueError:
|
|
130
|
+
return val
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# story: e45s10
|
|
3
|
+
# Refresh context-engineering.md sync stamp from SSOT (CLAUDE.md + PRINCIPLES.md).
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
7
|
+
TARGET="$REPO_ROOT/docs/references/context-engineering.md"
|
|
8
|
+
STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
9
|
+
|
|
10
|
+
[[ -f "$TARGET" ]] || exit 0
|
|
11
|
+
|
|
12
|
+
if grep -q 'Last synced:' "$TARGET"; then
|
|
13
|
+
sed -i.bak "s/\*\*Last synced:\*\*.*/\*\*Last synced:\*\* ${STAMP} (from CLAUDE.md + PRINCIPLES.md)/" "$TARGET"
|
|
14
|
+
rm -f "${TARGET}.bak"
|
|
15
|
+
else
|
|
16
|
+
printf '\n**Last synced:** %s (from CLAUDE.md + PRINCIPLES.md)\n' "$STAMP" >> "$TARGET"
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
echo "sync-context-engineering-ref: stamped $TARGET"
|
|
@@ -12,6 +12,12 @@ import json, os, re, sys, time
|
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from datetime import datetime, timezone
|
|
14
14
|
|
|
15
|
+
# Add parent or current directory to sys.path so we can import local helper modules
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
17
|
+
|
|
18
|
+
from simple_yaml import parse_simple_yaml
|
|
19
|
+
from trace_renderer import emit_matrix_json, emit_trace_md, emit_okf_bundle
|
|
20
|
+
|
|
15
21
|
ROOT = Path(sys.argv[1])
|
|
16
22
|
MATRIX_JSON = Path(sys.argv[2])
|
|
17
23
|
TRACE_MD = Path(sys.argv[3])
|
|
@@ -19,136 +25,6 @@ OKF_DIR = Path(sys.argv[4])
|
|
|
19
25
|
STRICT = int(sys.argv[5])
|
|
20
26
|
MODE = sys.argv[6]
|
|
21
27
|
|
|
22
|
-
# -----------------------------------------------------------------------
|
|
23
|
-
# 1. Parse release-plan.yaml → story inventory
|
|
24
|
-
# -----------------------------------------------------------------------
|
|
25
|
-
def parse_simple_yaml(text: str) -> dict:
|
|
26
|
-
"""Parse flat and one-level-nested YAML including lists of objects."""
|
|
27
|
-
root: dict = {}
|
|
28
|
-
# Each stack entry: (indent, container, parent_dict, key_in_parent)
|
|
29
|
-
stack: list = [(0, root, None, None)]
|
|
30
|
-
skip_until_indent = None # skip block scalar continuation lines (| and >)
|
|
31
|
-
for i, raw in enumerate(text.splitlines()):
|
|
32
|
-
stripped = raw.strip()
|
|
33
|
-
if not stripped or stripped.startswith("#"):
|
|
34
|
-
# Reset block-scalar skip on blank lines
|
|
35
|
-
continue
|
|
36
|
-
indent = len(raw) - len(raw.lstrip())
|
|
37
|
-
|
|
38
|
-
# Skip block scalar continuation (| and > in YAML)
|
|
39
|
-
if skip_until_indent is not None:
|
|
40
|
-
if indent > skip_until_indent:
|
|
41
|
-
continue
|
|
42
|
-
else:
|
|
43
|
-
skip_until_indent = None
|
|
44
|
-
|
|
45
|
-
# Pop stack until we're at the right nesting level
|
|
46
|
-
# Don't pop list containers at same indent — sibling items stay in same list
|
|
47
|
-
while len(stack) > 1:
|
|
48
|
-
if indent < stack[-1][0]:
|
|
49
|
-
stack.pop()
|
|
50
|
-
elif indent == stack[-1][0]:
|
|
51
|
-
# Same indent: pop dicts (moving to sibling key), keep lists (sibling items)
|
|
52
|
-
if isinstance(stack[-1][1], list) and stripped.startswith("- "):
|
|
53
|
-
break # stay in same list for next item
|
|
54
|
-
elif isinstance(stack[-1][1], dict) and stripped.startswith("- "):
|
|
55
|
-
# dict at same indent with upcoming list item — convert to list
|
|
56
|
-
entry_container = stack[-1][1]
|
|
57
|
-
entry_parent = stack[-1][2]
|
|
58
|
-
entry_key = stack[-1][3]
|
|
59
|
-
if entry_parent is not None and entry_key is not None:
|
|
60
|
-
if not isinstance(entry_parent.get(entry_key), list):
|
|
61
|
-
entry_parent[entry_key] = []
|
|
62
|
-
stack[-1] = (stack[-1][0], entry_parent[entry_key], entry_parent, entry_key)
|
|
63
|
-
break
|
|
64
|
-
stack.pop()
|
|
65
|
-
else:
|
|
66
|
-
stack.pop() # other same-indent transitions
|
|
67
|
-
else:
|
|
68
|
-
break # indent > stack indent — staying inside
|
|
69
|
-
|
|
70
|
-
container = stack[-1][1]
|
|
71
|
-
parent_dict = stack[-1][2]
|
|
72
|
-
key_in_parent = stack[-1][3]
|
|
73
|
-
|
|
74
|
-
# List item
|
|
75
|
-
if stripped.startswith("- "):
|
|
76
|
-
inner = stripped[2:]
|
|
77
|
-
# Convert dict to list if needed (preserve original indent from dict push)
|
|
78
|
-
if isinstance(container, dict) and parent_dict is not None and key_in_parent is not None:
|
|
79
|
-
if not isinstance(parent_dict.get(key_in_parent), list):
|
|
80
|
-
orig_indent = stack[-1][0]
|
|
81
|
-
parent_dict[key_in_parent] = []
|
|
82
|
-
container = parent_dict[key_in_parent]
|
|
83
|
-
stack[-1] = (orig_indent, container, parent_dict, key_in_parent)
|
|
84
|
-
|
|
85
|
-
if ":" in inner:
|
|
86
|
-
k, _, v = inner.partition(":")
|
|
87
|
-
k = k.strip()
|
|
88
|
-
v = v.strip()
|
|
89
|
-
# Detect block scalars in list items too
|
|
90
|
-
if v in ("|", ">", "|-", ">-", "|+", ">+"):
|
|
91
|
-
item = {k: v}
|
|
92
|
-
container.append(item)
|
|
93
|
-
skip_until_indent = indent
|
|
94
|
-
continue
|
|
95
|
-
item = {k: _yaml_scalar(v) if v else None}
|
|
96
|
-
container.append(item)
|
|
97
|
-
if v == "":
|
|
98
|
-
stack.append((indent, item, container, k))
|
|
99
|
-
else:
|
|
100
|
-
container.append(_yaml_scalar(inner))
|
|
101
|
-
continue
|
|
102
|
-
|
|
103
|
-
# Key: value
|
|
104
|
-
if ":" not in stripped:
|
|
105
|
-
continue
|
|
106
|
-
key, _, val = stripped.partition(":")
|
|
107
|
-
key = key.strip()
|
|
108
|
-
val = val.strip()
|
|
109
|
-
|
|
110
|
-
# Detect block scalars (|, >, |- etc.)
|
|
111
|
-
if val in ("|", ">", "|-", ">-", "|+", ">+"):
|
|
112
|
-
if isinstance(container, list) and container:
|
|
113
|
-
container[-1][key] = val # store the marker
|
|
114
|
-
elif isinstance(container, dict):
|
|
115
|
-
container[key] = val
|
|
116
|
-
skip_until_indent = indent
|
|
117
|
-
continue
|
|
118
|
-
|
|
119
|
-
if isinstance(container, list) and container:
|
|
120
|
-
# Inside a list item dict
|
|
121
|
-
container[-1][key] = _yaml_scalar(val) if val else None
|
|
122
|
-
if val == "":
|
|
123
|
-
nxt = {}
|
|
124
|
-
container[-1][key] = nxt
|
|
125
|
-
stack.append((indent, nxt, container[-1], key))
|
|
126
|
-
elif isinstance(container, dict):
|
|
127
|
-
if val == "":
|
|
128
|
-
# Store as dict first; will convert to list if next line is "-"
|
|
129
|
-
nxt = {}
|
|
130
|
-
container[key] = nxt
|
|
131
|
-
stack.append((indent, nxt, container, key))
|
|
132
|
-
else:
|
|
133
|
-
container[key] = _yaml_scalar(val)
|
|
134
|
-
return root
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def _yaml_scalar(val: str):
|
|
138
|
-
"""Convert a YAML scalar string to Python type."""
|
|
139
|
-
val = val.strip('"').strip("'")
|
|
140
|
-
if val in ("true", "false"):
|
|
141
|
-
return val == "true"
|
|
142
|
-
if val in ("null", "~", ""):
|
|
143
|
-
return None
|
|
144
|
-
try:
|
|
145
|
-
return int(val)
|
|
146
|
-
except ValueError:
|
|
147
|
-
try:
|
|
148
|
-
return float(val)
|
|
149
|
-
except ValueError:
|
|
150
|
-
return val
|
|
151
|
-
|
|
152
28
|
# Parse release plan for epics/stories
|
|
153
29
|
release_plan_path = ROOT / "specs" / "release-plan.yaml"
|
|
154
30
|
release_text = release_plan_path.read_text(encoding="utf-8")
|
|
@@ -248,10 +124,6 @@ for line in result.stdout.splitlines():
|
|
|
248
124
|
# -----------------------------------------------------------------------
|
|
249
125
|
# 4. Resolve each story through oracle tiers
|
|
250
126
|
# -----------------------------------------------------------------------
|
|
251
|
-
# Tier 1: explicit story tag (already captured above)
|
|
252
|
-
# Tier 2: file-name heuristic match
|
|
253
|
-
# Tier 3: epic capsule task reference
|
|
254
|
-
|
|
255
127
|
# Collect all file basenames for heuristic matching
|
|
256
128
|
all_files = []
|
|
257
129
|
for root_dir, dirs, files in os.walk(str(ROOT)):
|
|
@@ -353,153 +225,10 @@ for sid in sorted(tagged_sids):
|
|
|
353
225
|
if sid in stories and dev_status.get(sid) == "done":
|
|
354
226
|
stale_tags.append(sid)
|
|
355
227
|
|
|
356
|
-
#
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
361
|
-
"matrix_version": "1.0",
|
|
362
|
-
"stories": matrix_stories,
|
|
363
|
-
"summary": {
|
|
364
|
-
"total_stories": len(stories),
|
|
365
|
-
"tagged_stories": len(tagged_sids & set(stories.keys())),
|
|
366
|
-
"dark_stories": dark_stories,
|
|
367
|
-
"dark_count": len(dark_stories),
|
|
368
|
-
"orphan_tags": orphan_tags,
|
|
369
|
-
"orphan_count": len(orphan_tags),
|
|
370
|
-
"stale_tags": stale_tags,
|
|
371
|
-
"stale_count": len(stale_tags),
|
|
372
|
-
"oracle_stats": {
|
|
373
|
-
"high": sum(1 for s in matrix_stories for l in s["links"] if l["confidence"] == "high"),
|
|
374
|
-
"medium": sum(1 for s in matrix_stories for l in s["links"] if l["confidence"] == "medium"),
|
|
375
|
-
"low": sum(1 for s in matrix_stories for l in s["links"] if l["confidence"] == "low")
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
MATRIX_JSON.parent.mkdir(parents=True, exist_ok=True)
|
|
381
|
-
MATRIX_JSON.write_text(json.dumps(matrix, indent=2), encoding="utf-8")
|
|
382
|
-
|
|
383
|
-
# -----------------------------------------------------------------------
|
|
384
|
-
# 6. Emit TRACEABILITY_LATEST.md (human-readable report)
|
|
385
|
-
# -----------------------------------------------------------------------
|
|
386
|
-
lines = []
|
|
387
|
-
lines.append("# Traceability Matrix")
|
|
388
|
-
lines.append("")
|
|
389
|
-
lines.append(f"**Generated:** {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}")
|
|
390
|
-
lines.append(f"**Total stories:** {len(stories)}")
|
|
391
|
-
lines.append(f"**Tagged stories:** {len(tagged_sids & set(stories.keys()))}")
|
|
392
|
-
lines.append(f"**Dark stories:** {len(dark_stories)}")
|
|
393
|
-
lines.append(f"**Orphan tags:** {len(orphan_tags)}")
|
|
394
|
-
lines.append(f"**Stale tags:** {len(stale_tags)}")
|
|
395
|
-
lines.append("")
|
|
396
|
-
lines.append("## Oracle Stats")
|
|
397
|
-
lines.append("")
|
|
398
|
-
lines.append(f"- **High** (explicit tag): {matrix['summary']['oracle_stats']['high']}")
|
|
399
|
-
lines.append(f"- **Medium** (file heuristic): {matrix['summary']['oracle_stats']['medium']}")
|
|
400
|
-
lines.append(f"- **Low** (task reference): {matrix['summary']['oracle_stats']['low']}")
|
|
401
|
-
lines.append("")
|
|
402
|
-
|
|
403
|
-
# Story coverage table
|
|
404
|
-
lines.append("## Story Coverage")
|
|
405
|
-
lines.append("")
|
|
406
|
-
lines.append("| Story | Title | Epic | BCP | WSJF | Status | Links |")
|
|
407
|
-
lines.append("|-------|-------|------|-----|------|--------|-------|")
|
|
408
|
-
for s in matrix_stories:
|
|
409
|
-
lines.append(f"| {s['id']} | {s['title'][:60]} | {s['epic_id']} | {s['bcp']} | {s['wsjf']} | {s['status']} | {s['link_count']} |")
|
|
410
|
-
lines.append("")
|
|
411
|
-
|
|
412
|
-
# Findings
|
|
413
|
-
if dark_stories:
|
|
414
|
-
lines.append("## Dark Stories (no code links)")
|
|
415
|
-
lines.append("")
|
|
416
|
-
for ds in dark_stories:
|
|
417
|
-
sinfo = stories.get(ds, {})
|
|
418
|
-
lines.append(f"- **{ds}**: {sinfo.get('title', 'Unknown')} (status: {dev_status.get(ds, '?')})")
|
|
419
|
-
lines.append("")
|
|
420
|
-
|
|
421
|
-
if orphan_tags:
|
|
422
|
-
lines.append("## Orphan Tags (tag in code, no matching story)")
|
|
423
|
-
lines.append("")
|
|
424
|
-
for ot in orphan_tags:
|
|
425
|
-
lines.append(f"- `{ot}`")
|
|
426
|
-
lines.append("")
|
|
427
|
-
|
|
428
|
-
if stale_tags:
|
|
429
|
-
lines.append("## Stale Tags (story done, tag still in code)")
|
|
430
|
-
lines.append("")
|
|
431
|
-
for st in stale_tags:
|
|
432
|
-
lines.append(f"- `{st}`")
|
|
433
|
-
lines.append("")
|
|
434
|
-
|
|
435
|
-
TRACE_MD.parent.mkdir(parents=True, exist_ok=True)
|
|
436
|
-
TRACE_MD.write_text("\n".join(lines), encoding="utf-8")
|
|
437
|
-
|
|
438
|
-
# -----------------------------------------------------------------------
|
|
439
|
-
# 7. Emit OKF bundle (specs/codebase-wiki/)
|
|
440
|
-
# -----------------------------------------------------------------------
|
|
441
|
-
OKF_DIR.mkdir(parents=True, exist_ok=True)
|
|
442
|
-
|
|
443
|
-
# Generate index.md
|
|
444
|
-
idx_lines = []
|
|
445
|
-
idx_lines.append("---")
|
|
446
|
-
idx_lines.append("type: Index")
|
|
447
|
-
idx_lines.append(f"generated_at: {datetime.now(timezone.utc).isoformat()}")
|
|
448
|
-
idx_lines.append(f"total_concepts: {len(stories)}")
|
|
449
|
-
idx_lines.append("---")
|
|
450
|
-
idx_lines.append("")
|
|
451
|
-
idx_lines.append("# Codebase Wiki — Story Traceability")
|
|
452
|
-
idx_lines.append("")
|
|
453
|
-
idx_lines.append("Auto-generated OKF bundle from trace-stories.sh.")
|
|
454
|
-
idx_lines.append("")
|
|
455
|
-
idx_lines.append("| Story | Title | Confidence | Links |")
|
|
456
|
-
idx_lines.append("|-------|-------|------------|-------|")
|
|
457
|
-
for s in matrix_stories:
|
|
458
|
-
confs = {l["confidence"] for l in s["links"]}
|
|
459
|
-
max_conf = "high" if "high" in confs else ("medium" if "medium" in confs else ("low" if confs else "none"))
|
|
460
|
-
idx_lines.append(f"| [{s['id']}](./{s['id']}.md) | {s['title'][:60]} | {max_conf} | {s['link_count']} |")
|
|
461
|
-
idx_lines.append("")
|
|
462
|
-
(OKF_DIR / "index.md").write_text("\n".join(idx_lines), encoding="utf-8")
|
|
463
|
-
|
|
464
|
-
# Generate one concept per story
|
|
465
|
-
for s in matrix_stories:
|
|
466
|
-
concept = []
|
|
467
|
-
concept.append("---")
|
|
468
|
-
concept.append(f"type: Story")
|
|
469
|
-
concept.append(f"id: {s['id']}")
|
|
470
|
-
concept.append(f"epic: {s['epic_id']}")
|
|
471
|
-
concept.append(f"bcps: {s['bcp']}")
|
|
472
|
-
concept.append(f"wsjf: {s['wsjf']}")
|
|
473
|
-
concept.append(f"implementation_status: {s['status']}")
|
|
474
|
-
max_confs = set()
|
|
475
|
-
for l in s["links"]:
|
|
476
|
-
max_confs.add(l["confidence"])
|
|
477
|
-
conf = "high" if "high" in max_confs else ("medium" if "medium" in max_confs else ("low" if max_confs else "none"))
|
|
478
|
-
concept.append(f"coverage_status: {'covered' if s['link_count'] > 0 else 'dark'}")
|
|
479
|
-
concept.append(f"confidence: {conf}")
|
|
480
|
-
concept.append("links:")
|
|
481
|
-
for l in s["links"]:
|
|
482
|
-
concept.append(f" - file: {l['file']}")
|
|
483
|
-
concept.append(f" line: {l['line']}")
|
|
484
|
-
concept.append(f" confidence: {l['confidence']}")
|
|
485
|
-
concept.append(f" method: {l['method']}")
|
|
486
|
-
concept.append("---")
|
|
487
|
-
concept.append("")
|
|
488
|
-
concept.append(f"# {s['id']}: {s['title']}")
|
|
489
|
-
concept.append("")
|
|
490
|
-
concept.append(f"**Epic:** {s['epic_id']} — {s.get('epic_title', 'Unknown')}")
|
|
491
|
-
concept.append(f"**Status:** {s['status']}")
|
|
492
|
-
concept.append(f"**BCP:** {s['bcp']} | **WSJF:** {s['wsjf']}")
|
|
493
|
-
concept.append("")
|
|
494
|
-
if s["links"]:
|
|
495
|
-
concept.append("## Implemented In")
|
|
496
|
-
concept.append("")
|
|
497
|
-
for l in s["links"]:
|
|
498
|
-
concept.append(f"- `{l['file']}` (line {l['line']}, confidence: {l['confidence']}, method: {l['method']})")
|
|
499
|
-
else:
|
|
500
|
-
concept.append("*No code links found — this is a dark story.*")
|
|
501
|
-
concept.append("")
|
|
502
|
-
(OKF_DIR / f"{s['id']}.md").write_text("\n".join(concept), encoding="utf-8")
|
|
228
|
+
# Emit outputs via trace_renderer
|
|
229
|
+
matrix = emit_matrix_json(MATRIX_JSON, stories, tagged_sids, dark_stories, orphan_tags, stale_tags, matrix_stories)
|
|
230
|
+
emit_trace_md(TRACE_MD, stories, tagged_sids, dark_stories, orphan_tags, stale_tags, matrix_stories, matrix, dev_status)
|
|
231
|
+
emit_okf_bundle(OKF_DIR, stories, matrix_stories)
|
|
503
232
|
|
|
504
233
|
# -----------------------------------------------------------------------
|
|
505
234
|
# 8. Strict mode — exit non-zero on P0 uncovered
|