create-quiver 0.4.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/.github/ISSUE_TEMPLATE/bug_report.md +15 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- package/.github/pull_request_template.md +4 -0
- package/.github/workflows/ci.yml +74 -0
- package/CHANGELOG.md +24 -0
- package/CODE_OF_CONDUCT.md +12 -0
- package/CONTRIBUTING.md +15 -0
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/README_FOR_AI.md +90 -0
- package/ROADMAP.md +20 -0
- package/SECURITY.md +12 -0
- package/TEMPLATE.md +108 -0
- package/bin/create-quiver.js +8 -0
- package/docs/CONTEXTO.md.template +45 -0
- package/docs/DOCUMENTATION_GUIDE.md.template +34 -0
- package/docs/GITFLOW_PR_GUIDE.md.template +52 -0
- package/docs/INDEX.md.template +41 -0
- package/docs/MOCK_DATA_GUIDE.md.template +31 -0
- package/docs/MULTI_AGENT_WORKFLOW.md.template +31 -0
- package/docs/STATUS.md.template +26 -0
- package/docs/SUPPORT_MATRIX.md.template +31 -0
- package/docs/TESTING_GUIDE_FOR_AI.md.template +42 -0
- package/docs/TROUBLESHOOTING.md.template +70 -0
- package/docs/UI_STANDARDS.md.template +31 -0
- package/docs/WORKFLOW.md.template +56 -0
- package/docs/ai/LESSONS.md.template +24 -0
- package/docs/ai/PRINCIPLES.md +25 -0
- package/docs/ai/RULES.yaml +33 -0
- package/i18n/es/README.md +15 -0
- package/i18n/es/README_FOR_AI.md +6 -0
- package/i18n/es/TEMPLATE.md +18 -0
- package/i18n/es/docs/CONTEXTO.md.template +9 -0
- package/i18n/es/docs/DOCUMENTATION_GUIDE.md.template +4 -0
- package/i18n/es/docs/GITFLOW_PR_GUIDE.md.template +4 -0
- package/i18n/es/docs/INDEX.md.template +10 -0
- package/i18n/es/docs/MOCK_DATA_GUIDE.md.template +4 -0
- package/i18n/es/docs/MULTI_AGENT_WORKFLOW.md.template +4 -0
- package/i18n/es/docs/STATUS.md.template +9 -0
- package/i18n/es/docs/TESTING_GUIDE_FOR_AI.md.template +7 -0
- package/i18n/es/docs/UI_STANDARDS.md.template +4 -0
- package/i18n/es/docs/WORKFLOW.md.template +6 -0
- package/i18n/es/docs/ai/LESSONS.md.template +3 -0
- package/i18n/es/docs/ai/PRINCIPLES.md +7 -0
- package/i18n/es/docs/ai/RULES.yaml +7 -0
- package/package.json +19 -0
- package/package.template.json +10 -0
- package/scripts/check-pr-readiness.sh +138 -0
- package/scripts/check-scope.sh +150 -0
- package/scripts/check-slice-readiness.sh +319 -0
- package/scripts/cleanup-slice.sh +177 -0
- package/scripts/init-docs.sh +368 -0
- package/scripts/migrate-project.sh +218 -0
- package/scripts/package-quiver.sh +124 -0
- package/scripts/refresh-active-slices.sh +232 -0
- package/scripts/release-quiver.sh +77 -0
- package/scripts/start-slice.sh +429 -0
- package/specs/[project-name]/EVIDENCE_REPORT.md.template +15 -0
- package/specs/[project-name]/SPEC.md.template +39 -0
- package/specs/[project-name]/STATUS.md.template +22 -0
- package/specs/[project-name]/slices/pr.md.template +97 -0
- package/specs/[project-name]/slices/slice-template/slice.json +69 -0
- package/specs/quiver-v05-readme-adoption-contract/EVIDENCE_REPORT.md +21 -0
- package/specs/quiver-v05-readme-adoption-contract/SPEC.md +40 -0
- package/specs/quiver-v05-readme-adoption-contract/STATUS.md +24 -0
- package/specs/quiver-v05-readme-adoption-contract/slices/slice-01-readme-adoption-contract/slice.json +68 -0
- package/specs/quiver-v06-release-readiness/EVIDENCE_REPORT.md +23 -0
- package/specs/quiver-v06-release-readiness/SPEC.md +40 -0
- package/specs/quiver-v06-release-readiness/STATUS.md +24 -0
- package/specs/quiver-v06-release-readiness/slices/slice-01-first-npm-release-readiness/slice.json +71 -0
- package/src/create-quiver/index.js +329 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
usage() {
|
|
6
|
+
cat <<'EOF'
|
|
7
|
+
Uso:
|
|
8
|
+
bash tools/scripts/check-pr-readiness.sh <ruta-al-slice.json>
|
|
9
|
+
|
|
10
|
+
Valida que un slice este listo para abrir PR:
|
|
11
|
+
- gate de validacion del slice
|
|
12
|
+
- pr.md con secciones obligatorias
|
|
13
|
+
- detailed testing steps
|
|
14
|
+
- rollback explicito
|
|
15
|
+
- branch del slice limpio y con diferencias contra develop
|
|
16
|
+
EOF
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fail() {
|
|
20
|
+
echo "FAIL: $1" >&2
|
|
21
|
+
exit 1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pass() {
|
|
25
|
+
echo "PASS: $1"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
warn() {
|
|
29
|
+
echo "WARN: $1"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[[ $# -eq 1 ]] || {
|
|
33
|
+
usage
|
|
34
|
+
exit 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
slice_input="$1"
|
|
38
|
+
|
|
39
|
+
command -v git >/dev/null 2>&1 || fail "git no esta disponible en PATH."
|
|
40
|
+
command -v node >/dev/null 2>&1 || fail "node no esta disponible en PATH."
|
|
41
|
+
|
|
42
|
+
repo_root="$(git rev-parse --show-toplevel)"
|
|
43
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
44
|
+
|
|
45
|
+
[[ -f "$slice_input" ]] || fail "No existe el slice '$slice_input'."
|
|
46
|
+
|
|
47
|
+
slice_abs="$(cd "$(dirname "$slice_input")" && pwd)/$(basename "$slice_input")"
|
|
48
|
+
|
|
49
|
+
"$script_dir/check-slice-readiness.sh" "$slice_abs" --gate validation
|
|
50
|
+
"$script_dir/check-scope.sh" "$slice_abs"
|
|
51
|
+
|
|
52
|
+
slice_meta=()
|
|
53
|
+
while IFS= read -r line; do
|
|
54
|
+
slice_meta+=("$line")
|
|
55
|
+
done < <(node - "$slice_abs" <<'NODE'
|
|
56
|
+
const fs = require('fs');
|
|
57
|
+
const path = require('path');
|
|
58
|
+
|
|
59
|
+
const slicePath = process.argv[2];
|
|
60
|
+
const json = JSON.parse(fs.readFileSync(slicePath, 'utf8'));
|
|
61
|
+
|
|
62
|
+
const branchName = String(json.git?.branch_name || '').trim();
|
|
63
|
+
const sliceId = String(json.slice_id || '').trim();
|
|
64
|
+
const prPath = path.join(path.dirname(slicePath), 'pr.md');
|
|
65
|
+
|
|
66
|
+
console.log(branchName);
|
|
67
|
+
console.log(sliceId);
|
|
68
|
+
console.log(prPath);
|
|
69
|
+
NODE
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
[[ ${#slice_meta[@]} -eq 3 ]] || fail "No se pudo leer la metadata del PR del slice."
|
|
73
|
+
|
|
74
|
+
branch_name="${slice_meta[0]}"
|
|
75
|
+
slice_id="${slice_meta[1]}"
|
|
76
|
+
pr_abs="${slice_meta[2]}"
|
|
77
|
+
|
|
78
|
+
[[ -n "$branch_name" ]] || fail "Falta git.branch_name en el slice."
|
|
79
|
+
[[ -f "$pr_abs" ]] || fail "Falta pr.md junto al slice."
|
|
80
|
+
|
|
81
|
+
current_branch="$(git branch --show-current)"
|
|
82
|
+
[[ "$current_branch" == "$branch_name" ]] || fail "Debes ejecutar este check desde la rama del slice. Actual: $current_branch Esperada: $branch_name"
|
|
83
|
+
pass "La rama actual coincide con la rama declarada por el slice."
|
|
84
|
+
|
|
85
|
+
if [[ -n "$(git status --porcelain)" ]]; then
|
|
86
|
+
fail "El worktree no esta limpio. Cerra la implementacion antes de abrir el PR."
|
|
87
|
+
fi
|
|
88
|
+
pass "El worktree esta limpio."
|
|
89
|
+
|
|
90
|
+
ahead_count="$(git rev-list --count origin/develop..HEAD)"
|
|
91
|
+
if [[ "$ahead_count" -le 0 ]]; then
|
|
92
|
+
if git merge-base --is-ancestor HEAD origin/develop; then
|
|
93
|
+
fail "La rama ya fue absorbida por origin/develop. Este gate aplica antes del merge."
|
|
94
|
+
fi
|
|
95
|
+
fail "La rama no tiene commits propios respecto de origin/develop."
|
|
96
|
+
fi
|
|
97
|
+
pass "La rama tiene commits propios contra origin/develop."
|
|
98
|
+
|
|
99
|
+
for heading in \
|
|
100
|
+
"## Title" \
|
|
101
|
+
"## Summary" \
|
|
102
|
+
"## Scope" \
|
|
103
|
+
"## Files" \
|
|
104
|
+
"## How to Test (DETAILED - REQUIRED)" \
|
|
105
|
+
"## Evidence" \
|
|
106
|
+
"## Rollback" \
|
|
107
|
+
"## Risks / Notes"
|
|
108
|
+
do
|
|
109
|
+
grep -Fxq "$heading" "$pr_abs" || fail "Falta la seccion obligatoria '$heading' en pr.md."
|
|
110
|
+
done
|
|
111
|
+
pass "pr.md contiene las secciones obligatorias."
|
|
112
|
+
|
|
113
|
+
for subheading in \
|
|
114
|
+
"### Required Environment" \
|
|
115
|
+
"### Worktree Access" \
|
|
116
|
+
"### Run the Project" \
|
|
117
|
+
"### Use Cases" \
|
|
118
|
+
"### Technical Verification"
|
|
119
|
+
do
|
|
120
|
+
grep -Fxq "$subheading" "$pr_abs" || fail "Falta la subseccion '$subheading' dentro de How to Test."
|
|
121
|
+
done
|
|
122
|
+
pass "How to Test incluye entorno, acceso al worktree, arranque, casos de uso y verificación técnica."
|
|
123
|
+
|
|
124
|
+
grep -Eq '#### Case [0-9]+:' "$pr_abs" || fail "How to Test debe tener al menos un caso de uso documentado (#### Case 1: ...)."
|
|
125
|
+
pass "Al menos un caso de uso documentado."
|
|
126
|
+
|
|
127
|
+
grep -Eq 'git revert ' "$pr_abs" || fail "Rollback debe incluir al menos un comando git revert."
|
|
128
|
+
pass "Rollback incluye comando git revert."
|
|
129
|
+
|
|
130
|
+
grep -Eiq '^\s*-\s*`manual review`$|^\s*-\s*`visual check`$|^\s*-\s*`screen test`$|^\s*-\s*`visual validation`$' "$pr_abs" && fail "How to Test cannot rely only on generic phrases."
|
|
131
|
+
|
|
132
|
+
node -e "JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'))" "$slice_abs" >/dev/null
|
|
133
|
+
pass "slice.json parsea correctamente."
|
|
134
|
+
|
|
135
|
+
git diff --check >/dev/null
|
|
136
|
+
pass "git diff --check no reporta problemas."
|
|
137
|
+
|
|
138
|
+
echo "PASS: Gate PR listo para '$slice_id'."
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
usage() {
|
|
6
|
+
cat <<'EOF'
|
|
7
|
+
Uso:
|
|
8
|
+
bash tools/scripts/check-scope.sh <ruta-al-slice.json> [--strict]
|
|
9
|
+
|
|
10
|
+
Compara los archivos tocados en la rama del slice contra los declarados
|
|
11
|
+
en slice.json.files. Detecta scope creep: archivos modificados fuera del
|
|
12
|
+
alcance declarado.
|
|
13
|
+
|
|
14
|
+
Opciones:
|
|
15
|
+
--strict Convierte cualquier archivo fuera de scope en error (default: warning)
|
|
16
|
+
EOF
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fail() {
|
|
20
|
+
echo "FAIL: $1" >&2
|
|
21
|
+
exit 1
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pass() {
|
|
25
|
+
echo "PASS: $1"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
warn() {
|
|
29
|
+
echo "WARN: $1"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
strict="false"
|
|
33
|
+
slice_input=""
|
|
34
|
+
|
|
35
|
+
while [[ $# -gt 0 ]]; do
|
|
36
|
+
case "$1" in
|
|
37
|
+
-h|--help)
|
|
38
|
+
usage
|
|
39
|
+
exit 0
|
|
40
|
+
;;
|
|
41
|
+
--strict)
|
|
42
|
+
strict="true"
|
|
43
|
+
;;
|
|
44
|
+
-*)
|
|
45
|
+
fail "Opcion desconocida: $1"
|
|
46
|
+
;;
|
|
47
|
+
*)
|
|
48
|
+
if [[ -n "$slice_input" ]]; then
|
|
49
|
+
fail "Solo se acepta un slice por ejecucion."
|
|
50
|
+
fi
|
|
51
|
+
slice_input="$1"
|
|
52
|
+
;;
|
|
53
|
+
esac
|
|
54
|
+
shift
|
|
55
|
+
done
|
|
56
|
+
|
|
57
|
+
[[ -n "$slice_input" ]] || fail "Debes indicar la ruta del slice."
|
|
58
|
+
|
|
59
|
+
command -v git >/dev/null 2>&1 || fail "git no esta disponible en PATH."
|
|
60
|
+
command -v node >/dev/null 2>&1 || fail "node no esta disponible en PATH."
|
|
61
|
+
|
|
62
|
+
repo_root="$(git rev-parse --show-toplevel)"
|
|
63
|
+
[[ -f "$slice_input" ]] || fail "No existe el slice '$slice_input'."
|
|
64
|
+
slice_abs="$(cd "$(dirname "$slice_input")" && pwd)/$(basename "$slice_input")"
|
|
65
|
+
|
|
66
|
+
# Leer archivos declarados en slice.json
|
|
67
|
+
declared_b64=$(node - "$slice_abs" <<'NODE'
|
|
68
|
+
const fs = require('fs');
|
|
69
|
+
let json;
|
|
70
|
+
try {
|
|
71
|
+
json = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
72
|
+
} catch (e) {
|
|
73
|
+
process.stderr.write('Error: No se pudo parsear slice.json: ' + e.message + '\n');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const files = Array.isArray(json.files) ? json.files : [];
|
|
77
|
+
process.stdout.write(Buffer.from(JSON.stringify(files)).toString('base64'));
|
|
78
|
+
NODE
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Obtener archivos tocados en la rama respecto de origin/develop
|
|
82
|
+
touched_raw=""
|
|
83
|
+
if git rev-parse --verify "origin/develop" >/dev/null 2>&1; then
|
|
84
|
+
touched_raw=$(git diff --name-only "origin/develop...HEAD" 2>/dev/null || echo "")
|
|
85
|
+
elif git rev-parse --verify "develop" >/dev/null 2>&1; then
|
|
86
|
+
touched_raw=$(git diff --name-only "develop...HEAD" 2>/dev/null || echo "")
|
|
87
|
+
else
|
|
88
|
+
warn "No se encontro rama origin/develop ni develop. Saltando check de scope."
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
if [[ -z "$touched_raw" ]]; then
|
|
93
|
+
warn "No se encontraron archivos modificados respecto de develop."
|
|
94
|
+
exit 0
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
touched_b64=$(echo "$touched_raw" | base64)
|
|
98
|
+
|
|
99
|
+
# Comparar con node
|
|
100
|
+
scope_result=$(node - "$declared_b64" "$touched_b64" <<'NODE'
|
|
101
|
+
const declared = JSON.parse(Buffer.from(process.argv[2], 'base64').toString('utf8'));
|
|
102
|
+
const touched = Buffer.from(process.argv[3], 'base64').toString('utf8')
|
|
103
|
+
.trim().split('\n').filter(Boolean);
|
|
104
|
+
|
|
105
|
+
// Archivos generados automaticamente por el workflow — siempre permitidos
|
|
106
|
+
const autoAllowed = [
|
|
107
|
+
/^specs\//,
|
|
108
|
+
/^docs\//,
|
|
109
|
+
/^\.worktrees\//,
|
|
110
|
+
/WORKTREE_CONTEXT\.md$/,
|
|
111
|
+
/EVIDENCE_REPORT\.md$/,
|
|
112
|
+
/STATUS\.md$/,
|
|
113
|
+
/SPEC\.md$/,
|
|
114
|
+
/\/pr\.md$/,
|
|
115
|
+
/\/slice\.json$/,
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const outOfScope = touched.filter(file => {
|
|
119
|
+
if (declared.includes(file)) return false;
|
|
120
|
+
if (autoAllowed.some(re => re.test(file))) return false;
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
process.stdout.write(outOfScope.join('\n'));
|
|
125
|
+
NODE
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if [[ -z "$scope_result" ]]; then
|
|
129
|
+
pass "Todos los archivos tocados estan dentro del scope declarado en slice.json."
|
|
130
|
+
exit 0
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
violation_count=0
|
|
134
|
+
while IFS= read -r file; do
|
|
135
|
+
[[ -n "$file" ]] || continue
|
|
136
|
+
violation_count=$((violation_count + 1))
|
|
137
|
+
if [[ "$strict" == "true" ]]; then
|
|
138
|
+
echo "FAIL: Archivo fuera de scope: $file" >&2
|
|
139
|
+
else
|
|
140
|
+
warn "Archivo fuera de scope: $file"
|
|
141
|
+
fi
|
|
142
|
+
done <<< "$scope_result"
|
|
143
|
+
|
|
144
|
+
if [[ "$violation_count" -gt 0 ]]; then
|
|
145
|
+
if [[ "$strict" == "true" ]]; then
|
|
146
|
+
fail "$violation_count archivo(s) fuera del scope declarado. Actualiza slice.json.files o revierte los cambios fuera de alcance."
|
|
147
|
+
else
|
|
148
|
+
warn "$violation_count archivo(s) fuera del scope declarado. Considera actualizar slice.json.files o revertir los cambios no previstos."
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
usage() {
|
|
6
|
+
cat <<'EOF'
|
|
7
|
+
Uso:
|
|
8
|
+
bash tools/scripts/check-slice-readiness.sh <ruta-al-slice.json> [--gate execution|validation] [--strict-overlap]
|
|
9
|
+
|
|
10
|
+
Valida que un slice tenga las precondiciones mínimas para ejecutarse o pasar a validación.
|
|
11
|
+
|
|
12
|
+
Opciones:
|
|
13
|
+
--gate <mode> Gate a validar. Default: execution
|
|
14
|
+
ready -> requiere status=ready (gate entre Track 1 y Track 2)
|
|
15
|
+
execution -> metadata, spec base mergeado y overlap
|
|
16
|
+
validation -> execution + estado completado y timestamps
|
|
17
|
+
--strict-overlap Convierte overlap con worktrees activos en error
|
|
18
|
+
EOF
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fail() {
|
|
22
|
+
echo "FAIL: $1" >&2
|
|
23
|
+
exit 1
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
pass() {
|
|
27
|
+
echo "PASS: $1"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
warn() {
|
|
31
|
+
echo "WARN: $1"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
gate="execution"
|
|
35
|
+
strict_overlap="false"
|
|
36
|
+
slice_input=""
|
|
37
|
+
|
|
38
|
+
while [[ $# -gt 0 ]]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
-h|--help)
|
|
41
|
+
usage
|
|
42
|
+
exit 0
|
|
43
|
+
;;
|
|
44
|
+
--gate)
|
|
45
|
+
shift
|
|
46
|
+
[[ $# -gt 0 ]] || fail "Falta valor para --gate."
|
|
47
|
+
gate="$1"
|
|
48
|
+
;;
|
|
49
|
+
--strict-overlap)
|
|
50
|
+
strict_overlap="true"
|
|
51
|
+
;;
|
|
52
|
+
-*)
|
|
53
|
+
fail "Opcion desconocida: $1"
|
|
54
|
+
;;
|
|
55
|
+
*)
|
|
56
|
+
if [[ -n "$slice_input" ]]; then
|
|
57
|
+
fail "Solo se acepta un slice por ejecucion."
|
|
58
|
+
fi
|
|
59
|
+
slice_input="$1"
|
|
60
|
+
;;
|
|
61
|
+
esac
|
|
62
|
+
shift
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
[[ -n "$slice_input" ]] || fail "Debes indicar la ruta del slice."
|
|
66
|
+
|
|
67
|
+
case "$gate" in
|
|
68
|
+
ready|execution|validation) ;;
|
|
69
|
+
*) fail "Gate invalido: $gate. Usa ready, execution o validation." ;;
|
|
70
|
+
esac
|
|
71
|
+
|
|
72
|
+
command -v git >/dev/null 2>&1 || fail "git no esta disponible en PATH."
|
|
73
|
+
command -v node >/dev/null 2>&1 || fail "node no esta disponible en PATH."
|
|
74
|
+
|
|
75
|
+
repo_root="$(git rev-parse --show-toplevel)"
|
|
76
|
+
|
|
77
|
+
[[ -f "$slice_input" ]] || fail "No existe el slice '$slice_input'."
|
|
78
|
+
|
|
79
|
+
slice_abs="$(cd "$(dirname "$slice_input")" && pwd)/$(basename "$slice_input")"
|
|
80
|
+
slice_rel="${slice_abs#$repo_root/}"
|
|
81
|
+
|
|
82
|
+
slice_meta=()
|
|
83
|
+
while IFS= read -r line; do
|
|
84
|
+
slice_meta+=("$line")
|
|
85
|
+
done < <(node - "$slice_abs" "$slice_rel" <<'NODE'
|
|
86
|
+
const fs = require('fs');
|
|
87
|
+
const path = require('path');
|
|
88
|
+
|
|
89
|
+
const [slicePath, sliceRel] = process.argv.slice(2);
|
|
90
|
+
|
|
91
|
+
function fail(message) {
|
|
92
|
+
console.error(`Error: ${message}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let json;
|
|
97
|
+
try {
|
|
98
|
+
json = JSON.parse(fs.readFileSync(slicePath, 'utf8'));
|
|
99
|
+
} catch (error) {
|
|
100
|
+
fail(`No se pudo parsear '${slicePath}' como JSON: ${error.message}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const ticket = typeof json.ticket === 'string' ? json.ticket.trim() : '';
|
|
104
|
+
const sliceId = typeof json.slice_id === 'string' ? json.slice_id.trim() : '';
|
|
105
|
+
const branchName = typeof json.git?.branch_name === 'string' ? json.git.branch_name.trim() : '';
|
|
106
|
+
const files = Array.isArray(json.files) ? json.files : [];
|
|
107
|
+
const acceptance = Array.isArray(json.acceptance) ? json.acceptance : [];
|
|
108
|
+
const specDir = path.dirname(path.dirname(path.dirname(sliceRel)));
|
|
109
|
+
|
|
110
|
+
if (!sliceId) {
|
|
111
|
+
fail('Falta "slice_id" en el slice.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!ticket) {
|
|
115
|
+
fail('Falta "ticket" en el slice.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!branchName) {
|
|
119
|
+
fail('Falta "git.branch_name" en el slice.');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (files.length === 0) {
|
|
123
|
+
fail('El slice debe declarar al menos un archivo en "files".');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (acceptance.length === 0) {
|
|
127
|
+
fail('El slice debe declarar criterios en "acceptance".');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(sliceId);
|
|
131
|
+
console.log(ticket);
|
|
132
|
+
console.log(branchName);
|
|
133
|
+
console.log(json.status || 'pending');
|
|
134
|
+
console.log(sliceId.startsWith('slice-00') ? 'true' : 'false');
|
|
135
|
+
console.log(specDir);
|
|
136
|
+
console.log(Buffer.from(JSON.stringify(files)).toString('base64'));
|
|
137
|
+
console.log(String(json.actual_hours ?? ''));
|
|
138
|
+
console.log(json.started_at ?? '');
|
|
139
|
+
console.log(json.completed_at ?? '');
|
|
140
|
+
NODE
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
[[ ${#slice_meta[@]} -eq 10 ]] || fail "No se pudo leer la metadata del slice."
|
|
144
|
+
|
|
145
|
+
slice_id="${slice_meta[0]}"
|
|
146
|
+
ticket="${slice_meta[1]}"
|
|
147
|
+
branch_name="${slice_meta[2]}"
|
|
148
|
+
slice_status="${slice_meta[3]}"
|
|
149
|
+
is_baseline="${slice_meta[4]}"
|
|
150
|
+
spec_dir_rel="${slice_meta[5]}"
|
|
151
|
+
files_b64="${slice_meta[6]}"
|
|
152
|
+
actual_hours="${slice_meta[7]}"
|
|
153
|
+
started_at="${slice_meta[8]}"
|
|
154
|
+
completed_at="${slice_meta[9]}"
|
|
155
|
+
|
|
156
|
+
for spec_file in SPEC.md STATUS.md EVIDENCE_REPORT.md; do
|
|
157
|
+
[[ -f "$repo_root/$spec_dir_rel/$spec_file" ]] || fail "Falta '$spec_dir_rel/$spec_file'."
|
|
158
|
+
done
|
|
159
|
+
pass "El spec local tiene SPEC.md, STATUS.md y EVIDENCE_REPORT.md."
|
|
160
|
+
|
|
161
|
+
if git cat-file -e "origin/develop:$slice_rel" 2>/dev/null; then
|
|
162
|
+
pass "El slice ya existe en origin/develop (PR base documental mergeado)."
|
|
163
|
+
else
|
|
164
|
+
if [[ "$gate" == "validation" ]]; then
|
|
165
|
+
warn "El slice no existe todavia en origin/develop. El PR base documental sigue pendiente de merge. Podes abrir el PR del slice igual — el humano mergea en orden."
|
|
166
|
+
else
|
|
167
|
+
fail "El slice no existe en origin/develop. Mergea primero el PR base documental."
|
|
168
|
+
fi
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
overlap_output="$(node - "$repo_root" "$branch_name" "$files_b64" <<'NODE'
|
|
172
|
+
const fs = require('fs');
|
|
173
|
+
const path = require('path');
|
|
174
|
+
const cp = require('child_process');
|
|
175
|
+
|
|
176
|
+
const [repoRoot, currentBranch, currentFilesB64] = process.argv.slice(2);
|
|
177
|
+
const currentFiles = JSON.parse(Buffer.from(currentFilesB64, 'base64').toString('utf8'));
|
|
178
|
+
|
|
179
|
+
function run(cmd, cwd = repoRoot) {
|
|
180
|
+
try {
|
|
181
|
+
return cp.execSync(cmd, {
|
|
182
|
+
cwd,
|
|
183
|
+
encoding: 'utf8',
|
|
184
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
185
|
+
}).trim();
|
|
186
|
+
} catch {
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function walkSlices(rootDir, acc) {
|
|
192
|
+
if (!fs.existsSync(rootDir)) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
|
197
|
+
const fullPath = path.join(rootDir, entry.name);
|
|
198
|
+
if (entry.isDirectory()) {
|
|
199
|
+
walkSlices(fullPath, acc);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (entry.isFile() && entry.name === 'slice.json' && fullPath.includes(`${path.sep}slices${path.sep}`)) {
|
|
204
|
+
const json = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
|
205
|
+
const branchName = json.git?.branch_name;
|
|
206
|
+
if (!branchName) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
acc.set(branchName, {
|
|
211
|
+
sliceId: json.slice_id || '',
|
|
212
|
+
files: Array.isArray(json.files) ? json.files : []
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function parseWorktrees(text) {
|
|
219
|
+
const entries = [];
|
|
220
|
+
const chunks = text.trim().split('\n\n').filter(Boolean);
|
|
221
|
+
|
|
222
|
+
for (const chunk of chunks) {
|
|
223
|
+
const entry = {};
|
|
224
|
+
for (const line of chunk.split('\n')) {
|
|
225
|
+
const idx = line.indexOf(' ');
|
|
226
|
+
if (idx === -1) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
entry[line.slice(0, idx)] = line.slice(idx + 1);
|
|
230
|
+
}
|
|
231
|
+
entries.push(entry);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return entries;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const sliceMap = new Map();
|
|
238
|
+
walkSlices(path.join(repoRoot, 'specs'), sliceMap);
|
|
239
|
+
walkSlices(path.join(repoRoot, 'specs-fix'), sliceMap);
|
|
240
|
+
|
|
241
|
+
const worktrees = parseWorktrees(run('git worktree list --porcelain'));
|
|
242
|
+
const warnings = [];
|
|
243
|
+
|
|
244
|
+
for (const entry of worktrees) {
|
|
245
|
+
const worktreePath = entry.worktree;
|
|
246
|
+
const branchRef = entry.branch || '';
|
|
247
|
+
const branchName = branchRef.replace('refs/heads/', '');
|
|
248
|
+
|
|
249
|
+
if (!branchName || branchName === currentBranch || worktreePath === repoRoot) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const meta = sliceMap.get(branchName);
|
|
254
|
+
if (!meta || meta.sliceId.startsWith('slice-00')) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const dirty = run('git status --porcelain', worktreePath) !== '';
|
|
259
|
+
const aheadCount = Number(run('git rev-list --count origin/develop..HEAD', worktreePath) || '0');
|
|
260
|
+
const active = dirty || aheadCount > 0;
|
|
261
|
+
|
|
262
|
+
if (!active) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const overlap = currentFiles.filter((item) => meta.files.includes(item));
|
|
267
|
+
if (overlap.length > 0) {
|
|
268
|
+
warnings.push(`${branchName}|${overlap.join(', ')}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
process.stdout.write(warnings.join('\n'));
|
|
273
|
+
NODE
|
|
274
|
+
)"
|
|
275
|
+
|
|
276
|
+
if [[ -n "$overlap_output" ]]; then
|
|
277
|
+
while IFS= read -r overlap_line; do
|
|
278
|
+
[[ -n "$overlap_line" ]] || continue
|
|
279
|
+
overlap_branch="${overlap_line%%|*}"
|
|
280
|
+
overlap_files="${overlap_line#*|}"
|
|
281
|
+
if [[ "$strict_overlap" == "true" ]]; then
|
|
282
|
+
fail "Overlap con worktree activo '$overlap_branch': $overlap_files"
|
|
283
|
+
fi
|
|
284
|
+
warn "Overlap con worktree activo '$overlap_branch': $overlap_files"
|
|
285
|
+
done <<< "$overlap_output"
|
|
286
|
+
else
|
|
287
|
+
pass "No se detecto overlap con worktrees activos."
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
case "$gate" in
|
|
291
|
+
ready)
|
|
292
|
+
[[ "$slice_status" == "ready" ]] || fail "Gate ready: slice.json debe estar en status=ready. Estado actual: $slice_status. Completa la especificacion en el Track 1 antes de pasar a ejecucion."
|
|
293
|
+
pass "Gate ready: el slice esta marcado como ready para ejecucion."
|
|
294
|
+
;;
|
|
295
|
+
execution)
|
|
296
|
+
if [[ "$slice_status" == "blocked" ]]; then
|
|
297
|
+
fail "El slice esta bloqueado (status=blocked). Resolve el bloqueante antes de ejecutar."
|
|
298
|
+
fi
|
|
299
|
+
if [[ "$slice_status" == "cancelled" ]]; then
|
|
300
|
+
fail "El slice esta cancelado (status=cancelled)."
|
|
301
|
+
fi
|
|
302
|
+
if [[ "$slice_status" == "completed" ]]; then
|
|
303
|
+
warn "El slice ya figura como completed. Revisa si realmente corresponde reejecutarlo."
|
|
304
|
+
fi
|
|
305
|
+
if [[ "$slice_status" == "draft" ]]; then
|
|
306
|
+
warn "El slice esta en estado 'draft'. Considera marcarlo como 'ready' antes de ejecutar."
|
|
307
|
+
fi
|
|
308
|
+
pass "Gate execution: metadata y precondiciones minimas OK."
|
|
309
|
+
;;
|
|
310
|
+
validation)
|
|
311
|
+
[[ "$slice_status" == "completed" ]] || fail "Para gate validation, slice.json debe estar en status=completed."
|
|
312
|
+
[[ -n "$completed_at" ]] || fail "Para gate validation, slice.json debe tener completed_at."
|
|
313
|
+
[[ -n "$started_at" ]] || fail "Para gate validation, slice.json debe tener started_at."
|
|
314
|
+
if [[ -z "$actual_hours" ]] || ! node -e "process.exit(Number(process.argv[1]) > 0 ? 0 : 1)" "$actual_hours" >/dev/null 2>&1; then
|
|
315
|
+
fail "Para gate validation, slice.json debe tener actual_hours > 0."
|
|
316
|
+
fi
|
|
317
|
+
pass "Gate validation: slice marcado como completado y con trazabilidad minima."
|
|
318
|
+
;;
|
|
319
|
+
esac
|