ai-core-framework 0.1.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 +21 -0
- package/.codex-plugin/plugin.json +35 -0
- package/.cursor-plugin/plugin.json +22 -0
- package/README.md +173 -0
- package/bin/ai-core-framework.js +110 -0
- package/core/README.md +162 -0
- package/core/agents/README.md +32 -0
- package/core/agents/business-analyst.md +269 -0
- package/core/agents/developer.md +375 -0
- package/core/agents/qa-tester.md +477 -0
- package/core/agents/scrum-master.md +136 -0
- package/core/agents/tech-lead.md +345 -0
- package/core/config/backlog.schema.json +38 -0
- package/core/config/docs-policy.default.json +37 -0
- package/core/config/release.schema.json +120 -0
- package/core/config/ticket.schema.json +253 -0
- package/core/rules/00-global-rules.md +373 -0
- package/core/rules/01-git-workflow.md +388 -0
- package/core/rules/02-code-quality.md +77 -0
- package/core/rules/03-security.md +78 -0
- package/core/rules/04-documentation.md +72 -0
- package/core/rules/05-testing-mandatory.md +374 -0
- package/core/rules/06-approval-gates.md +388 -0
- package/core/rules/07-definition-of-ready.md +112 -0
- package/core/rules/08-definition-of-done.md +149 -0
- package/core/scripts/ai-core.sh +456 -0
- package/core/scripts/generate-views.sh +210 -0
- package/core/scripts/install-codex-prompts.sh +127 -0
- package/core/scripts/log-user-request.sh +113 -0
- package/core/scripts/setup-project.sh +183 -0
- package/core/scripts/sync-platforms.sh +322 -0
- package/core/scripts/validate-audit-log.sh +73 -0
- package/core/scripts/validate-docs.sh +365 -0
- package/core/scripts/validate-permissions.sh +132 -0
- package/core/scripts/validate-state.sh +611 -0
- package/core/scripts/workflow.sh +513 -0
- package/core/skills/README.md +21 -0
- package/core/skills/ai-core-commands/SKILL.md +86 -0
- package/core/skills/brainstorming/SKILL.md +40 -0
- package/core/skills/development-implement-task/SKILL.md +308 -0
- package/core/skills/executing-ticket/SKILL.md +28 -0
- package/core/skills/git-branch-status/SKILL.md +56 -0
- package/core/skills/git-cleanup-branches/SKILL.md +57 -0
- package/core/skills/git-scan-untracked/SKILL.md +50 -0
- package/core/skills/meta-generate-views/SKILL.md +54 -0
- package/core/skills/meta-request-log/SKILL.md +61 -0
- package/core/skills/meta-sprint-report/SKILL.md +59 -0
- package/core/skills/meta-sync-platforms/SKILL.md +53 -0
- package/core/skills/meta-ticket-health/SKILL.md +61 -0
- package/core/skills/meta-validate-audit-log/SKILL.md +42 -0
- package/core/skills/meta-validate-docs/SKILL.md +58 -0
- package/core/skills/meta-validate-permissions/SKILL.md +53 -0
- package/core/skills/meta-validate-state/SKILL.md +58 -0
- package/core/skills/planning-analyze-requirements/SKILL.md +471 -0
- package/core/skills/planning-backlog-status/SKILL.md +57 -0
- package/core/skills/planning-document-existing-requirements/SKILL.md +246 -0
- package/core/skills/planning-estimate-task/SKILL.md +60 -0
- package/core/skills/planning-groom-ticket/SKILL.md +442 -0
- package/core/skills/planning-mark-ready/SKILL.md +111 -0
- package/core/skills/planning-plan-refactor/SKILL.md +66 -0
- package/core/skills/planning-plan-sprint/SKILL.md +112 -0
- package/core/skills/planning-prioritize-backlog/SKILL.md +62 -0
- package/core/skills/planning-write-plan/SKILL.md +68 -0
- package/core/skills/project-detect-stack/SKILL.md +71 -0
- package/core/skills/project-discover-codebase/SKILL.md +74 -0
- package/core/skills/project-setup-project/SKILL.md +113 -0
- package/core/skills/qa-bug-status/SKILL.md +52 -0
- package/core/skills/qa-report-bug/SKILL.md +518 -0
- package/core/skills/qa-smoke-test/SKILL.md +387 -0
- package/core/skills/qa-triage-bug/SKILL.md +62 -0
- package/core/skills/qa-verify-fix/SKILL.md +446 -0
- package/core/skills/release-hotfix/SKILL.md +117 -0
- package/core/skills/release-release/SKILL.md +123 -0
- package/core/skills/release-rollback/SKILL.md +62 -0
- package/core/skills/review-create-pr/SKILL.md +418 -0
- package/core/skills/review-merge-pr/SKILL.md +425 -0
- package/core/skills/review-techlead-review/SKILL.md +547 -0
- package/core/skills/using-ai-core/SKILL.md +72 -0
- package/core/skills/verification-before-done/SKILL.md +35 -0
- package/core/skills/writing-implementation-plan/SKILL.md +45 -0
- package/core/templates/ci/ai-core-governance.yml +112 -0
- package/core/templates/ci/node-pnpm.yml +35 -0
- package/core/templates/pm/retrospective-template.md +47 -0
- package/core/templates/pm/sprint-plan-template.md +45 -0
- package/core/templates/pr/pull-request-template.md +247 -0
- package/core/templates/project/CODEOWNERS +11 -0
- package/core/templates/project/docs-policy.json +3 -0
- package/core/templates/project/project-config.yaml +137 -0
- package/core/templates/project/project-structure.yaml +76 -0
- package/core/templates/qa/bug-report-template.md +371 -0
- package/core/templates/qa/test-plan-template.md +57 -0
- package/core/templates/release/release-record-template.json +67 -0
- package/core/templates/requirements/PRD-template.md +58 -0
- package/core/templates/requirements/user-story-template.md +381 -0
- package/core/templates/technical/ADR-template.md +46 -0
- package/core/templates/technical/refactor-plan-template.md +84 -0
- package/core/templates/technical/tech-design-template.md +71 -0
- package/core/workflows/bug-lifecycle.md +56 -0
- package/core/workflows/feature-lifecycle.md +347 -0
- package/core/workflows/hotfix-lifecycle.md +65 -0
- package/core/workflows/sprint-lifecycle.md +56 -0
- package/lib/install-codex.js +85 -0
- package/package.json +36 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/validate-state.sh
|
|
3
|
+
#
|
|
4
|
+
# Validates ticket state machine integrity per RULE 06 (Approval Gates).
|
|
5
|
+
#
|
|
6
|
+
# Checks:
|
|
7
|
+
# 1. JSON validity
|
|
8
|
+
# 2. Required fields present
|
|
9
|
+
# 3. State value valid
|
|
10
|
+
# 4. State transitions follow allowed matrix
|
|
11
|
+
# 5. State history complete (no gaps)
|
|
12
|
+
# 6. State history entries valid (have command + agent)
|
|
13
|
+
# 7. No zombie tickets (alerts only)
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# bash scripts/validate-state.sh # Validate all tickets
|
|
17
|
+
# bash scripts/validate-state.sh TICKET-042 # Validate one
|
|
18
|
+
#
|
|
19
|
+
# Exit codes:
|
|
20
|
+
# 0: All valid
|
|
21
|
+
# 1: Validation failures
|
|
22
|
+
# 2: Script error
|
|
23
|
+
#
|
|
24
|
+
# Last updated: 2026-04-18
|
|
25
|
+
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
# Colors
|
|
29
|
+
RED='\033[0;31m'
|
|
30
|
+
GREEN='\033[0;32m'
|
|
31
|
+
YELLOW='\033[1;33m'
|
|
32
|
+
BLUE='\033[0;34m'
|
|
33
|
+
NC='\033[0m'
|
|
34
|
+
|
|
35
|
+
# ============================================================
|
|
36
|
+
# Config
|
|
37
|
+
# ============================================================
|
|
38
|
+
TICKETS_DIR="project/tickets"
|
|
39
|
+
BUGS_DIR="project/bugs"
|
|
40
|
+
BACKLOG_FILE="project/backlog/backlog.json"
|
|
41
|
+
SCHEMA_FILE="core/config/ticket.schema.json"
|
|
42
|
+
|
|
43
|
+
# ============================================================
|
|
44
|
+
# Allowed state transitions matrix (per RULE 06)
|
|
45
|
+
# Format: "FROM_STATE TO_STATE"
|
|
46
|
+
# ============================================================
|
|
47
|
+
declare -a ALLOWED_TRANSITIONS=(
|
|
48
|
+
"null DRAFT"
|
|
49
|
+
"DRAFT GROOMED"
|
|
50
|
+
"DRAFT BLOCKED"
|
|
51
|
+
"DRAFT CANCELLED"
|
|
52
|
+
"GROOMED READY"
|
|
53
|
+
"GROOMED DRAFT"
|
|
54
|
+
"GROOMED BLOCKED"
|
|
55
|
+
"GROOMED CANCELLED"
|
|
56
|
+
"READY IN_PROGRESS"
|
|
57
|
+
"READY BLOCKED"
|
|
58
|
+
"READY GROOMED"
|
|
59
|
+
"READY CANCELLED"
|
|
60
|
+
"IN_PROGRESS IN_REVIEW"
|
|
61
|
+
"IN_PROGRESS BLOCKED"
|
|
62
|
+
"IN_PROGRESS CANCELLED"
|
|
63
|
+
"IN_REVIEW IN_PROGRESS"
|
|
64
|
+
"IN_REVIEW QA"
|
|
65
|
+
"IN_REVIEW BLOCKED"
|
|
66
|
+
"QA DONE"
|
|
67
|
+
"QA IN_PROGRESS"
|
|
68
|
+
"QA BLOCKED"
|
|
69
|
+
"BLOCKED DRAFT"
|
|
70
|
+
"BLOCKED GROOMED"
|
|
71
|
+
"BLOCKED READY"
|
|
72
|
+
"BLOCKED IN_PROGRESS"
|
|
73
|
+
"BLOCKED IN_REVIEW"
|
|
74
|
+
"BLOCKED QA"
|
|
75
|
+
"BLOCKED CANCELLED"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
VALID_STATES=("DRAFT" "GROOMED" "READY" "IN_PROGRESS" "IN_REVIEW" "QA" "DONE" "BLOCKED" "CANCELLED")
|
|
79
|
+
|
|
80
|
+
# ============================================================
|
|
81
|
+
# Helpers
|
|
82
|
+
# ============================================================
|
|
83
|
+
log_info() { echo -e "${BLUE}ℹ${NC} $1"; }
|
|
84
|
+
log_pass() { echo -e "${GREEN}✓${NC} $1"; }
|
|
85
|
+
log_warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
|
86
|
+
log_fail() { echo -e "${RED}✗${NC} $1"; }
|
|
87
|
+
|
|
88
|
+
is_valid_state() {
|
|
89
|
+
local state="$1"
|
|
90
|
+
for s in "${VALID_STATES[@]}"; do
|
|
91
|
+
[ "$s" = "$state" ] && return 0
|
|
92
|
+
done
|
|
93
|
+
return 1
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
is_valid_transition() {
|
|
97
|
+
local from="$1"
|
|
98
|
+
local to="$2"
|
|
99
|
+
local pair="$from $to"
|
|
100
|
+
|
|
101
|
+
for allowed in "${ALLOWED_TRANSITIONS[@]}"; do
|
|
102
|
+
[ "$allowed" = "$pair" ] && return 0
|
|
103
|
+
done
|
|
104
|
+
return 1
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
parse_rfc3339_epoch() {
|
|
108
|
+
local timestamp="$1"
|
|
109
|
+
|
|
110
|
+
# GNU date, Linux
|
|
111
|
+
if date -d "$timestamp" +%s >/dev/null 2>&1; then
|
|
112
|
+
date -d "$timestamp" +%s
|
|
113
|
+
return 0
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# BSD date, macOS, normalize trailing Z
|
|
117
|
+
local normalized
|
|
118
|
+
normalized=$(printf '%s' "$timestamp" | sed 's/Z$/+0000/' | sed -E 's/([+-][0-9]{2}):([0-9]{2})$/\1\2/')
|
|
119
|
+
if date -j -f "%Y-%m-%dT%H:%M:%S%z" "$normalized" +%s >/dev/null 2>&1; then
|
|
120
|
+
date -j -f "%Y-%m-%dT%H:%M:%S%z" "$normalized" +%s
|
|
121
|
+
return 0
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
return 1
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# ============================================================
|
|
128
|
+
# Validate single ticket
|
|
129
|
+
# ============================================================
|
|
130
|
+
validate_ticket() {
|
|
131
|
+
local ticket_file="$1"
|
|
132
|
+
local ticket_id
|
|
133
|
+
ticket_id=$(basename "$ticket_file" .json)
|
|
134
|
+
|
|
135
|
+
local errors=0
|
|
136
|
+
local warnings=0
|
|
137
|
+
|
|
138
|
+
# Check 1: Valid JSON
|
|
139
|
+
if ! jq empty "$ticket_file" 2>/dev/null; then
|
|
140
|
+
log_fail "$ticket_id: Invalid JSON"
|
|
141
|
+
return 1
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# Check 2: Required fields
|
|
145
|
+
local required_fields=("id" "title" "type" "status" "created_at" "state_history")
|
|
146
|
+
for field in "${required_fields[@]}"; do
|
|
147
|
+
if ! jq -e ".$field" "$ticket_file" > /dev/null 2>&1; then
|
|
148
|
+
log_fail "$ticket_id: Missing required field '$field'"
|
|
149
|
+
errors=$((errors + 1))
|
|
150
|
+
fi
|
|
151
|
+
done
|
|
152
|
+
|
|
153
|
+
if [ "$errors" -gt 0 ]; then
|
|
154
|
+
return 1
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# Check 3: ID matches filename
|
|
158
|
+
local id_in_file
|
|
159
|
+
id_in_file=$(jq -r '.id' "$ticket_file")
|
|
160
|
+
if [ "$id_in_file" != "$ticket_id" ]; then
|
|
161
|
+
log_fail "$ticket_id: ID mismatch ('$id_in_file' in file, '$ticket_id' in filename)"
|
|
162
|
+
errors=$((errors + 1))
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# Check 4: Status is valid
|
|
166
|
+
local status
|
|
167
|
+
status=$(jq -r '.status' "$ticket_file")
|
|
168
|
+
if ! is_valid_state "$status"; then
|
|
169
|
+
log_fail "$ticket_id: Invalid status '$status'"
|
|
170
|
+
log_fail " Valid: ${VALID_STATES[*]}"
|
|
171
|
+
errors=$((errors + 1))
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
# Check 5: State history not empty
|
|
175
|
+
local history_count
|
|
176
|
+
history_count=$(jq '.state_history | length' "$ticket_file")
|
|
177
|
+
if [ "$history_count" -eq 0 ]; then
|
|
178
|
+
log_fail "$ticket_id: Empty state_history (RULE AG-008)"
|
|
179
|
+
errors=$((errors + 1))
|
|
180
|
+
return 1
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# Check 6: Each state history entry has required fields
|
|
184
|
+
local entry_errors=0
|
|
185
|
+
for i in $(seq 0 $((history_count - 1))); do
|
|
186
|
+
local entry
|
|
187
|
+
entry=$(jq ".state_history[$i]" "$ticket_file")
|
|
188
|
+
|
|
189
|
+
for field in "to_state" "at" "by_agent"; do
|
|
190
|
+
if ! echo "$entry" | jq -e ".$field" > /dev/null 2>&1; then
|
|
191
|
+
log_fail "$ticket_id: state_history[$i] missing '$field'"
|
|
192
|
+
entry_errors=$((entry_errors + 1))
|
|
193
|
+
fi
|
|
194
|
+
done
|
|
195
|
+
|
|
196
|
+
# by_command optional only for first entry
|
|
197
|
+
if [ "$i" -gt 0 ]; then
|
|
198
|
+
if ! echo "$entry" | jq -e ".by_command" > /dev/null 2>&1; then
|
|
199
|
+
log_warn "$ticket_id: state_history[$i] missing 'by_command' (recommended per RULE AG-002)"
|
|
200
|
+
warnings=$((warnings + 1))
|
|
201
|
+
fi
|
|
202
|
+
fi
|
|
203
|
+
done
|
|
204
|
+
|
|
205
|
+
if [ "$entry_errors" -gt 0 ]; then
|
|
206
|
+
errors=$((errors + entry_errors))
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
# Check 7: All transitions valid (per matrix)
|
|
210
|
+
local transition_errors=0
|
|
211
|
+
for i in $(seq 0 $((history_count - 1))); do
|
|
212
|
+
local from_state to_state
|
|
213
|
+
from_state=$(jq -r ".state_history[$i].from_state // \"null\"" "$ticket_file")
|
|
214
|
+
to_state=$(jq -r ".state_history[$i].to_state" "$ticket_file")
|
|
215
|
+
|
|
216
|
+
if ! is_valid_transition "$from_state" "$to_state"; then
|
|
217
|
+
log_fail "$ticket_id: Invalid transition #$i: $from_state → $to_state (RULE AG-001)"
|
|
218
|
+
transition_errors=$((transition_errors + 1))
|
|
219
|
+
fi
|
|
220
|
+
done
|
|
221
|
+
|
|
222
|
+
if [ "$transition_errors" -gt 0 ]; then
|
|
223
|
+
errors=$((errors + transition_errors))
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
# Check 8: Current status matches last history entry
|
|
227
|
+
local last_state
|
|
228
|
+
last_state=$(jq -r '.state_history[-1].to_state' "$ticket_file")
|
|
229
|
+
if [ "$status" != "$last_state" ]; then
|
|
230
|
+
log_fail "$ticket_id: Current status '$status' doesn't match last history entry '$last_state' (RULE AG-008)"
|
|
231
|
+
errors=$((errors + 1))
|
|
232
|
+
fi
|
|
233
|
+
|
|
234
|
+
# Check 9: Estimate present if past GROOMED
|
|
235
|
+
local non_estimate_states=("DRAFT" "BLOCKED" "CANCELLED")
|
|
236
|
+
local needs_estimate=true
|
|
237
|
+
for s in "${non_estimate_states[@]}"; do
|
|
238
|
+
[ "$s" = "$status" ] && needs_estimate=false
|
|
239
|
+
done
|
|
240
|
+
|
|
241
|
+
if [ "$needs_estimate" = true ]; then
|
|
242
|
+
if ! jq -e '.estimate.story_points' "$ticket_file" > /dev/null 2>&1; then
|
|
243
|
+
log_warn "$ticket_id: No estimate (status=$status, expected per RULE GR-001)"
|
|
244
|
+
warnings=$((warnings + 1))
|
|
245
|
+
fi
|
|
246
|
+
fi
|
|
247
|
+
|
|
248
|
+
# Check 10: Zombie detection
|
|
249
|
+
local updated_at
|
|
250
|
+
updated_at=$(jq -r '.updated_at // .created_at' "$ticket_file")
|
|
251
|
+
if [ -n "$updated_at" ] && [ "$updated_at" != "null" ]; then
|
|
252
|
+
local now_epoch updated_epoch days_old
|
|
253
|
+
now_epoch=$(date +%s)
|
|
254
|
+
updated_epoch=$(parse_rfc3339_epoch "$updated_at" 2>/dev/null || echo "$now_epoch")
|
|
255
|
+
days_old=$(( (now_epoch - updated_epoch) / 86400 ))
|
|
256
|
+
|
|
257
|
+
local threshold
|
|
258
|
+
case "$status" in
|
|
259
|
+
DRAFT) threshold=14 ;;
|
|
260
|
+
GROOMED) threshold=30 ;;
|
|
261
|
+
READY) threshold=21 ;;
|
|
262
|
+
IN_PROGRESS) threshold=10 ;;
|
|
263
|
+
IN_REVIEW) threshold=5 ;;
|
|
264
|
+
QA) threshold=7 ;;
|
|
265
|
+
BLOCKED) threshold=30 ;;
|
|
266
|
+
*) threshold=9999 ;;
|
|
267
|
+
esac
|
|
268
|
+
|
|
269
|
+
if [ "$days_old" -gt "$threshold" ]; then
|
|
270
|
+
log_warn "$ticket_id: Zombie ticket, status=$status, idle ${days_old}d (threshold: ${threshold}d, RULE AG-007)"
|
|
271
|
+
warnings=$((warnings + 1))
|
|
272
|
+
fi
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
# Check 11: Machine-readable DoD gates for DONE tickets
|
|
276
|
+
if [ "$status" = "DONE" ]; then
|
|
277
|
+
for field in code_complete tests_passed docs_updated review_approved qa_verified release_notes_updated security_checked; do
|
|
278
|
+
if [ "$(jq -r ".dod_checklist.$field // false" "$ticket_file")" != "true" ]; then
|
|
279
|
+
log_fail "$ticket_id: DONE requires dod_checklist.$field=true (RULE DOD-002)"
|
|
280
|
+
errors=$((errors + 1))
|
|
281
|
+
fi
|
|
282
|
+
done
|
|
283
|
+
|
|
284
|
+
if ! jq -e '.completed_at' "$ticket_file" >/dev/null 2>&1 || [ "$(jq -r '.completed_at // empty' "$ticket_file")" = "" ]; then
|
|
285
|
+
log_fail "$ticket_id: DONE requires completed_at"
|
|
286
|
+
errors=$((errors + 1))
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
if ! jq -e '.pr_url' "$ticket_file" >/dev/null 2>&1 || [ "$(jq -r '.pr_url // empty' "$ticket_file")" = "" ]; then
|
|
290
|
+
log_fail "$ticket_id: DONE requires pr_url"
|
|
291
|
+
errors=$((errors + 1))
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
if [ "$(jq -r '.qa_evidence.required // true' "$ticket_file")" = "true" ]; then
|
|
295
|
+
local qa_path
|
|
296
|
+
qa_path=$(jq -r '.qa_evidence.path // empty' "$ticket_file")
|
|
297
|
+
if [ -z "$qa_path" ] || [ ! -e "$qa_path" ]; then
|
|
298
|
+
log_fail "$ticket_id: DONE requires qa_evidence.path pointing to an existing file"
|
|
299
|
+
errors=$((errors + 1))
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
# Check 12: Documentation evidence paths are real when declared
|
|
305
|
+
while IFS= read -r doc_path; do
|
|
306
|
+
[ -z "$doc_path" ] && continue
|
|
307
|
+
if [ ! -e "$doc_path" ]; then
|
|
308
|
+
log_fail "$ticket_id: documentation path does not exist: $doc_path"
|
|
309
|
+
errors=$((errors + 1))
|
|
310
|
+
fi
|
|
311
|
+
done < <(jq -r '.documentation.paths[]? // empty' "$ticket_file")
|
|
312
|
+
|
|
313
|
+
if [ "$(jq -r '.adr.required // false' "$ticket_file")" = "true" ]; then
|
|
314
|
+
local adr_path
|
|
315
|
+
adr_path=$(jq -r '.adr.path // empty' "$ticket_file")
|
|
316
|
+
if [ -z "$adr_path" ] || [ ! -e "$adr_path" ]; then
|
|
317
|
+
log_fail "$ticket_id: ADR required but adr.path is missing or does not exist"
|
|
318
|
+
errors=$((errors + 1))
|
|
319
|
+
fi
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
if [ "$(jq -r '.runbook.required // false' "$ticket_file")" = "true" ]; then
|
|
323
|
+
local runbook_path
|
|
324
|
+
runbook_path=$(jq -r '.runbook.path // empty' "$ticket_file")
|
|
325
|
+
if [ -z "$runbook_path" ] || [ ! -e "$runbook_path" ]; then
|
|
326
|
+
log_fail "$ticket_id: runbook required but runbook.path is missing or does not exist"
|
|
327
|
+
errors=$((errors + 1))
|
|
328
|
+
fi
|
|
329
|
+
fi
|
|
330
|
+
|
|
331
|
+
# Result
|
|
332
|
+
if [ "$errors" -eq 0 ]; then
|
|
333
|
+
if [ "$warnings" -gt 0 ]; then
|
|
334
|
+
log_pass "$ticket_id ($warnings warning$([ $warnings -eq 1 ] || echo s))"
|
|
335
|
+
else
|
|
336
|
+
log_pass "$ticket_id"
|
|
337
|
+
fi
|
|
338
|
+
return 0
|
|
339
|
+
else
|
|
340
|
+
return 1
|
|
341
|
+
fi
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# ============================================================
|
|
345
|
+
# Validate backlog references
|
|
346
|
+
# ============================================================
|
|
347
|
+
validate_backlog() {
|
|
348
|
+
if [ ! -f "$BACKLOG_FILE" ]; then
|
|
349
|
+
log_warn "No backlog file: $BACKLOG_FILE"
|
|
350
|
+
return 0
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
if ! jq empty "$BACKLOG_FILE" 2>/dev/null; then
|
|
354
|
+
log_fail "Backlog: Invalid JSON"
|
|
355
|
+
return 1
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
local errors=0
|
|
359
|
+
|
|
360
|
+
for field in "version" "updated_at" "updated_by" "items"; do
|
|
361
|
+
if ! jq -e ".$field" "$BACKLOG_FILE" > /dev/null 2>&1; then
|
|
362
|
+
log_fail "Backlog: Missing required field '$field'"
|
|
363
|
+
errors=$((errors + 1))
|
|
364
|
+
fi
|
|
365
|
+
done
|
|
366
|
+
|
|
367
|
+
local duplicate_ids
|
|
368
|
+
duplicate_ids=$(jq -r '.items[].ticket_id' "$BACKLOG_FILE" | sort | uniq -d | tr '\n' ' ')
|
|
369
|
+
if [ -n "$duplicate_ids" ]; then
|
|
370
|
+
log_fail "Backlog: Duplicate ticket IDs: $duplicate_ids"
|
|
371
|
+
errors=$((errors + 1))
|
|
372
|
+
fi
|
|
373
|
+
|
|
374
|
+
local duplicate_ranks
|
|
375
|
+
duplicate_ranks=$(jq -r '.items[].rank' "$BACKLOG_FILE" | sort -n | uniq -d | tr '\n' ' ')
|
|
376
|
+
if [ -n "$duplicate_ranks" ]; then
|
|
377
|
+
log_fail "Backlog: Duplicate ranks: $duplicate_ranks"
|
|
378
|
+
errors=$((errors + 1))
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
local item_count
|
|
382
|
+
item_count=$(jq '.items | length' "$BACKLOG_FILE")
|
|
383
|
+
|
|
384
|
+
if [ "$item_count" -gt 0 ]; then
|
|
385
|
+
local expected=1
|
|
386
|
+
while IFS= read -r rank; do
|
|
387
|
+
if [ "$rank" -ne "$expected" ]; then
|
|
388
|
+
log_fail "Backlog: ranks must be continuous starting at 1 (expected $expected, got $rank)"
|
|
389
|
+
errors=$((errors + 1))
|
|
390
|
+
break
|
|
391
|
+
fi
|
|
392
|
+
expected=$((expected + 1))
|
|
393
|
+
done < <(jq -r '.items[].rank' "$BACKLOG_FILE" | sort -n)
|
|
394
|
+
fi
|
|
395
|
+
|
|
396
|
+
while IFS= read -r ticket_id; do
|
|
397
|
+
if [ ! -f "$TICKETS_DIR/$ticket_id.json" ]; then
|
|
398
|
+
log_fail "Backlog: References missing ticket $ticket_id"
|
|
399
|
+
errors=$((errors + 1))
|
|
400
|
+
continue
|
|
401
|
+
fi
|
|
402
|
+
|
|
403
|
+
local backlog_priority ticket_priority status rank
|
|
404
|
+
backlog_priority=$(jq -r --arg id "$ticket_id" '.items[] | select(.ticket_id == $id) | .priority // empty' "$BACKLOG_FILE")
|
|
405
|
+
ticket_priority=$(jq -r '.priority // empty' "$TICKETS_DIR/$ticket_id.json")
|
|
406
|
+
status=$(jq -r '.status // empty' "$TICKETS_DIR/$ticket_id.json")
|
|
407
|
+
rank=$(jq -r --arg id "$ticket_id" '.items[] | select(.ticket_id == $id) | .rank' "$BACKLOG_FILE")
|
|
408
|
+
|
|
409
|
+
if [ -n "$backlog_priority" ] && [ -n "$ticket_priority" ] && [ "$backlog_priority" != "$ticket_priority" ]; then
|
|
410
|
+
log_fail "Backlog: $ticket_id priority mismatch (backlog=$backlog_priority, ticket=$ticket_priority)"
|
|
411
|
+
errors=$((errors + 1))
|
|
412
|
+
fi
|
|
413
|
+
|
|
414
|
+
if { [ "$status" = "DONE" ] || [ "$status" = "CANCELLED" ]; } && [ "$rank" -le 10 ]; then
|
|
415
|
+
log_fail "Backlog: $ticket_id is $status but still ranked in top 10"
|
|
416
|
+
errors=$((errors + 1))
|
|
417
|
+
fi
|
|
418
|
+
|
|
419
|
+
if [ "$status" = "BLOCKED" ] && ! jq -e '.unblock_plan // .blocker.unblock_plan // .comments[]? | select((.text // "") | test("unblock|blocked|dependency"; "i"))' "$TICKETS_DIR/$ticket_id.json" >/dev/null 2>&1; then
|
|
420
|
+
log_fail "Backlog: $ticket_id is BLOCKED without machine-readable unblock_plan or blocker comment"
|
|
421
|
+
errors=$((errors + 1))
|
|
422
|
+
fi
|
|
423
|
+
|
|
424
|
+
while IFS= read -r dependency_id; do
|
|
425
|
+
[ -z "$dependency_id" ] && continue
|
|
426
|
+
local dependency_rank
|
|
427
|
+
dependency_rank=$(jq -r --arg id "$dependency_id" '.items[]? | select(.ticket_id == $id) | .rank // empty' "$BACKLOG_FILE")
|
|
428
|
+
if [ -n "$dependency_rank" ] && [ "$dependency_rank" -gt "$rank" ]; then
|
|
429
|
+
log_fail "Backlog: $ticket_id rank $rank depends on $dependency_id rank $dependency_rank; dependency must be ranked first"
|
|
430
|
+
errors=$((errors + 1))
|
|
431
|
+
fi
|
|
432
|
+
done < <(jq -r '.dependencies.blocked_by[]? // empty' "$TICKETS_DIR/$ticket_id.json")
|
|
433
|
+
done < <(jq -r '.items[].ticket_id' "$BACKLOG_FILE")
|
|
434
|
+
|
|
435
|
+
if [ "$errors" -eq 0 ]; then
|
|
436
|
+
log_pass "Backlog"
|
|
437
|
+
return 0
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
return 1
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
# ============================================================
|
|
444
|
+
# Validate release governance records
|
|
445
|
+
# ============================================================
|
|
446
|
+
validate_releases() {
|
|
447
|
+
local releases_dir="project/releases"
|
|
448
|
+
|
|
449
|
+
if [ ! -d "$releases_dir" ]; then
|
|
450
|
+
return 0
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
local errors=0
|
|
454
|
+
local checked=0
|
|
455
|
+
|
|
456
|
+
while IFS= read -r -d '' release_file; do
|
|
457
|
+
checked=$((checked + 1))
|
|
458
|
+
local release_id
|
|
459
|
+
release_id=$(basename "$release_file" .json)
|
|
460
|
+
|
|
461
|
+
if ! jq empty "$release_file" 2>/dev/null; then
|
|
462
|
+
log_fail "Release $release_id: Invalid JSON"
|
|
463
|
+
errors=$((errors + 1))
|
|
464
|
+
continue
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
for field in version status created_at created_by scope approvals rollback_plan qa security known_issues; do
|
|
468
|
+
if ! jq -e ".$field" "$release_file" >/dev/null 2>&1; then
|
|
469
|
+
log_fail "Release $release_id: Missing required field '$field'"
|
|
470
|
+
errors=$((errors + 1))
|
|
471
|
+
fi
|
|
472
|
+
done
|
|
473
|
+
|
|
474
|
+
local release_status
|
|
475
|
+
release_status=$(jq -r '.status // empty' "$release_file")
|
|
476
|
+
|
|
477
|
+
if [ "$release_status" = "READY" ] || [ "$release_status" = "RELEASED" ]; then
|
|
478
|
+
for approval in tech_lead qa release_owner; do
|
|
479
|
+
if [ "$(jq -r ".approvals.$approval.approved // false" "$release_file")" != "true" ]; then
|
|
480
|
+
log_fail "Release $release_id: approvals.$approval.approved must be true when status=$release_status"
|
|
481
|
+
errors=$((errors + 1))
|
|
482
|
+
fi
|
|
483
|
+
done
|
|
484
|
+
|
|
485
|
+
if [ "$(jq -r '.rollback_plan.verified // false' "$release_file")" != "true" ]; then
|
|
486
|
+
log_fail "Release $release_id: rollback_plan.verified must be true when status=$release_status"
|
|
487
|
+
errors=$((errors + 1))
|
|
488
|
+
fi
|
|
489
|
+
|
|
490
|
+
if [ "$(jq -r '.security.dependency_audit_passed // false' "$release_file")" != "true" ] ||
|
|
491
|
+
[ "$(jq -r '.security.sast_passed // false' "$release_file")" != "true" ]; then
|
|
492
|
+
log_fail "Release $release_id: security dependency audit and SAST must pass when status=$release_status"
|
|
493
|
+
errors=$((errors + 1))
|
|
494
|
+
fi
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
if [ "$release_status" = "RELEASED" ] &&
|
|
498
|
+
[ "$(jq -r '.qa.post_release_smoke_required // true' "$release_file")" = "true" ] &&
|
|
499
|
+
[ "$(jq -r '.qa.post_release_smoke_passed // false' "$release_file")" != "true" ]; then
|
|
500
|
+
log_fail "Release $release_id: RELEASED requires post-release smoke test evidence"
|
|
501
|
+
errors=$((errors + 1))
|
|
502
|
+
fi
|
|
503
|
+
|
|
504
|
+
while IFS= read -r ticket_id; do
|
|
505
|
+
[ -z "$ticket_id" ] && continue
|
|
506
|
+
if [ ! -f "$TICKETS_DIR/$ticket_id.json" ]; then
|
|
507
|
+
log_fail "Release $release_id: references missing ticket $ticket_id"
|
|
508
|
+
errors=$((errors + 1))
|
|
509
|
+
elif [ "$(jq -r '.status' "$TICKETS_DIR/$ticket_id.json")" != "DONE" ]; then
|
|
510
|
+
log_fail "Release $release_id: includes $ticket_id but ticket is not DONE"
|
|
511
|
+
errors=$((errors + 1))
|
|
512
|
+
fi
|
|
513
|
+
done < <(jq -r '.scope.tickets[]? // empty' "$release_file")
|
|
514
|
+
|
|
515
|
+
local high_known_without_approval
|
|
516
|
+
high_known_without_approval=$(jq -r '.known_issues[]? | select((.severity == "SEV-1" or .severity == "SEV-2") and ((.approver // "") == "")) | .id' "$release_file" | tr '\n' ' ')
|
|
517
|
+
if [ -n "$high_known_without_approval" ]; then
|
|
518
|
+
log_fail "Release $release_id: high severity known issues require approver: $high_known_without_approval"
|
|
519
|
+
errors=$((errors + 1))
|
|
520
|
+
fi
|
|
521
|
+
done < <(find "$releases_dir" -name '*.json' -type f -print0)
|
|
522
|
+
|
|
523
|
+
if [ "$checked" -gt 0 ] && [ "$errors" -eq 0 ]; then
|
|
524
|
+
log_pass "Releases"
|
|
525
|
+
fi
|
|
526
|
+
|
|
527
|
+
return "$errors"
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
# ============================================================
|
|
531
|
+
# Main
|
|
532
|
+
# ============================================================
|
|
533
|
+
echo ""
|
|
534
|
+
echo "════════════════════════════════════════════════════════"
|
|
535
|
+
echo " 🔍 Validating ticket state machine"
|
|
536
|
+
echo "════════════════════════════════════════════════════════"
|
|
537
|
+
echo ""
|
|
538
|
+
|
|
539
|
+
# Verify dependencies
|
|
540
|
+
if ! command -v jq &> /dev/null; then
|
|
541
|
+
log_fail "jq not installed. Install: apt-get install jq | brew install jq"
|
|
542
|
+
exit 2
|
|
543
|
+
fi
|
|
544
|
+
|
|
545
|
+
# Verify directories
|
|
546
|
+
if [ ! -d "$TICKETS_DIR" ]; then
|
|
547
|
+
log_warn "No tickets directory: $TICKETS_DIR"
|
|
548
|
+
exit 0
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
# Determine which tickets to validate
|
|
552
|
+
TICKETS_TO_CHECK=()
|
|
553
|
+
if [ $# -gt 0 ]; then
|
|
554
|
+
for arg in "$@"; do
|
|
555
|
+
file="$TICKETS_DIR/$arg.json"
|
|
556
|
+
if [ -f "$file" ]; then
|
|
557
|
+
TICKETS_TO_CHECK+=("$file")
|
|
558
|
+
else
|
|
559
|
+
log_fail "Ticket not found: $arg"
|
|
560
|
+
exit 1
|
|
561
|
+
fi
|
|
562
|
+
done
|
|
563
|
+
else
|
|
564
|
+
while IFS= read -r -d '' file; do
|
|
565
|
+
TICKETS_TO_CHECK+=("$file")
|
|
566
|
+
done < <(find "$TICKETS_DIR" -name '*.json' -print0)
|
|
567
|
+
fi
|
|
568
|
+
|
|
569
|
+
if [ ${#TICKETS_TO_CHECK[@]} -eq 0 ]; then
|
|
570
|
+
log_warn "No tickets to validate"
|
|
571
|
+
exit 0
|
|
572
|
+
fi
|
|
573
|
+
|
|
574
|
+
log_info "Validating ${#TICKETS_TO_CHECK[@]} ticket(s)..."
|
|
575
|
+
echo ""
|
|
576
|
+
|
|
577
|
+
# Run validation
|
|
578
|
+
PASSED=0
|
|
579
|
+
FAILED=0
|
|
580
|
+
for ticket in "${TICKETS_TO_CHECK[@]}"; do
|
|
581
|
+
if validate_ticket "$ticket"; then
|
|
582
|
+
PASSED=$((PASSED + 1))
|
|
583
|
+
else
|
|
584
|
+
FAILED=$((FAILED + 1))
|
|
585
|
+
fi
|
|
586
|
+
done
|
|
587
|
+
|
|
588
|
+
if [ $# -eq 0 ]; then
|
|
589
|
+
if ! validate_backlog; then
|
|
590
|
+
FAILED=$((FAILED + 1))
|
|
591
|
+
fi
|
|
592
|
+
if ! validate_releases; then
|
|
593
|
+
FAILED=$((FAILED + 1))
|
|
594
|
+
fi
|
|
595
|
+
fi
|
|
596
|
+
|
|
597
|
+
# Summary
|
|
598
|
+
echo ""
|
|
599
|
+
echo "════════════════════════════════════════════════════════"
|
|
600
|
+
if [ "$FAILED" -eq 0 ]; then
|
|
601
|
+
echo -e " ${GREEN}✅ All $PASSED ticket(s) valid${NC}"
|
|
602
|
+
echo "════════════════════════════════════════════════════════"
|
|
603
|
+
exit 0
|
|
604
|
+
else
|
|
605
|
+
echo -e " ${RED}❌ $FAILED of $((PASSED + FAILED)) ticket(s) failed validation${NC}"
|
|
606
|
+
echo "════════════════════════════════════════════════════════"
|
|
607
|
+
echo ""
|
|
608
|
+
echo "Per RULE 06 (Approval Gates), all tickets must follow state machine."
|
|
609
|
+
echo "See: core/rules/06-approval-gates.md"
|
|
610
|
+
exit 1
|
|
611
|
+
fi
|