@trycore/spec-build-harness 0.8.5 → 0.10.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/.claude-plugin/plugin.json +1 -1
- package/GOVERNANCE.md +27 -4
- package/INSTALL.md +27 -5
- package/METODOLOGIA.md +55 -5
- package/README.md +39 -6
- package/VERSION +1 -1
- package/agents/build/build-orchestrator.md +33 -7
- package/agents/build/dor-dod-gatekeeper.md +13 -5
- package/agents/build/wiring-adversarial-verifier.md +52 -5
- package/commands/build/architect.md +1 -1
- package/commands/build/claim.md +46 -0
- package/commands/build/escalate.md +36 -0
- package/commands/build/front.md +9 -3
- package/commands/build/onboard.md +59 -14
- package/commands/build/prototype.md +3 -2
- package/commands/build/reflect.md +60 -40
- package/commands/build/release.md +10 -7
- package/commands/build/resume.md +33 -13
- package/commands/build/slice.md +32 -27
- package/commands/build/status.md +35 -0
- package/commands/build/work.md +11 -8
- package/config/build-config.template.json +4 -0
- package/dist/cli.js +22 -0
- package/dist/commands/doctor.js +42 -0
- package/dist/commands/init.js +84 -1
- package/dist/commands/migrate.js +48 -0
- package/dist/commands/status.js +34 -0
- package/dist/lib/normalize.js +276 -0
- package/dist/lib/paths.js +6 -0
- package/dist/lib/runtime-client.js +196 -0
- package/dist/lib/settings-merge.js +3 -3
- package/dist/lib/state-bundle.js +46 -0
- package/docs/commands.md +25 -8
- package/docs/getting-started.md +1 -0
- package/docs/hooks.md +114 -27
- package/docs/runtime/guia-modo-dual-y-migracion.md +136 -0
- package/docs/runtime/plan-migracion-harness-v0.9.md +11 -0
- package/docs/runtime/protocolo-cliente-runtime.md +109 -34
- package/hooks/build/build-gate-check.sh +21 -0
- package/hooks/build/context-monitor.sh +82 -15
- package/hooks/build/context-sync.sh +192 -0
- package/hooks/build/design-source-guard.sh +30 -2
- package/hooks/build/dual-compare.sh +92 -0
- package/hooks/build/event-emitter.sh +75 -0
- package/hooks/build/gitflow-guard.sh +164 -14
- package/hooks/build/heartbeat.sh +259 -0
- package/hooks/build/lib/agent-context.sh +139 -0
- package/hooks/build/lib/config.sh +27 -0
- package/hooks/build/lib/projection.sh +71 -0
- package/hooks/build/lib/runtime-client.sh +465 -0
- package/hooks/build/lib/runtime-ops.sh +221 -0
- package/hooks/build/lib/state-io.sh +5 -18
- package/hooks/build/load-build-state.sh +64 -2
- package/hooks/build/reflect-nudge.sh +15 -0
- package/hooks/build/release-gate-nudge.sh +15 -0
- package/hooks/build/release-ops.sh +164 -0
- package/hooks/build/scaffold-guard.sh +29 -2
- package/hooks/build/session-start.sh +103 -0
- package/hooks/build/session-stop.sh +22 -0
- package/hooks/build/slice-ops.sh +877 -0
- package/hooks/build/stack-guard.sh +8 -0
- package/hooks/build/statusline-bridge.sh +24 -3
- package/hooks/build-harness.json +16 -0
- package/package.json +3 -3
- package/scripts/check-agnostic.sh +3 -1
- package/scripts/check-pack-clean.sh +31 -0
- package/scripts/check-runtime-purity.sh +43 -0
- package/scripts/lib/front-plan.py +4 -0
- package/scripts/lib/graph-bundle.py +133 -0
- package/scripts/runtime-purity-allow.txt +5 -0
- package/scripts/smoke-test.sh +1 -1
- package/scripts/tests/lib/http-stub.py +46 -0
- package/scripts/tests/test-baseline-verdict.sh +92 -0
- package/scripts/tests/test-config.sh +25 -0
- package/scripts/tests/test-hooks-runtime.sh +853 -0
- package/scripts/tests/test-install.sh +57 -0
- package/scripts/tests/test-runtime-client.sh +298 -0
- package/scripts/tests/test-schema.sh +29 -1
- package/scripts/tests/test-skill-ops.sh +847 -0
- package/skills/building-a-micro-change/SKILL.md +22 -4
- package/skills/building-a-slice/SKILL.md +55 -21
- package/skills/building-a-slice/assets/baseline-verdict.sh +172 -0
- package/skills/building-a-slice/references/dod.md +12 -3
- package/skills/building-a-slice/references/dor.md +3 -2
- package/skills/building-a-slice/references/evidence-budget.md +51 -0
- package/skills/building-a-slice/references/exploration-fanout.md +1 -1
- package/skills/building-a-slice/references/gitflow.md +1 -1
- package/skills/building-a-slice/references/regression-baseline.md +67 -0
- package/skills/building-a-slice/references/runtime-protocol.md +75 -0
- package/skills/building-a-slice/references/state-protocol.md +12 -1
- package/skills/building-a-slice/workflows/README.md +7 -3
- package/skills/building-a-slice/workflows/explore-fanout.workflow.js +3 -3
- package/skills/building-a-slice/workflows/wiring-verify.workflow.js +26 -4
- package/skills/managing-parallel-front/SKILL.md +32 -16
- package/skills/openspec-archive-change/SKILL.md +15 -0
- package/skills/prototyping-screens/SKILL.md +9 -5
- package/skills/releasing-a-version/SKILL.md +25 -16
- package/skills/releasing-a-version/references/release-dod.md +7 -5
- package/skills/releasing-a-version/workflows/README.md +2 -1
- package/skills/releasing-a-version/workflows/release-gate.workflow.js +6 -5
- package/skills/setup-architecture/SKILL.md +4 -2
- package/state/README.md +16 -1
- package/state/build-state.schema.json +2 -1
- package/templates/CLAUDE.md.template +16 -0
- package/templates/settings-hooks.template.json +8 -4
- package/internal/skills/auditar-arnes/SKILL.md +0 -29
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
set -uo pipefail
|
|
7
7
|
|
|
8
8
|
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
9
|
+
# [EP-OR-08-B] El allowlist deja de ser un fichero que edita un humano: en dual/runtime lo
|
|
10
|
+
# escribe context-sync.sh desde el asset gobernado del runtime. El guard sigue leyéndolo de
|
|
11
|
+
# disco (cero red en PreToolUse); solo avisa si el contexto local está sin sincronizar.
|
|
12
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
source "$HERE/lib/projection.sh"
|
|
9
14
|
ALLOW="$ROOT/.claude/config/stack-allowlist.json"
|
|
10
15
|
[ -f "$ALLOW" ] || exit 0
|
|
11
16
|
|
|
@@ -80,6 +85,9 @@ if [ -n "$VIOL" ]; then
|
|
|
80
85
|
echo "⛔ stack-guard: dependencias fuera del stack declarado del PRD (allowlist): $VIOL" >&2
|
|
81
86
|
echo " Si es intencional, actualiza .claude/config/stack-allowlist.json y deja nota en GOVERNANCE.md," >&2
|
|
82
87
|
echo " o consulta al agente stack-guardian. Allowlist: .claude/config/stack-allowlist.json" >&2
|
|
88
|
+
if [ "$(runtime_mode)" != "legacy" ] && projection_is_stale; then
|
|
89
|
+
echo " ⚠ Contexto local sin sincronizar (stale): el allowlist puede estar desactualizado respecto al runtime." >&2
|
|
90
|
+
fi
|
|
83
91
|
exit 2
|
|
84
92
|
fi
|
|
85
93
|
exit 0
|
|
@@ -1,15 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# statusline-bridge.sh — statusLine command (canal CLI). Imprime la línea de estado
|
|
3
3
|
# y escribe el puente de contexto que lee context-monitor.sh. Fail-open siempre.
|
|
4
|
+
# [EP-OR-08-B] En dual/runtime añade el plano de contexto: versión sincronizada (✓ vN),
|
|
5
|
+
# proyección sin refrescar (⚠ stale) y lease perdido (⚠ lease). Cero red: todo sale de
|
|
6
|
+
# ficheros locales (caché de proyección y estado del daemon).
|
|
4
7
|
set -uo pipefail
|
|
8
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
9
|
payload="$(cat)"
|
|
6
10
|
command -v python3 >/dev/null 2>&1 || { echo "🏗️ build"; exit 0; }
|
|
7
|
-
|
|
11
|
+
|
|
12
|
+
source "$HERE/lib/runtime-client.sh"
|
|
13
|
+
source "$HERE/lib/projection.sh"
|
|
14
|
+
|
|
15
|
+
CTXTAG=""
|
|
16
|
+
if [ "$(runtime_mode)" != "legacy" ]; then
|
|
17
|
+
if projection_is_stale; then
|
|
18
|
+
CTXTAG=" · ⚠ stale"
|
|
19
|
+
else
|
|
20
|
+
ver="$(projection_get context.version "")"
|
|
21
|
+
[ -n "$ver" ] && CTXTAG=" · ✓ v$ver"
|
|
22
|
+
fi
|
|
23
|
+
if grep -q '"lease_lost": true' "$(config_root)/.claude/state/heartbeat-status.json" 2>/dev/null; then
|
|
24
|
+
CTXTAG="$CTXTAG · ⚠ lease"
|
|
25
|
+
fi
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
echo "$payload" | CTXTAG="$CTXTAG" python3 -c '
|
|
8
29
|
import json,sys,os,time,re,tempfile
|
|
9
30
|
try:
|
|
10
31
|
p=json.load(sys.stdin)
|
|
11
32
|
except Exception:
|
|
12
|
-
print("🏗️ build"); sys.exit(0)
|
|
33
|
+
print("🏗️ build"+os.environ.get("CTXTAG","")); sys.exit(0)
|
|
13
34
|
sid=str(p.get("session_id","default"))
|
|
14
35
|
if re.search(r"[\\/]|\.\.", sid): sid=re.sub(r"[^A-Za-z0-9_-]","_",sid) # sanitiza path traversal
|
|
15
36
|
rem=None
|
|
@@ -28,5 +49,5 @@ if rem is not None:
|
|
|
28
49
|
except OSError: pass
|
|
29
50
|
except Exception: pass
|
|
30
51
|
tag = f"🏗️ build · ctx {rem}%" if rem is not None else "🏗️ build"
|
|
31
|
-
print(tag)
|
|
52
|
+
print(tag+os.environ.get("CTXTAG",""))
|
|
32
53
|
' 2>/dev/null || echo "🏗️ build"
|
package/hooks/build-harness.json
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
{
|
|
5
5
|
"matcher": "startup|clear|compact",
|
|
6
6
|
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/session-start.sh\""
|
|
10
|
+
},
|
|
7
11
|
{
|
|
8
12
|
"type": "command",
|
|
9
13
|
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/load-build-state.sh\""
|
|
@@ -59,6 +63,10 @@
|
|
|
59
63
|
{
|
|
60
64
|
"type": "command",
|
|
61
65
|
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/context-monitor.sh\""
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"type": "command",
|
|
69
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/event-emitter.sh\""
|
|
62
70
|
}
|
|
63
71
|
]
|
|
64
72
|
}
|
|
@@ -90,9 +98,17 @@
|
|
|
90
98
|
"type": "command",
|
|
91
99
|
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/release-gate-nudge.sh\""
|
|
92
100
|
},
|
|
101
|
+
{
|
|
102
|
+
"type": "command",
|
|
103
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/dual-compare.sh\""
|
|
104
|
+
},
|
|
93
105
|
{
|
|
94
106
|
"type": "command",
|
|
95
107
|
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/context-monitor.sh\""
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"type": "command",
|
|
111
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}/hooks/build/session-stop.sh\""
|
|
96
112
|
}
|
|
97
113
|
]
|
|
98
114
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trycore/spec-build-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Arnés agéntico de construcción de Trycore para Claude Code: pipeline de dos loops (slice por épica + release gate) con gates de calidad, estado compartido y OpenSpec. Compañero de @trycore/spec-product-flow. Agnóstico al proyecto.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
"state/",
|
|
18
18
|
"templates/",
|
|
19
19
|
"asset-types.json",
|
|
20
|
-
"internal/",
|
|
21
20
|
"docs/",
|
|
22
21
|
"!docs/superpowers",
|
|
23
22
|
"scripts/",
|
|
@@ -32,7 +31,8 @@
|
|
|
32
31
|
},
|
|
33
32
|
"scripts": {
|
|
34
33
|
"build": "tsc",
|
|
35
|
-
"
|
|
34
|
+
"test:unit": "tsc -p tsconfig.test.json && find dist-test -name '*.test.js' -print0 | xargs -0 node --test",
|
|
35
|
+
"prepublishOnly": "npm run build && npm run test:unit && bash scripts/check-version-sync.sh && bash scripts/check-agnostic.sh && bash scripts/check-runtime-purity.sh && bash scripts/check-state-clean.sh && bash scripts/check-pack-clean.sh",
|
|
36
36
|
"test:smoke": "bash scripts/smoke-test.sh"
|
|
37
37
|
},
|
|
38
38
|
"keywords": [
|
|
@@ -47,6 +47,8 @@ TARGETS=(
|
|
|
47
47
|
"$REPO_ROOT/config"
|
|
48
48
|
"$REPO_ROOT/state"
|
|
49
49
|
"$REPO_ROOT/templates"
|
|
50
|
+
"$REPO_ROOT/src"
|
|
51
|
+
"$REPO_ROOT/asset-types.json"
|
|
50
52
|
"$REPO_ROOT/internal"
|
|
51
53
|
"$REPO_ROOT/docs/customization"
|
|
52
54
|
"$REPO_ROOT/METODOLOGIA.md"
|
|
@@ -59,7 +61,7 @@ TARGETS=(
|
|
|
59
61
|
for target in "${TARGETS[@]}"; do
|
|
60
62
|
[[ -e "$target" ]] || continue
|
|
61
63
|
hits=$(grep -riEn -- "$PATTERN" "$target" 2>/dev/null \
|
|
62
|
-
--include='*.md' --include='*.json' --include='*.sh' --include='*.js' --include='*.mjs' \
|
|
64
|
+
--include='*.md' --include='*.json' --include='*.sh' --include='*.js' --include='*.mjs' --include='*.ts' \
|
|
63
65
|
--exclude-dir='node_modules' || true)
|
|
64
66
|
if [[ -n "$hits" ]]; then
|
|
65
67
|
VIOLATIONS+=("$hits")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-pack-clean: verifica que el tarball de npm NO incluya contenido no distribuible.
|
|
3
|
+
# Regla del repo (CLAUDE.md): MAINTAINERS.md, internal/ y docs/superpowers/ no se distribuyen en npm.
|
|
4
|
+
# Corre en prepublishOnly: si algo de eso entra al pack, bloquea la publicación.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
8
|
+
cd "$REPO_ROOT"
|
|
9
|
+
|
|
10
|
+
echo "▶ check-pack-clean: verificando el contenido del tarball (npm pack --dry-run)…"
|
|
11
|
+
|
|
12
|
+
# Lista de rutas prohibidas en el paquete publicado (relativas a la raíz del tarball).
|
|
13
|
+
FORBIDDEN='^(internal/|MAINTAINERS\.md$|docs/superpowers/)'
|
|
14
|
+
|
|
15
|
+
FILES="$(npm pack --dry-run --json 2>/dev/null | python3 -c '
|
|
16
|
+
import json, sys
|
|
17
|
+
data = json.load(sys.stdin)
|
|
18
|
+
for f in data[0]["files"]:
|
|
19
|
+
print(f["path"])
|
|
20
|
+
')"
|
|
21
|
+
|
|
22
|
+
LEAKS="$(printf '%s\n' "$FILES" | grep -E "$FORBIDDEN" || true)"
|
|
23
|
+
|
|
24
|
+
if [ -n "$LEAKS" ]; then
|
|
25
|
+
echo "✗ Contenido NO distribuible dentro del tarball:" >&2
|
|
26
|
+
printf '%s\n' "$LEAKS" | sed 's/^/ /' >&2
|
|
27
|
+
echo " Revisa package.json#files (regla: MAINTAINERS.md, internal/ y docs/superpowers/ no se publican)." >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
echo "✓ Tarball limpio: sin internal/, MAINTAINERS.md ni docs/superpowers/."
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-runtime-purity.sh — ratchet de la migración v0.9 [EP-OR-08-C].
|
|
3
|
+
# Prohíbe que skills/, commands/ y agents/ instruyan el protocolo del FICHERO local
|
|
4
|
+
# (.claude/state/build-state.json) fuera de dos excepciones declaradas:
|
|
5
|
+
# (a) los ficheros listados en scripts/runtime-purity-allow.txt — documentación del
|
|
6
|
+
# modo `legacy`, que vive mientras viva legacy (spec §4.2-E);
|
|
7
|
+
# (b) líneas que declaran explícitamente el modo `legacy` (una tabla de modos, una nota
|
|
8
|
+
# «en legacy … »): ahí la mención es correcta y necesaria.
|
|
9
|
+
# Salida 0 si limpio, 1 si hay menciones fuera de esas excepciones. Corre en prepublishOnly.
|
|
10
|
+
set -uo pipefail
|
|
11
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
12
|
+
ALLOW="$ROOT/scripts/runtime-purity-allow.txt"
|
|
13
|
+
PATTERN='build-state\.json|build-state\.schema\.json|build_state'
|
|
14
|
+
|
|
15
|
+
hits="$(grep -rEn -- "$PATTERN" "$ROOT/skills" "$ROOT/commands" "$ROOT/agents" \
|
|
16
|
+
--include='*.md' --include='*.js' --include='*.json' 2>/dev/null || true)"
|
|
17
|
+
|
|
18
|
+
fail=0
|
|
19
|
+
violaciones=""
|
|
20
|
+
while IFS= read -r linea; do
|
|
21
|
+
[ -n "$linea" ] || continue
|
|
22
|
+
archivo="${linea%%:*}"
|
|
23
|
+
rel="${archivo#$ROOT/}"
|
|
24
|
+
if [ -f "$ALLOW" ] && grep -qxF "$rel" "$ALLOW" 2>/dev/null; then continue; fi
|
|
25
|
+
# (b) la línea declara el modo legacy
|
|
26
|
+
case "$linea" in
|
|
27
|
+
*legacy*|*LEGACY*|*Legacy*) continue ;;
|
|
28
|
+
esac
|
|
29
|
+
violaciones="$violaciones
|
|
30
|
+
$linea"
|
|
31
|
+
fail=1
|
|
32
|
+
done <<< "$hits"
|
|
33
|
+
|
|
34
|
+
if [ "$fail" = 0 ]; then
|
|
35
|
+
echo "✓ check-runtime-purity: skills/commands/agents no instruyen el fichero de estado fuera de legacy."
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
echo "✗ check-runtime-purity: menciones del fichero de estado fuera de las excepciones declaradas:"
|
|
39
|
+
echo "$violaciones"
|
|
40
|
+
echo ""
|
|
41
|
+
echo "Migra el protocolo a slice-ops.sh/release-ops.sh, o declara la línea como modo legacy,"
|
|
42
|
+
echo "o añade el fichero a scripts/runtime-purity-allow.txt si es documentación de legacy."
|
|
43
|
+
exit 1
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""front-plan.py — selecciona el conjunto disjunto máximo de épicas no fundacionales.
|
|
3
3
|
|
|
4
|
+
[EP-OR-08-C] Este script SIGUE SIENDO LOCAL (spec §4.2-C rev. 4): es el generador del plan
|
|
5
|
+
(conjunto disjunto + merge_order) que un humano aprueba al abrir el front en la consola del
|
|
6
|
+
hub. No llama al runtime ni lee el estado: entra JSON por stdin, sale JSON por stdout.
|
|
7
|
+
|
|
4
8
|
stdin: JSON [{"epica","layer","files_scope":[glob]}]
|
|
5
9
|
stdout: {"selected":[...],"serialized":[{"epica","reason"}],"excluded_foundational":[...]}
|
|
6
10
|
Determinista: orden por 'epica'. Solape de globs por prefijo de directorio o glob idéntico.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""graph-bundle.py — prepara el BUNDLE DE GRAFO que un ADMIN sube en la consola del hub.
|
|
3
|
+
|
|
4
|
+
[EP-OR-08-C] El import del grafo es un acto de GOBIERNO (spec §4.1-D rev. 4): la superficie
|
|
5
|
+
`POST …/graph/import` exige sesión de ADMIN y rechaza el token de agente. Por eso el arnés
|
|
6
|
+
**prepara y valida** el bundle en local — nunca lo sube — y `/build:onboard` se lo entrega a
|
|
7
|
+
una persona.
|
|
8
|
+
|
|
9
|
+
Uso:
|
|
10
|
+
python3 graph-bundle.py < epics.json > bundle.json # avisos por stderr
|
|
11
|
+
|
|
12
|
+
Entrada (stdin, JSON):
|
|
13
|
+
{"project_ref": "…",
|
|
14
|
+
"epics": [{"code","title","layer","files_scope":[],"depends_on":[],
|
|
15
|
+
"stories":[{"id","title"}]}],
|
|
16
|
+
"release_lines": [{"id","epics":[]}]}
|
|
17
|
+
|
|
18
|
+
Salida (stdout, JSON): el bundle normalizado y DETERMINISTA (orden estable por código);
|
|
19
|
+
los problemas no bloquean: se listan como avisos en stderr y en `warnings` del bundle, con
|
|
20
|
+
el criterio del arnés — nada se pierde en silencio, nada bloquea la preparación.
|
|
21
|
+
|
|
22
|
+
Códigos de salida: 0 = bundle generado (con o sin avisos) · 1 = entrada ilegible.
|
|
23
|
+
"""
|
|
24
|
+
import json
|
|
25
|
+
import sys
|
|
26
|
+
import datetime
|
|
27
|
+
|
|
28
|
+
BUNDLE_VERSION = 1
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def norm_list(v):
|
|
32
|
+
return [x for x in v if isinstance(x, str)] if isinstance(v, list) else []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main():
|
|
36
|
+
avisos = []
|
|
37
|
+
try:
|
|
38
|
+
raw = json.load(sys.stdin)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
print("graph-bundle: entrada ilegible (se esperaba JSON por stdin): %s" % e,
|
|
41
|
+
file=sys.stderr)
|
|
42
|
+
return 1
|
|
43
|
+
if not isinstance(raw, dict):
|
|
44
|
+
print("graph-bundle: la entrada debe ser un objeto JSON", file=sys.stderr)
|
|
45
|
+
return 1
|
|
46
|
+
|
|
47
|
+
epicas = {}
|
|
48
|
+
for e in raw.get("epics") or []:
|
|
49
|
+
if not isinstance(e, dict):
|
|
50
|
+
continue
|
|
51
|
+
code = e.get("code")
|
|
52
|
+
if not isinstance(code, str) or not code:
|
|
53
|
+
avisos.append("épica sin `code`: descartada")
|
|
54
|
+
continue
|
|
55
|
+
if code in epicas:
|
|
56
|
+
avisos.append("código duplicado %s: se conserva la PRIMERA aparición" % code)
|
|
57
|
+
continue
|
|
58
|
+
if not e.get("layer"):
|
|
59
|
+
avisos.append("%s sin `layer`: el DoR no podrá aplicar la regla de cimiento" % code)
|
|
60
|
+
historias = []
|
|
61
|
+
for h in e.get("stories") or []:
|
|
62
|
+
if isinstance(h, dict) and isinstance(h.get("id"), str):
|
|
63
|
+
historias.append({"id": h["id"], "title": h.get("title")})
|
|
64
|
+
epicas[code] = {
|
|
65
|
+
"code": code,
|
|
66
|
+
"title": e.get("title"),
|
|
67
|
+
"layer": e.get("layer"),
|
|
68
|
+
"files_scope": sorted(norm_list(e.get("files_scope"))),
|
|
69
|
+
"depends_on": sorted(norm_list(e.get("depends_on"))),
|
|
70
|
+
"stories": sorted(historias, key=lambda h: h["id"]),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for code, e in epicas.items():
|
|
74
|
+
for dep in e["depends_on"]:
|
|
75
|
+
if dep not in epicas:
|
|
76
|
+
avisos.append("%s depende de %s, que no está en el grafo" % (code, dep))
|
|
77
|
+
|
|
78
|
+
# Ciclos: DFS iterativa (nada de recursión: un grafo hostil no debe reventar la pila).
|
|
79
|
+
estado = {}
|
|
80
|
+
for raiz in sorted(epicas):
|
|
81
|
+
if estado.get(raiz):
|
|
82
|
+
continue
|
|
83
|
+
pila = [(raiz, iter(epicas[raiz]["depends_on"]))]
|
|
84
|
+
estado[raiz] = 1
|
|
85
|
+
while pila:
|
|
86
|
+
nodo, it = pila[-1]
|
|
87
|
+
avanzo = False
|
|
88
|
+
for dep in it:
|
|
89
|
+
if dep not in epicas:
|
|
90
|
+
continue
|
|
91
|
+
if estado.get(dep) == 1:
|
|
92
|
+
avisos.append("ciclo de dependencias detectado: %s → %s" % (nodo, dep))
|
|
93
|
+
continue
|
|
94
|
+
if not estado.get(dep):
|
|
95
|
+
estado[dep] = 1
|
|
96
|
+
pila.append((dep, iter(epicas[dep]["depends_on"])))
|
|
97
|
+
avanzo = True
|
|
98
|
+
break
|
|
99
|
+
if not avanzo:
|
|
100
|
+
estado[nodo] = 2
|
|
101
|
+
pila.pop()
|
|
102
|
+
|
|
103
|
+
lineas = []
|
|
104
|
+
for r in raw.get("release_lines") or []:
|
|
105
|
+
if not isinstance(r, dict) or not isinstance(r.get("id"), str):
|
|
106
|
+
continue
|
|
107
|
+
miembros = [c for c in norm_list(r.get("epics"))]
|
|
108
|
+
for c in miembros:
|
|
109
|
+
if c not in epicas:
|
|
110
|
+
avisos.append("la línea %s referencia %s, que no está en el grafo" % (r["id"], c))
|
|
111
|
+
lineas.append({"id": r["id"], "epics": miembros})
|
|
112
|
+
lineas.sort(key=lambda r: r["id"])
|
|
113
|
+
|
|
114
|
+
bundle = {
|
|
115
|
+
"bundle_version": BUNDLE_VERSION,
|
|
116
|
+
"kind": "graph",
|
|
117
|
+
"project_ref": raw.get("project_ref"),
|
|
118
|
+
"generated_at": datetime.datetime.now(datetime.timezone.utc)
|
|
119
|
+
.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
120
|
+
"generated_by": "trycore-build-harness/graph-bundle.py",
|
|
121
|
+
"epics": [epicas[c] for c in sorted(epicas)],
|
|
122
|
+
"release_lines": lineas,
|
|
123
|
+
"warnings": avisos,
|
|
124
|
+
}
|
|
125
|
+
json.dump(bundle, sys.stdout, indent=2, ensure_ascii=False, sort_keys=False)
|
|
126
|
+
sys.stdout.write("\n")
|
|
127
|
+
for a in avisos:
|
|
128
|
+
print("aviso: %s" % a, file=sys.stderr)
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
sys.exit(main())
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Ficheros que TODAVÍA instruyen el protocolo del fichero de estado local.
|
|
2
|
+
# Es un ratchet: cada tarea de EP-OR-08-C quita los suyos y el check impide que vuelvan.
|
|
3
|
+
# Al cerrar el sub-slice debe quedar SOLO el protocolo legacy, que vive mientras viva
|
|
4
|
+
# legacy (spec §4.2-E).
|
|
5
|
+
skills/building-a-slice/references/state-protocol.md
|
package/scripts/smoke-test.sh
CHANGED
|
@@ -5,7 +5,7 @@ DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
5
5
|
FAILED=0
|
|
6
6
|
|
|
7
7
|
echo "── Tests del motor de contexto y front ──"
|
|
8
|
-
for t in test-schema.sh test-reconciler.sh test-context-monitor.sh test-front-plan.sh test-install.sh; do
|
|
8
|
+
for t in test-schema.sh test-config.sh test-runtime-client.sh test-hooks-runtime.sh test-skill-ops.sh test-reconciler.sh test-context-monitor.sh test-front-plan.sh test-install.sh; do
|
|
9
9
|
if bash "$DIR/tests/$t"; then echo "smoke: $t OK"; else echo "smoke: $t FALLÓ"; FAILED=1; fi
|
|
10
10
|
done
|
|
11
11
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Stub HTTP mínimo del runtime para tests de runtime-client.sh, sin depender del hub
|
|
3
|
+
real (que se construye en otro repo/sesión — EP-OR-13). Uso:
|
|
4
|
+
python3 http-stub.py <port> <routes.json> <calls-log.jsonl>
|
|
5
|
+
routes.json: {"METHOD /path": {"status": 200, "body": {...}}, ...} — se relee EN CADA
|
|
6
|
+
request (no solo al arrancar), así un test puede reescribir routes.json a mitad de
|
|
7
|
+
ejecución para simular una segunda respuesta del mismo endpoint.
|
|
8
|
+
Cada request recibida se apendiza como una línea JSON en calls-log.jsonl (método, path,
|
|
9
|
+
header Authorization, body) para que el test verifique lo que el cliente envió.
|
|
10
|
+
"""
|
|
11
|
+
import sys, json, http.server
|
|
12
|
+
|
|
13
|
+
PORT = int(sys.argv[1])
|
|
14
|
+
ROUTES_PATH = sys.argv[2]
|
|
15
|
+
CALLS_LOG = sys.argv[3]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
19
|
+
def _handle(self, method):
|
|
20
|
+
length = int(self.headers.get('Content-Length', 0) or 0)
|
|
21
|
+
body = self.rfile.read(length) if length else b''
|
|
22
|
+
with open(CALLS_LOG, 'a') as f:
|
|
23
|
+
f.write(json.dumps({
|
|
24
|
+
"method": method, "path": self.path,
|
|
25
|
+
"auth": self.headers.get('Authorization', ''),
|
|
26
|
+
"body": body.decode('utf-8', 'replace'),
|
|
27
|
+
}) + "\n")
|
|
28
|
+
try:
|
|
29
|
+
routes = json.loads(open(ROUTES_PATH).read())
|
|
30
|
+
except Exception:
|
|
31
|
+
routes = {}
|
|
32
|
+
key = f"{method} {self.path}"
|
|
33
|
+
route = routes.get(key, {"status": 404, "body": {"error": "not_found"}})
|
|
34
|
+
self.send_response(route["status"])
|
|
35
|
+
self.send_header('Content-Type', 'application/json')
|
|
36
|
+
self.end_headers()
|
|
37
|
+
self.wfile.write(json.dumps(route["body"]).encode('utf-8'))
|
|
38
|
+
|
|
39
|
+
def do_GET(self): self._handle("GET")
|
|
40
|
+
def do_POST(self): self._handle("POST")
|
|
41
|
+
def do_PUT(self): self._handle("PUT")
|
|
42
|
+
def log_message(self, *a): pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
http.server.HTTPServer(("127.0.0.1", PORT), Handler).serve_forever()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test-baseline-verdict.sh — prueba baseline-verdict.sh (skill building-a-slice) contra
|
|
3
|
+
# un repo git temporal con remoto local y una suite fake de rojos controlados.
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
6
|
+
SCRIPT="$ROOT/skills/building-a-slice/assets/baseline-verdict.sh"
|
|
7
|
+
[ -f "$SCRIPT" ] || { echo "FAIL no existe baseline-verdict.sh"; exit 1; }
|
|
8
|
+
fail=0
|
|
9
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
10
|
+
|
|
11
|
+
# ── Fixture: remoto bare + clon con suite fake ───────────────────────────────
|
|
12
|
+
git init --quiet --bare "$TMP/remote.git"
|
|
13
|
+
git clone --quiet "$TMP/remote.git" "$TMP/proj" 2>/dev/null
|
|
14
|
+
cd "$TMP/proj"
|
|
15
|
+
git config user.email t@t.local && git config user.name t
|
|
16
|
+
git symbolic-ref HEAD refs/heads/main
|
|
17
|
+
|
|
18
|
+
# Suite fake agnóstica: cada tests/*.txt con contenido "red" pinta "FAIL <fichero>".
|
|
19
|
+
cat > suite.sh <<'SH'
|
|
20
|
+
#!/usr/bin/env bash
|
|
21
|
+
rc=0
|
|
22
|
+
for f in tests/*.txt; do
|
|
23
|
+
if grep -q red "$f"; then echo "FAIL $f"; rc=1; else echo "PASS $f"; fi
|
|
24
|
+
done
|
|
25
|
+
exit $rc
|
|
26
|
+
SH
|
|
27
|
+
mkdir tests
|
|
28
|
+
echo red > tests/heredado.txt # rojo ambiental heredado
|
|
29
|
+
echo green > tests/sano.txt
|
|
30
|
+
git add -A && git commit --quiet -m base && git push --quiet -u origin main 2>/dev/null
|
|
31
|
+
|
|
32
|
+
export BASELINE_SUITE_CMD="bash suite.sh"
|
|
33
|
+
export BASELINE_LINK_DIRS=""
|
|
34
|
+
|
|
35
|
+
# ── capture contra origin/main escribe el JSON con el rojo heredado ──────────
|
|
36
|
+
SHA="$(git rev-parse origin/main)"
|
|
37
|
+
bash "$SCRIPT" capture origin/main >/dev/null 2>&1
|
|
38
|
+
[ $? -eq 0 ] && echo "OK capture rc=0" || { echo "FAIL capture rc"; fail=1; }
|
|
39
|
+
CACHE=".baseline/baseline-verdict.${SHA}.json"
|
|
40
|
+
[ -f "$CACHE" ] && echo "OK capture escribe JSON por sha" || { echo "FAIL falta $CACHE"; fail=1; }
|
|
41
|
+
python3 - "$CACHE" "$SHA" <<'PY' && echo "OK contenido del veredicto" || { echo "FAIL contenido del veredicto"; fail=1; }
|
|
42
|
+
import json, sys
|
|
43
|
+
d = json.load(open(sys.argv[1]))
|
|
44
|
+
assert d["sha"] == sys.argv[2], d["sha"]
|
|
45
|
+
assert d["red_files"] == ["tests/heredado.txt"], d["red_files"]
|
|
46
|
+
assert d["captured_at"] and d["suite_cmd"], d
|
|
47
|
+
PY
|
|
48
|
+
|
|
49
|
+
# ── compare sin cambios: heredado no cuenta como regresión ───────────────────
|
|
50
|
+
out="$(bash "$SCRIPT" compare origin/main 2>&1)"; rc=$?
|
|
51
|
+
[ $rc -eq 0 ] && echo "OK compare limpio rc=0" || { echo "FAIL compare limpio (rc=$rc)"; fail=1; }
|
|
52
|
+
echo "$out" | grep -q "heredados sin cambio: 1" && echo "OK reporta heredado" || { echo "FAIL no reporta heredado"; fail=1; }
|
|
53
|
+
|
|
54
|
+
# ── regresión nueva en el working tree → rc=1 y la nombra ────────────────────
|
|
55
|
+
echo red > tests/sano.txt
|
|
56
|
+
out="$(bash "$SCRIPT" compare origin/main 2>&1)"; rc=$?
|
|
57
|
+
[ $rc -eq 1 ] && echo "OK compare detecta regresión (rc=1)" || { echo "FAIL rc regresión (rc=$rc)"; fail=1; }
|
|
58
|
+
echo "$out" | grep -q "tests/sano.txt" && echo "OK nombra el fichero regresado" || { echo "FAIL no nombra la regresión"; fail=1; }
|
|
59
|
+
|
|
60
|
+
# ── arreglo del heredado → rc=0 y cuenta 1 arreglado ─────────────────────────
|
|
61
|
+
echo green > tests/sano.txt
|
|
62
|
+
echo green > tests/heredado.txt
|
|
63
|
+
out="$(bash "$SCRIPT" compare origin/main 2>&1)"; rc=$?
|
|
64
|
+
[ $rc -eq 0 ] && echo "OK compare tras arreglo rc=0" || { echo "FAIL rc tras arreglo (rc=$rc)"; fail=1; }
|
|
65
|
+
echo "$out" | grep -q "arreglados : 1" && echo "OK cuenta arreglado" || { echo "FAIL no cuenta arreglado"; fail=1; }
|
|
66
|
+
git checkout --quiet -- tests
|
|
67
|
+
|
|
68
|
+
# ── GUARD: ref local rechazado con rc=2 ──────────────────────────────────────
|
|
69
|
+
out="$(bash "$SCRIPT" capture main 2>&1)"; rc=$?
|
|
70
|
+
[ $rc -eq 2 ] && echo "OK guard rechaza ref local (rc=2)" || { echo "FAIL guard ref local (rc=$rc)"; fail=1; }
|
|
71
|
+
echo "$out" | grep -qi "origin/<destino>" && echo "OK guard explica el porqué" || { echo "FAIL guard sin mensaje"; fail=1; }
|
|
72
|
+
|
|
73
|
+
# ── GUARD: sha solo-local (no pusheado) rechazado ────────────────────────────
|
|
74
|
+
git checkout --quiet -b feature/local
|
|
75
|
+
echo x > local.txt && git add local.txt && git commit --quiet -m local
|
|
76
|
+
LOCAL_SHA="$(git rev-parse HEAD)"
|
|
77
|
+
out="$(bash "$SCRIPT" capture "$LOCAL_SHA" 2>&1)"; rc=$?
|
|
78
|
+
[ $rc -eq 2 ] && echo "OK guard rechaza sha no remoto (rc=2)" || { echo "FAIL guard sha local (rc=$rc)"; fail=1; }
|
|
79
|
+
git checkout --quiet main
|
|
80
|
+
|
|
81
|
+
# ── sha remoto verificado sí pasa el guard (capture por sha) ─────────────────
|
|
82
|
+
bash "$SCRIPT" capture "$SHA" >/dev/null 2>&1
|
|
83
|
+
[ $? -eq 0 ] && echo "OK acepta sha contenido en rama remota" || { echo "FAIL sha remoto rechazado"; fail=1; }
|
|
84
|
+
|
|
85
|
+
# ── invalidación: el remoto avanza → compare pide re-capturar (rc=3) ─────────
|
|
86
|
+
echo green > tests/nuevo.txt && git add tests/nuevo.txt && git commit --quiet -m avanza
|
|
87
|
+
git push --quiet origin main 2>/dev/null
|
|
88
|
+
out="$(bash "$SCRIPT" compare origin/main 2>&1)"; rc=$?
|
|
89
|
+
[ $rc -eq 3 ] && echo "OK cache invalidado al avanzar el remoto (rc=3)" || { echo "FAIL invalidación (rc=$rc)"; fail=1; }
|
|
90
|
+
echo "$out" | grep -q "re-captura" && echo "OK pide re-capturar" || { echo "FAIL no pide re-capturar"; fail=1; }
|
|
91
|
+
|
|
92
|
+
exit $fail
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -uo pipefail
|
|
3
|
+
ROOT="$(git rev-parse --show-toplevel)"
|
|
4
|
+
source "$ROOT/hooks/build/lib/config.sh" 2>/dev/null || { echo "FAIL no existe lib/config.sh"; exit 1; }
|
|
5
|
+
fail=0
|
|
6
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
7
|
+
|
|
8
|
+
echo '{"context":{"warning_pct":40},"runtime":{"mode":"dual"}}' > "$TMP/cfg.json"
|
|
9
|
+
got="$(BUILD_CONFIG_FILE="$TMP/cfg.json" config_get context.warning_pct 35)"
|
|
10
|
+
[ "$got" = 40 ] && echo "OK config_get valor" || { echo "FAIL config_get valor ($got)"; fail=1; }
|
|
11
|
+
got="$(BUILD_CONFIG_FILE="$TMP/cfg.json" config_get context.critical_pct 25)"
|
|
12
|
+
[ "$got" = 25 ] && echo "OK config_get default" || { echo "FAIL config_get default ($got)"; fail=1; }
|
|
13
|
+
got="$(BUILD_CONFIG_FILE="$TMP/cfg.json" config_get runtime.mode legacy)"
|
|
14
|
+
[ "$got" = dual ] && echo "OK config_get anidado" || { echo "FAIL config_get anidado ($got)"; fail=1; }
|
|
15
|
+
|
|
16
|
+
# Regresión: state-io.sh sigue exponiendo config_get vía sourcing, cero cambio de
|
|
17
|
+
# comportamiento. Subshell AISLADO que solo sourcea state-io.sh (nunca config.sh
|
|
18
|
+
# directamente) — si state-io.sh dejara de sourcear config.sh, config_get no existiría
|
|
19
|
+
# aquí y el subshell fallaría. Sourcear ambos en el shell del test (como antes) no
|
|
20
|
+
# probaba nada: config_get ya estaba definida por el primer source, pasara lo que
|
|
21
|
+
# pasara con el segundo.
|
|
22
|
+
got="$(bash -c 'source "$1/hooks/build/lib/state-io.sh" 2>/dev/null && BUILD_CONFIG_FILE="$2" config_get context.warning_pct 35' _ "$ROOT" "$TMP/cfg.json" 2>&1)"
|
|
23
|
+
[ "$got" = 40 ] && echo "OK state-io re-exporta config_get" || { echo "FAIL state-io no re-exporta ($got)"; fail=1; }
|
|
24
|
+
|
|
25
|
+
exit $fail
|