tickmarkr 2.7.7 → 2.7.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.7.7",
3
+ "version": "2.7.8",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -94,6 +94,7 @@
94
94
  "skills/tickmarkr-auto/",
95
95
  "skills/tickmarkr-loop/",
96
96
  "skills/tickmarkr-overseer/",
97
+ "scripts/ci/",
97
98
  "src/",
98
99
  "tests/"
99
100
  ]
@@ -1601,9 +1601,12 @@ Create specialist seats using `orca terminal create --worktree path:<repo> --com
1601
1601
 
1602
1602
  - **GRADE PUBLIC CI FROM JOB LOGS AT RUN-END ONLY (law 48).** A badge is not the grade, and
1603
1603
  `gh run view --job --log` is empty while the run is in progress. At run-end execute
1604
- `scripts/grade-ci.sh <run-id> <expected-count> <tag>` and require GREEN from both jobs; its
1605
- UNREADABLE state is a hard stop, not a green default. Derive the expected glob pathspec count with
1606
- `git ls-files ':(glob)tests/**/*.test.ts'` (including the `.tsx` sibling where present), never a bare
1604
+ `scripts/grade-ci.sh <run-id> <expected-count> <tag>` and require GREEN from every job of the
1605
+ declared inventory it prints first — serial `test` and `test-macos`, or sharded, per OS, the four
1606
+ `suite <os> k/4` shards, `single-fork <os>` and `aggregate <os>`; a missing, duplicated or unknown
1607
+ job reads UNREADABLE, never a skip. Its UNREADABLE state is a hard stop, not a green default.
1608
+ Derive the expected glob pathspec count with `git ls-files ':(glob)tests/**/*.test.ts'`
1609
+ (including the `.tsx` sibling where present), never a bare
1607
1610
  `tests/**/*.test.ts`: bare `**` is not a git glob (law 49).
1608
1611
 
1609
1612
  - **RE-READ THE REGISTRY BEFORE RULING (law 51).** A successful Release run and the first
@@ -1,9 +1,36 @@
1
1
  #!/bin/bash
2
- # grade-ci.sh <run-id> <expected-count> [tag] — grade both public CI jobs from their JOB LOGS.
3
- # Grade only at run-end. A missing/in-progress/empty log is UNREADABLE, never evidence of green.
2
+ # grade-ci.sh <run-id> <expected-count> [tag] — grade EVERY job of a public CI run's closed inventory from
3
+ # its JOB LOG. Grade only at run-end. A missing/in-progress/empty log is UNREADABLE, never evidence of green.
4
4
  # Exit 0 GREEN, 1 RED, 2 UNREADABLE, 3 INFRA_KILLED: a job the GitHub runner shut down or cancelled with
5
5
  # no failed or timed-out test (the classifier's class, D-897) — never green, never a pass, only unproven.
6
6
  # UNREADABLE dominates a mixed result, then RED: a failure anywhere dominates a kill.
7
+ #
8
+ # Two CLOSED inventories (v2.7.8 T4; the workflow renders these names): serial {test, test-macos}; sharded,
9
+ # for os in ubuntu-latest and macos-latest, `suite <os> k/4` for k 1..4, `single-fork <os>`, `aggregate <os>`.
10
+ # A run holding any sharded name is graded against the sharded inventory, else the serial one. The first
11
+ # line is `INVENTORY form=<serial|sharded> declared=<n> observed=<n>`; each missing, duplicated or unknown
12
+ # name prints `INVENTORY_DEFECT kind=<missing|duplicate|unknown> job=<json-quoted name>` and makes the run
13
+ # UNREADABLE: a job the inventory does not name is never skipped.
14
+ #
15
+ # A verdict line is read only from the log lines whose gh step column equals its declared step, exactly
16
+ # once, else UNREADABLE — a line a test or any other step prints, before or after the real one, never decides:
17
+ # COUNT_ORACLE <GREEN|RED> expected=<n|UNREADABLE> actual=<n|UNREADABLE> step `Assert collected test-file count`
18
+ # COVERAGE_FLOORS verdict=… floors=… failing=… (T1's final floors line) step `Recheck coverage floors`
19
+ # SET_ORACLE verdict=… expected=… actual=… defects=… (T3's set oracle) step `Assert collected test-file set`
20
+ # ROLES — the shared classifier (classify-vitest-log.sh) stays the one log verdict:
21
+ # serial the base rule (count oracle GREEN at the expected count, the classifier given that count
22
+ # reading CLEAN or RPC_ONLY, passed+skipped equal to it) AND exactly one COVERAGE_FLOORS
23
+ # verdict=GREEN, so a dropped floors step reads UNREADABLE.
24
+ # shard, single-fork
25
+ # GREEN only on conclusion success whose whole log the classifier, given no count, reads CLEAN
26
+ # or RPC_ONLY: their wrapper already applied the shard contract in-job.
27
+ # aggregate GREEN only on conclusion success with exactly one COVERAGE_FLOORS verdict=GREEN, exactly one
28
+ # `SET_ORACLE verdict=GREEN expected=N actual=N defects=0`, and the lines of its step
29
+ # `Merge coverage (one threshold check per OS)` classified CLEAN or RPC_ONLY.
30
+ # Exit annotations: SERIAL keeps the one exception run-ci-vitest.sh grants (a failure explained ONLY by steps
31
+ # that exited exactly 1 on a log of their own the classifier reads RPC_ONLY); any other failing annotation is
32
+ # never GREEN. Shard, single-fork and aggregate admit none — their wrapper applied the exception in-job.
33
+ # INFRA_KILLED is the base rule for every role.
7
34
  set -u
8
35
 
9
36
  run=${1:?run id required}
@@ -14,34 +41,96 @@ repo=${TKR_GRADE_CI_REPO:-alzahrani-khalid/tickmarkr}
14
41
  out_dir=${TKR_GRADE_CI_DIR:-${TKR_STATE_DIR:-.tickmarkr}/overseer/diag}
15
42
  mkdir -p "$out_dir" || { echo "UNREADABLE: cannot create log directory $out_dir"; exit 2; }
16
43
 
44
+ serial_inventory=$'test\ntest-macos'
45
+ sharded_inventory=$(for os in ubuntu-latest macos-latest; do
46
+ for k in 1 2 3 4; do echo "suite $os $k/4"; done
47
+ echo "single-fork $os"
48
+ echo "aggregate $os"
49
+ done)
50
+ COUNT_STEP="Assert collected test-file count"
51
+ FLOORS_STEP="Recheck coverage floors"
52
+ SET_STEP="Assert collected test-file set"
53
+ MERGE_STEP="Merge coverage (one threshold check per OS)"
54
+ COUNT_FORMAT='COUNT_ORACLE (GREEN|RED) expected=([0-9]+|UNREADABLE) actual=([0-9]+|UNREADABLE)'
55
+ FLOORS_FORMAT='COVERAGE_FLOORS verdict=(GREEN|RED|UNREADABLE) floors=([0-9]+|UNREADABLE) failing=([0-9]+|UNREADABLE)'
56
+ SET_FORMAT='SET_ORACLE verdict=(GREEN|RED|UNREADABLE) expected=([0-9]+|UNREADABLE) actual=([0-9]+|UNREADABLE) defects=[0-9]+'
57
+
17
58
  verdict=0
18
59
  mark_unreadable() { verdict=2; }
19
60
  mark_red() { [ "$verdict" -eq 2 ] || verdict=1; }
20
61
  mark_killed() { [ "$verdict" -ne 0 ] || verdict=3; }
21
62
  field() { printf '%s\n' "$classified" | sed -n "s/^VITEST_LOG.* $1=\([^ ]*\).*/\1/p"; }
63
+ count() { printf '%s' "$1" | grep -c ''; }
64
+ # json <name> — one JSON string; awk -v decodes gh's @tsv escapes (\t \n \r \\) first
65
+ json() {
66
+ LC_ALL=C awk -v s="$1" 'BEGIN {
67
+ for (i = 1; i < 32; i++) esc[sprintf("%c", i)] = sprintf("\\u%04x", i)
68
+ esc["\""] = "\\\""; esc["\\"] = "\\\\"
69
+ for (i = 1; i <= length(s); i++) { c = substr(s, i, 1); out = out (c in esc ? esc[c] : c) }
70
+ print "\"" out "\""
71
+ }'
72
+ }
73
+ defect() { echo "INVENTORY_DEFECT kind=$1 job=$(json "$2")"; mark_unreadable; }
74
+ # step_log <log> <step> — the job-log lines gh's step column attributes to <step>, as gh printed them
75
+ step_log() { awk -F'\t' -v s="$2" 'NF >= 3 && $2 == s' "$1"; }
76
+ # verdict_line <log> <step> <family> <format> — the ONE <family> line <step> printed, in <format>;
77
+ # else MISSING, MULTIPLE(n) or MALFORMED, each UNREADABLE
78
+ verdict_line() {
79
+ local lines
80
+ lines=$(step_log "$1" "$2" | awk -v esc="$(printf '\033')" '{
81
+ gsub(/\^\[\[[0-9;]*m/, ""); gsub(esc "\\[[0-9;]*m", "")
82
+ sub(/^[^\t]*\t[^\t]*\t[0-9T:.-]+Z ?/, ""); sub(/[ \t\r]+$/, ""); print
83
+ }' | grep -E "^$3( |\$)")
84
+ case $(count "$lines") in
85
+ 0) echo MISSING ;;
86
+ 1) if printf '%s\n' "$lines" | grep -qxE "$4"; then printf '%s\n' "$lines"; else echo MALFORMED; fi ;;
87
+ *) echo "MULTIPLE($(count "$lines"))" ;;
88
+ esac
89
+ }
90
+ readable() { case $1 in MISSING|MULTIPLE*|MALFORMED) return 1 ;; esac; }
91
+ floors_green() { printf '%s\n' "$1" | grep -qxE 'COVERAGE_FLOORS verdict=GREEN floors=[0-9]+ failing=0'; }
22
92
 
23
93
  jobs=$(gh run view "$run" --repo "$repo" --json jobs \
24
94
  --jq '.jobs[] | [.databaseId, .name, .status, (.conclusion // "")] | @tsv') \
25
95
  || { echo "UNREADABLE: job list"; exit 2; }
26
- [ -n "$jobs" ] || { echo "UNREADABLE: empty job list"; exit 2; }
96
+ # an empty job list is an inventory like any other: serial, observed=0, every declared job missing
97
+
98
+ names=$(printf '%s\n' "$jobs" | cut -f2)
99
+ if printf '%s\n' "$names" | grep -qxF -f <(printf '%s\n' "$sharded_inventory"); then
100
+ form=sharded; declared=$sharded_inventory
101
+ else
102
+ form=serial; declared=$serial_inventory
103
+ fi
104
+ echo "INVENTORY form=$form declared=$(count "$declared") observed=$(count "$names")"
105
+ while IFS= read -r name; do
106
+ case $(printf '%s\n' "$names" | grep -cxF -- "$name") in
107
+ 0) defect missing "$name" ;;
108
+ 1) ;;
109
+ *) defect duplicate "$name" ;;
110
+ esac
111
+ done <<< "$declared"
112
+ while IFS= read -r name; do
113
+ printf '%s\n' "$declared" | grep -qxF -- "$name" || defect unknown "$name"
114
+ done < <([ -z "$jobs" ] || printf '%s\n' "$names" | awk '!seen[$0]++')
27
115
 
28
- seen_test=0
29
- seen_macos=0
30
- while IFS=$'\t' read -r id name status conclusion; do
116
+ [ -z "$jobs" ] || while IFS=$'\t' read -r id name status conclusion; do
117
+ # an unknown job has no role to grade: its INVENTORY_DEFECT already made the run UNREADABLE
118
+ printf '%s\n' "$declared" | grep -qxF -- "$name" || continue
31
119
  case "$name" in
32
- test) seen_test=1 ;;
33
- test-macos) seen_macos=1 ;;
34
- *) continue ;;
120
+ test|test-macos) role=serial ;;
121
+ suite\ *) role=shard ;;
122
+ single-fork\ *) role=single-fork ;;
123
+ *) role=aggregate ;;
35
124
  esac
36
125
 
37
- echo "$name: status=$status conclusion=${conclusion:-none} job=$id"
126
+ echo "$name: role=$role status=$status conclusion=${conclusion:-none} job=$id"
38
127
  if [ "$status" != "completed" ]; then
39
128
  echo "$name: UNREADABLE (job has not reached run-end)"
40
129
  mark_unreadable
41
130
  continue
42
131
  fi
43
132
 
44
- log="$out_dir/CI-$tag-$name.log"
133
+ log="$out_dir/CI-$tag-$(printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '-').log"
45
134
  gh run view --repo "$repo" --job "$id" --log > "$log" 2>/dev/null
46
135
  if [ ! -s "$log" ]; then
47
136
  echo "$name: UNREADABLE (empty log)"
@@ -49,24 +138,28 @@ while IFS=$'\t' read -r id name status conclusion; do
49
138
  continue
50
139
  fi
51
140
 
52
- oracle=$(grep -oE 'COUNT_ORACLE [A-Z]+ expected=[0-9A-Z]+ actual=[0-9A-Z]+' "$log" | tail -1)
53
141
  files=$(grep -oE 'Test Files .*' "$log" | sed 's/[[:space:]]*$//')
54
142
  # OBS-1184: the log verdict comes from the ONE classifier the public CI wrapper also uses
55
143
  # (scripts/run-ci-vitest.sh), so the badge and this grade cannot disagree. Its rules — complete
56
144
  # summaries, failed/timed-out tests, coverage-threshold misses, and the RPC-timeout-only exception
57
145
  # to "any unhandled error is RED" (OBS-1058) — live in classify-vitest-log.sh, stated once there.
58
- # The expected count is the job's declared file contract: both steps' summaries must collect it all.
59
- classified=$(bash "$here/classify-vitest-log.sh" "$log" "$expected" 2>&1)
146
+ # A serial job's expected count is its declared file contract: both steps' summaries must collect it all.
147
+ # A shard or single-fork job carries no count: its wrapper held it to its plan in-job.
148
+ if [ "$role" = serial ]; then
149
+ classified=$(bash "$here/classify-vitest-log.sh" "$log" "$expected" 2>&1)
150
+ else
151
+ classified=$(bash "$here/classify-vitest-log.sh" "$log" 2>&1)
152
+ fi
60
153
  log_verdict=$(field verdict)
61
154
  passed=$(field passed); skipped=$(field skipped); failed=$(field failed); timedout=$(field timedout)
62
155
  unhandled=$(field unhandled); rpc=$(field runner_rpc_timeouts); coverage=$(field coverage_misses)
63
156
  kills=$(field runner_kills)
64
157
  errors=$(grep -oE '##\[error\].*' "$log" | sort | uniq -c | sed 's/^ *//' | tr '\n' ';')
65
158
  # The commands' own outcomes stand, as in the wrapper: GREEN needs a success conclusion with no step
66
- # exit annotation, or a failure explained ONLY by steps that exited exactly 1 on a log of their own
67
- # (gh's step column) that the classifier calls RPC_ONLY — the one exception run-ci-vitest.sh grants
68
- # (a pre-wrapper run concludes failure on it). Any other code (2, a signal's 137), an unexplained
69
- # failure, or a cancelled, timed-out or missing conclusion is RED.
159
+ # exit annotation, or — SERIAL only — a failure explained ONLY by steps that exited exactly 1 on a log
160
+ # of their own (gh's step column) that the classifier calls RPC_ONLY. Any other code (2, a signal's
161
+ # 137), any annotation on another role, an unexplained failure, or a cancelled, timed-out or missing
162
+ # conclusion is RED.
70
163
  exits=$(grep -E '##\[error\]Process completed with exit code [0-9]+' "$log")
71
164
  case "$conclusion" in
72
165
  success) outcome=ok ;;
@@ -76,9 +169,9 @@ while IFS=$'\t' read -r id name status conclusion; do
76
169
  if [ -n "$exits" ]; then
77
170
  while IFS= read -r exit_line; do
78
171
  code=$(printf '%s\n' "$exit_line" | sed -E 's/.*Process completed with exit code ([0-9]+).*/\1/')
79
- if [ "$code" != 1 ]; then outcome="exit-$code"; break; fi
172
+ if [ "$code" != 1 ] || [ "$role" != serial ]; then outcome="exit-$code"; break; fi
80
173
  step=$(printf '%s\n' "$exit_line" | awk -F'\t' 'NF >= 3 { print $2 }')
81
- awk -F'\t' -v s="$step" 'NF >= 3 && $2 == s' "$log" > "$log.step"
174
+ step_log "$log" "$step" > "$log.step"
82
175
  case $(bash "$here/classify-vitest-log.sh" "$log.step" 2>&1) in
83
176
  "VITEST_LOG verdict=RPC_ONLY "*) ;;
84
177
  *) outcome="exit-1-not-rpc-only"; break ;;
@@ -86,36 +179,60 @@ while IFS=$'\t' read -r id name status conclusion; do
86
179
  done <<< "$exits"
87
180
  fi
88
181
 
89
- echo "$name: oracle=[${oracle:-MISSING}] files=[$(printf '%s' "$files" | tr '\n' '|')] passed=$passed skipped=$skipped failed=$failed timedout=$timedout unhandled=$unhandled runner_rpc_timeouts=$rpc coverage_misses=$coverage runner_kills=${kills:-?} log=[$(field reason)] outcome=[$outcome] errors=[$errors]"
90
- # A killed runner skips the count step, so its missing oracle is the kill's, not an unreadable log:
182
+ oracles=
183
+ case $role in
184
+ serial)
185
+ oracle=$(verdict_line "$log" "$COUNT_STEP" COUNT_ORACLE "$COUNT_FORMAT")
186
+ floors=$(verdict_line "$log" "$FLOORS_STEP" COVERAGE_FLOORS "$FLOORS_FORMAT")
187
+ oracles="oracle=[$oracle] floors=[$floors] " ;;
188
+ aggregate)
189
+ floors=$(verdict_line "$log" "$FLOORS_STEP" COVERAGE_FLOORS "$FLOORS_FORMAT")
190
+ set_oracle=$(verdict_line "$log" "$SET_STEP" SET_ORACLE "$SET_FORMAT")
191
+ step_log "$log" "$MERGE_STEP" > "$log.merge"
192
+ merge=$(bash "$here/classify-vitest-log.sh" "$log.merge" 2>&1 | sed -n 's/^VITEST_LOG verdict=\([^ ]*\) reason=\([^ ]*\) .*/\1 \2/p')
193
+ oracles="floors=[$floors] set=[$set_oracle] merge=[$merge] " ;;
194
+ esac
195
+
196
+ echo "$name: ${oracles}files=[$(printf '%s' "$files" | tr '\n' '|')] passed=$passed skipped=$skipped failed=$failed timedout=$timedout unhandled=$unhandled runner_rpc_timeouts=$rpc coverage_misses=$coverage runner_kills=${kills:-?} log=[$(field reason)] outcome=[$outcome] errors=[$errors]"
197
+ # A killed runner skips the oracle steps, so a missing verdict line is the kill's, not an unreadable log:
91
198
  # INFRA_KILLED unless a step's own failing exit or a failed/timed-out test dominates it (then RED).
92
199
  if [ "$log_verdict" = INFRA_KILLED ] && [ "${outcome#exit-}" = "$outcome" ]; then
93
200
  echo "$name: INFRA_KILLED"
94
201
  mark_killed
202
+ continue
95
203
  elif [ "${kills:-0}" -gt 0 ] 2>/dev/null; then
96
- echo "$name: RED"
97
- mark_red
98
- elif [ -z "$oracle" ] || [ -z "$files" ] || [ -z "$log_verdict" ]; then
99
- echo "$name: UNREADABLE"
100
- mark_unreadable
101
- elif [ "$oracle" = "COUNT_ORACLE GREEN expected=$expected actual=$expected" ] \
102
- && { [ "$log_verdict" = CLEAN ] || [ "$log_verdict" = RPC_ONLY ]; } && [ "$outcome" = ok ] \
103
- && [ $((passed + skipped)) -eq "$expected" ]; then
104
- echo "$name: GREEN"
204
+ grade=RED
205
+ elif [ -z "$log_verdict" ]; then
206
+ grade=UNREADABLE
105
207
  else
106
- echo "$name: RED"
107
- mark_red
208
+ case $log_verdict in CLEAN|RPC_ONLY) log_ok=1 ;; *) log_ok= ;; esac
209
+ grade=RED
210
+ case $role in
211
+ serial)
212
+ if ! readable "$oracle" || ! readable "$floors" || [ -z "$files" ]; then
213
+ grade=UNREADABLE
214
+ elif [ "$oracle" = "COUNT_ORACLE GREEN expected=$expected actual=$expected" ] && floors_green "$floors" \
215
+ && [ -n "$log_ok" ] && [ "$outcome" = ok ] && [ $((passed + skipped)) -eq "$expected" ]; then
216
+ grade=GREEN
217
+ fi ;;
218
+ shard|single-fork)
219
+ if [ "$outcome" = ok ] && [ "$conclusion" = success ] && [ -n "$log_ok" ]; then grade=GREEN; fi ;;
220
+ aggregate)
221
+ if ! readable "$floors" || ! readable "$set_oracle"; then
222
+ grade=UNREADABLE
223
+ elif [ "$outcome" = ok ] && [ "$conclusion" = success ] && floors_green "$floors" \
224
+ && [ "$set_oracle" = "SET_ORACLE verdict=GREEN expected=$expected actual=$expected defects=0" ] \
225
+ && { [ "${merge%% *}" = CLEAN ] || [ "${merge%% *}" = RPC_ONLY ]; }; then
226
+ grade=GREEN
227
+ fi ;;
228
+ esac
108
229
  fi
230
+ echo "$name: $grade"
231
+ case $grade in
232
+ RED) mark_red ;;
233
+ UNREADABLE) mark_unreadable ;;
234
+ esac
109
235
  done <<< "$jobs"
110
236
 
111
- if [ "$seen_test" -ne 1 ]; then
112
- echo "test: UNREADABLE (job missing from run)"
113
- mark_unreadable
114
- fi
115
- if [ "$seen_macos" -ne 1 ]; then
116
- echo "test-macos: UNREADABLE (job missing from run)"
117
- mark_unreadable
118
- fi
119
-
120
237
  echo "VERDICT rc=$verdict (0=all GREEN 1=RED 2=UNREADABLE 3=INFRA_KILLED)"
121
238
  exit "$verdict"