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.
Files changed (103) hide show
  1. package/.claude-plugin/plugin.json +21 -0
  2. package/.codex-plugin/plugin.json +35 -0
  3. package/.cursor-plugin/plugin.json +22 -0
  4. package/README.md +173 -0
  5. package/bin/ai-core-framework.js +110 -0
  6. package/core/README.md +162 -0
  7. package/core/agents/README.md +32 -0
  8. package/core/agents/business-analyst.md +269 -0
  9. package/core/agents/developer.md +375 -0
  10. package/core/agents/qa-tester.md +477 -0
  11. package/core/agents/scrum-master.md +136 -0
  12. package/core/agents/tech-lead.md +345 -0
  13. package/core/config/backlog.schema.json +38 -0
  14. package/core/config/docs-policy.default.json +37 -0
  15. package/core/config/release.schema.json +120 -0
  16. package/core/config/ticket.schema.json +253 -0
  17. package/core/rules/00-global-rules.md +373 -0
  18. package/core/rules/01-git-workflow.md +388 -0
  19. package/core/rules/02-code-quality.md +77 -0
  20. package/core/rules/03-security.md +78 -0
  21. package/core/rules/04-documentation.md +72 -0
  22. package/core/rules/05-testing-mandatory.md +374 -0
  23. package/core/rules/06-approval-gates.md +388 -0
  24. package/core/rules/07-definition-of-ready.md +112 -0
  25. package/core/rules/08-definition-of-done.md +149 -0
  26. package/core/scripts/ai-core.sh +456 -0
  27. package/core/scripts/generate-views.sh +210 -0
  28. package/core/scripts/install-codex-prompts.sh +127 -0
  29. package/core/scripts/log-user-request.sh +113 -0
  30. package/core/scripts/setup-project.sh +183 -0
  31. package/core/scripts/sync-platforms.sh +322 -0
  32. package/core/scripts/validate-audit-log.sh +73 -0
  33. package/core/scripts/validate-docs.sh +365 -0
  34. package/core/scripts/validate-permissions.sh +132 -0
  35. package/core/scripts/validate-state.sh +611 -0
  36. package/core/scripts/workflow.sh +513 -0
  37. package/core/skills/README.md +21 -0
  38. package/core/skills/ai-core-commands/SKILL.md +86 -0
  39. package/core/skills/brainstorming/SKILL.md +40 -0
  40. package/core/skills/development-implement-task/SKILL.md +308 -0
  41. package/core/skills/executing-ticket/SKILL.md +28 -0
  42. package/core/skills/git-branch-status/SKILL.md +56 -0
  43. package/core/skills/git-cleanup-branches/SKILL.md +57 -0
  44. package/core/skills/git-scan-untracked/SKILL.md +50 -0
  45. package/core/skills/meta-generate-views/SKILL.md +54 -0
  46. package/core/skills/meta-request-log/SKILL.md +61 -0
  47. package/core/skills/meta-sprint-report/SKILL.md +59 -0
  48. package/core/skills/meta-sync-platforms/SKILL.md +53 -0
  49. package/core/skills/meta-ticket-health/SKILL.md +61 -0
  50. package/core/skills/meta-validate-audit-log/SKILL.md +42 -0
  51. package/core/skills/meta-validate-docs/SKILL.md +58 -0
  52. package/core/skills/meta-validate-permissions/SKILL.md +53 -0
  53. package/core/skills/meta-validate-state/SKILL.md +58 -0
  54. package/core/skills/planning-analyze-requirements/SKILL.md +471 -0
  55. package/core/skills/planning-backlog-status/SKILL.md +57 -0
  56. package/core/skills/planning-document-existing-requirements/SKILL.md +246 -0
  57. package/core/skills/planning-estimate-task/SKILL.md +60 -0
  58. package/core/skills/planning-groom-ticket/SKILL.md +442 -0
  59. package/core/skills/planning-mark-ready/SKILL.md +111 -0
  60. package/core/skills/planning-plan-refactor/SKILL.md +66 -0
  61. package/core/skills/planning-plan-sprint/SKILL.md +112 -0
  62. package/core/skills/planning-prioritize-backlog/SKILL.md +62 -0
  63. package/core/skills/planning-write-plan/SKILL.md +68 -0
  64. package/core/skills/project-detect-stack/SKILL.md +71 -0
  65. package/core/skills/project-discover-codebase/SKILL.md +74 -0
  66. package/core/skills/project-setup-project/SKILL.md +113 -0
  67. package/core/skills/qa-bug-status/SKILL.md +52 -0
  68. package/core/skills/qa-report-bug/SKILL.md +518 -0
  69. package/core/skills/qa-smoke-test/SKILL.md +387 -0
  70. package/core/skills/qa-triage-bug/SKILL.md +62 -0
  71. package/core/skills/qa-verify-fix/SKILL.md +446 -0
  72. package/core/skills/release-hotfix/SKILL.md +117 -0
  73. package/core/skills/release-release/SKILL.md +123 -0
  74. package/core/skills/release-rollback/SKILL.md +62 -0
  75. package/core/skills/review-create-pr/SKILL.md +418 -0
  76. package/core/skills/review-merge-pr/SKILL.md +425 -0
  77. package/core/skills/review-techlead-review/SKILL.md +547 -0
  78. package/core/skills/using-ai-core/SKILL.md +72 -0
  79. package/core/skills/verification-before-done/SKILL.md +35 -0
  80. package/core/skills/writing-implementation-plan/SKILL.md +45 -0
  81. package/core/templates/ci/ai-core-governance.yml +112 -0
  82. package/core/templates/ci/node-pnpm.yml +35 -0
  83. package/core/templates/pm/retrospective-template.md +47 -0
  84. package/core/templates/pm/sprint-plan-template.md +45 -0
  85. package/core/templates/pr/pull-request-template.md +247 -0
  86. package/core/templates/project/CODEOWNERS +11 -0
  87. package/core/templates/project/docs-policy.json +3 -0
  88. package/core/templates/project/project-config.yaml +137 -0
  89. package/core/templates/project/project-structure.yaml +76 -0
  90. package/core/templates/qa/bug-report-template.md +371 -0
  91. package/core/templates/qa/test-plan-template.md +57 -0
  92. package/core/templates/release/release-record-template.json +67 -0
  93. package/core/templates/requirements/PRD-template.md +58 -0
  94. package/core/templates/requirements/user-story-template.md +381 -0
  95. package/core/templates/technical/ADR-template.md +46 -0
  96. package/core/templates/technical/refactor-plan-template.md +84 -0
  97. package/core/templates/technical/tech-design-template.md +71 -0
  98. package/core/workflows/bug-lifecycle.md +56 -0
  99. package/core/workflows/feature-lifecycle.md +347 -0
  100. package/core/workflows/hotfix-lifecycle.md +65 -0
  101. package/core/workflows/sprint-lifecycle.md +56 -0
  102. package/lib/install-codex.js +85 -0
  103. 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