@drafthq/draft 3.3.1 → 3.5.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.
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env bash
2
+ # okf-plan-concepts.sh — derive the DETERMINISTIC expected-concept set for an OKF
3
+ # bundle, BEFORE any page is written.
4
+ #
5
+ # The OKF emitter used to let the LLM enumerate the concept list in-context, so
6
+ # under context pressure it silently under-enumerated and modules went
7
+ # undocumented — and okf-validate.sh only ever checked the pages that *did* get
8
+ # written. This tool makes the boundary of the work a tool output: every package
9
+ # / module / component the graph knows about (at or above a fan-in floor), plus
10
+ # every entrypoint, becomes a REQUIRED concept the bundle must contain. Pages
11
+ # below the floor (or matching an allow-defer glob) are recorded as deferred with
12
+ # a reason, never silently dropped.
13
+ #
14
+ # Discovery priority:
15
+ # 1. --manifest FILE — explicit component list (authoritative; every entry required)
16
+ # 2. graph — graph-arch.sh packages (fan_in) + entry_points
17
+ # 3. heuristic — top-level source dirs (engine unavailable; degraded:true)
18
+ #
19
+ # Output: concept-plan.json (see schema below). The generation loop iterates
20
+ # `generated_order`; okf-coverage-check.sh gates promotion on every required
21
+ # `concept_id` existing as a non-stub page.
22
+ #
23
+ # Usage:
24
+ # okf-plan-concepts.sh --repo DIR [--scope PATH] [--manifest FILE]
25
+ # [--min-fan-in N] [--allow-defer GLOB]... [--out FILE] [--json]
26
+ #
27
+ # Exit codes: 0 plan written, 1 invocation error, 2 no expected set could be
28
+ # derived (graph + manifest both unavailable).
29
+ set -euo pipefail
30
+
31
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+ # shellcheck source=scripts/tools/_lib.sh
33
+ source "$SCRIPT_DIR/_lib.sh"
34
+
35
+ REPO="."
36
+ SCOPE="."
37
+ MANIFEST=""
38
+ MIN_FAN_IN=2
39
+ OUT=""
40
+ JSON=0
41
+ ALLOW_DEFER=()
42
+
43
+ usage() {
44
+ cat <<'EOF'
45
+ okf-plan-concepts.sh — derive the deterministic expected-concept set for an OKF bundle.
46
+
47
+ Usage:
48
+ okf-plan-concepts.sh --repo DIR [--scope PATH] [--manifest FILE]
49
+ [--min-fan-in N] [--allow-defer GLOB]... [--out FILE] [--json]
50
+
51
+ Flags:
52
+ --repo DIR Repository root (default: cwd).
53
+ --scope PATH Sub-tree for module-scoped init (default: .).
54
+ --manifest FILE Component list (one component per line; '#' comments; blanks
55
+ ignored). When present it is authoritative — every entry is
56
+ required and the graph is not consulted.
57
+ --min-fan-in N Package fan-in floor for "required" (default: 2). Packages
58
+ below the floor are deferred with a reason.
59
+ --allow-defer GLOB Defer (don't require) components whose name matches GLOB.
60
+ Repeatable. Deferred entries still appear in the plan.
61
+ --out FILE Write the plan JSON here (default: stdout).
62
+ --json Also echo the plan JSON to stdout when --out is given.
63
+ --help Show this help.
64
+
65
+ Exit: 0 plan written, 1 invocation error, 2 no expected set derivable.
66
+ EOF
67
+ }
68
+
69
+ while [[ $# -gt 0 ]]; do
70
+ case "$1" in
71
+ --repo) REPO="$2"; shift 2;;
72
+ --scope) SCOPE="$2"; shift 2;;
73
+ --manifest) MANIFEST="$2"; shift 2;;
74
+ --min-fan-in) MIN_FAN_IN="$2"; shift 2;;
75
+ --allow-defer) ALLOW_DEFER+=("$2"); shift 2;;
76
+ --out) OUT="$2"; shift 2;;
77
+ --json) JSON=1; shift;;
78
+ --help|-h) usage; exit 0;;
79
+ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
80
+ *) echo "Unexpected arg: $1" >&2; usage >&2; exit 1;;
81
+ esac
82
+ done
83
+
84
+ [[ -d "$REPO" ]] || { echo "ERROR: --repo '$REPO' is not a directory" >&2; exit 1; }
85
+ [[ "$MIN_FAN_IN" =~ ^[0-9]+$ ]] || { echo "ERROR: --min-fan-in must be an integer" >&2; exit 1; }
86
+
87
+ # Slugify a component name into a bundle-safe filename stem.
88
+ slug() {
89
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' \
90
+ | sed -E 's/^-+//; s/-+$//; s/-+/-/g'
91
+ }
92
+
93
+ # Does $1 match any --allow-defer glob?
94
+ is_deferred_name() {
95
+ local name="$1" g
96
+ for g in "${ALLOW_DEFER[@]:-}"; do
97
+ [[ -z "$g" ]] && continue
98
+ # shellcheck disable=SC2053
99
+ [[ "$name" == $g ]] && return 0
100
+ done
101
+ return 1
102
+ }
103
+
104
+ # Accumulators (parallel arrays describing each expected concept).
105
+ E_ID=(); E_TYPE=(); E_RES=(); E_FANIN=(); E_REQ=(); E_REASON=()
106
+ SOURCE="heuristic"
107
+ DEGRADED="false"
108
+
109
+ add_concept() {
110
+ # name section type resource fan_in required reason
111
+ local name="$1" section="$2" type="$3" resource="$4" fan_in="$5" required="$6" reason="$7"
112
+ local stem; stem="$(slug "$name")"
113
+ [[ -n "$stem" ]] || stem="component"
114
+ E_ID+=("$section/$stem.md")
115
+ E_TYPE+=("$type")
116
+ E_RES+=("$resource")
117
+ E_FANIN+=("$fan_in")
118
+ E_REQ+=("$required")
119
+ E_REASON+=("$reason")
120
+ }
121
+
122
+ # --- 1. Manifest path (authoritative) ---
123
+ plan_from_manifest() {
124
+ local line name
125
+ while IFS= read -r line || [[ -n "$line" ]]; do
126
+ line="${line%%#*}"
127
+ name="$(printf '%s' "$line" | sed -E 's/^[[:space:]]*-?[[:space:]]*//; s/[[:space:]]*$//')"
128
+ [[ -z "$name" ]] && continue
129
+ if is_deferred_name "$name"; then
130
+ add_concept "$name" systems Module "$name" 0 false "manifest: allow-defer match"
131
+ else
132
+ add_concept "$name" systems Module "$name" 0 true ""
133
+ fi
134
+ done < "$MANIFEST"
135
+ SOURCE="manifest"
136
+ }
137
+
138
+ # --- 2. Graph path ---
139
+ plan_from_graph() {
140
+ local arch; arch="$(scripts_graph_arch)" || return 1
141
+ [[ -n "$arch" ]] || return 1
142
+ echo "$arch" | jq -e '.packages != null' >/dev/null 2>&1 || return 1
143
+
144
+ SOURCE="graph"
145
+ local name fan_in type required reason
146
+ # Packages → systems/<pkg>.md
147
+ while IFS=$'\t' read -r name fan_in; do
148
+ [[ -z "$name" ]] && continue
149
+ if is_deferred_name "$name"; then
150
+ required=false; reason="allow-defer match"; type=Module
151
+ elif (( fan_in >= MIN_FAN_IN )); then
152
+ required=true; reason=""; type=Subsystem
153
+ else
154
+ required=false; reason="fan_in $fan_in < floor $MIN_FAN_IN"; type=Module
155
+ fi
156
+ add_concept "$name" systems "$type" "$name" "$fan_in" "$required" "$reason"
157
+ done < <(echo "$arch" | jq -r '.packages[]? | [.name, (.fan_in // 0)] | @tsv')
158
+
159
+ # Entry points → entrypoints/<name>.md (always required)
160
+ while IFS= read -r name; do
161
+ [[ -z "$name" ]] && continue
162
+ if is_deferred_name "$name"; then
163
+ add_concept "$name" entrypoints Entrypoint "$name" 0 false "allow-defer match"
164
+ else
165
+ add_concept "$name" entrypoints Entrypoint "$name" 0 true ""
166
+ fi
167
+ done < <(echo "$arch" | jq -r '
168
+ (.entry_points // [])[]? | if type=="object" then (.name // .path // empty) else . end' \
169
+ | sort -u)
170
+ }
171
+
172
+ # graph-arch.sh wrapper that tolerates the "unavailable" sentinel.
173
+ scripts_graph_arch() {
174
+ local out
175
+ out="$("$SCRIPT_DIR/graph-arch.sh" --repo "$REPO" 2>/dev/null || true)"
176
+ [[ -n "$out" ]] || return 1
177
+ echo "$out" | jq -e '.source == "unavailable"' >/dev/null 2>&1 && return 1
178
+ printf '%s' "$out"
179
+ }
180
+
181
+ # --- 3. Heuristic fallback ---
182
+ plan_from_heuristic() {
183
+ SOURCE="heuristic"
184
+ DEGRADED="true"
185
+ local scope_dir="$REPO/$SCOPE"
186
+ [[ -d "$scope_dir" ]] || scope_dir="$REPO"
187
+ local d name
188
+ while IFS= read -r d; do
189
+ name="$(basename "$d")"
190
+ case "$name" in
191
+ test|tests|qa|tools|vendor|node_modules|.git|dist|build|target) continue;;
192
+ .*) continue;;
193
+ esac
194
+ # Only dirs that actually contain source-ish files.
195
+ if find "$d" -maxdepth 2 -type f \
196
+ \( -name '*.go' -o -name '*.py' -o -name '*.js' -o -name '*.ts' \
197
+ -o -name '*.rs' -o -name '*.java' -o -name '*.rb' -o -name '*.sh' \
198
+ -o -name '*.c' -o -name '*.cpp' -o -name '*.kt' \) 2>/dev/null \
199
+ | head -1 | grep -q .; then
200
+ if is_deferred_name "$name"; then
201
+ add_concept "$name" systems Module "$name" 0 false "allow-defer match"
202
+ else
203
+ add_concept "$name" systems Module "$name" 0 true ""
204
+ fi
205
+ fi
206
+ done < <(find "$scope_dir" -mindepth 1 -maxdepth 1 -type d | sort)
207
+ }
208
+
209
+ # --- Drive discovery in priority order ---
210
+ if [[ -n "$MANIFEST" ]]; then
211
+ [[ -f "$MANIFEST" ]] || { echo "ERROR: --manifest not found: $MANIFEST" >&2; exit 1; }
212
+ plan_from_manifest
213
+ elif plan_from_graph; then
214
+ :
215
+ else
216
+ plan_from_heuristic
217
+ fi
218
+
219
+ if [[ ${#E_ID[@]} -eq 0 ]]; then
220
+ echo "ERROR: no expected concepts derived (graph + manifest unavailable, heuristic empty)" >&2
221
+ exit 2
222
+ fi
223
+
224
+ # Required-first, then deferred; stable within group (topological-ish: high fan-in
225
+ # subsystems first so forward cross-links resolve during generation).
226
+ emit_plan() {
227
+ local n=${#E_ID[@]} i
228
+ # Build sortable index lines: <req_rank>\t<fanin_desc>\t<idx>
229
+ local order=()
230
+ for ((i=0; i<n; i++)); do
231
+ local rank=1; [[ "${E_REQ[$i]}" == "true" ]] && rank=0
232
+ order+=("$(printf '%d\t%010d\t%d' "$rank" "$(( 9999999999 - ${E_FANIN[$i]:-0} ))" "$i")")
233
+ done
234
+ local sorted; sorted="$(printf '%s\n' "${order[@]}" | sort)"
235
+
236
+ local req=0 def=0
237
+ for ((i=0; i<n; i++)); do
238
+ [[ "${E_REQ[$i]}" == "true" ]] && req=$((req+1)) || def=$((def+1))
239
+ done
240
+
241
+ {
242
+ printf '{\n'
243
+ printf ' "version": 1,\n'
244
+ printf ' "repo": "%s",\n' "$(json_escape "$REPO")"
245
+ printf ' "scope": "%s",\n' "$(json_escape "$SCOPE")"
246
+ printf ' "source": "%s",\n' "$SOURCE"
247
+ printf ' "degraded": %s,\n' "$DEGRADED"
248
+ printf ' "min_fan_in": %d,\n' "$MIN_FAN_IN"
249
+ # generated_order
250
+ printf ' "generated_order": ['
251
+ local first=1
252
+ while IFS=$'\t' read -r _ _ idx; do
253
+ [[ -z "$idx" ]] && continue
254
+ [[ $first -eq 1 ]] && first=0 || printf ','
255
+ printf '"%s"' "$(json_escape "${E_ID[$idx]}")"
256
+ done <<< "$sorted"
257
+ printf '],\n'
258
+ # expected[]
259
+ printf ' "expected": [\n'
260
+ first=1
261
+ while IFS=$'\t' read -r _ _ idx; do
262
+ [[ -z "$idx" ]] && continue
263
+ [[ $first -eq 1 ]] && first=0 || printf ',\n'
264
+ local reason_json="null"
265
+ [[ -n "${E_REASON[$idx]}" ]] && reason_json="\"$(json_escape "${E_REASON[$idx]}")\""
266
+ printf ' {"concept_id":"%s","type":"%s","resource":"%s","fan_in":%d,"required":%s,"reason_if_deferred":%s}' \
267
+ "$(json_escape "${E_ID[$idx]}")" \
268
+ "$(json_escape "${E_TYPE[$idx]}")" \
269
+ "$(json_escape "${E_RES[$idx]}")" \
270
+ "${E_FANIN[$idx]:-0}" \
271
+ "${E_REQ[$idx]}" \
272
+ "$reason_json"
273
+ done <<< "$sorted"
274
+ printf '\n ],\n'
275
+ printf ' "counts": {"expected_total": %d, "required": %d, "deferred": %d}\n' "$n" "$req" "$def"
276
+ printf '}\n'
277
+ }
278
+ }
279
+
280
+ PLAN_JSON="$(emit_plan)"
281
+
282
+ # Validate our own output parses before writing.
283
+ if command -v jq >/dev/null 2>&1; then
284
+ echo "$PLAN_JSON" | jq -e '.expected' >/dev/null 2>&1 \
285
+ || { echo "ERROR: generated plan is not valid JSON (internal error)" >&2; exit 1; }
286
+ fi
287
+
288
+ if [[ -n "$OUT" ]]; then
289
+ mkdir -p "$(dirname "$OUT")"
290
+ printf '%s' "$PLAN_JSON" > "$OUT"
291
+ echo "concept plan → $OUT (source=$SOURCE, $(echo "$PLAN_JSON" | jq -r '.counts.required') required, $(echo "$PLAN_JSON" | jq -r '.counts.deferred') deferred)" >&2
292
+ [[ $JSON -eq 1 ]] && printf '%s' "$PLAN_JSON"
293
+ else
294
+ printf '%s' "$PLAN_JSON"
295
+ fi
296
+ exit 0
@@ -25,6 +25,8 @@ BUNDLE=""
25
25
  ARCH_OUT=""
26
26
  WEB_OUT=""
27
27
  CMAP_INTO=()
28
+ COVERAGE_REPORT=""
29
+ VALIDATED_AT=""
28
30
 
29
31
  usage() {
30
32
  cat <<'EOF'
@@ -40,6 +42,10 @@ Flags:
40
42
  --web FILE Write a self-contained, offline HTML viewer (single file:
41
43
  all pages inlined, built-in markdown renderer, sidebar +
42
44
  search). Double-click to open — no server, no internet.
45
+ --coverage-report FILE okf-coverage-check.sh JSON; its mapped/required/pct and
46
+ validity are rendered into the architecture.md banner.
47
+ --validated-at STR Timestamp string shown in the banner (caller supplies it;
48
+ this tool has no clock dependency).
43
49
  --help Show this help.
44
50
 
45
51
  Requires jq (already a Draft prereq) for --web. Exit 0 ok, 1 error, 2 bundle not found.
@@ -51,6 +57,8 @@ while [[ $# -gt 0 ]]; do
51
57
  --arch-out) ARCH_OUT="$2"; shift 2;;
52
58
  --concept-map-into) CMAP_INTO+=("$2"); shift 2;;
53
59
  --web) WEB_OUT="$2"; shift 2;;
60
+ --coverage-report) COVERAGE_REPORT="$2"; shift 2;;
61
+ --validated-at) VALIDATED_AT="$2"; shift 2;;
54
62
  --help|-h) usage; exit 0;;
55
63
  -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
56
64
  *)
@@ -92,6 +100,27 @@ strip_frontmatter() {
92
100
  ' "$1"
93
101
  }
94
102
 
103
+ # Coverage-honesty banner, sourced from okf-coverage-check.sh's JSON report.
104
+ # Silent when no report is supplied (keeps the view backward-compatible).
105
+ emit_coverage_banner() {
106
+ [[ -n "$COVERAGE_REPORT" && -f "$COVERAGE_REPORT" ]] || return 0
107
+ command -v jq >/dev/null 2>&1 || return 0
108
+ local valid mapped required pct
109
+ valid="$(jq -r '.valid // empty' "$COVERAGE_REPORT" 2>/dev/null || true)"
110
+ mapped="$(jq -r '.mapped // empty' "$COVERAGE_REPORT" 2>/dev/null || true)"
111
+ required="$(jq -r '.required // empty' "$COVERAGE_REPORT" 2>/dev/null || true)"
112
+ pct="$(jq -r '.coverage_pct // empty' "$COVERAGE_REPORT" 2>/dev/null || true)"
113
+ [[ -n "$mapped" && -n "$required" ]] || return 0
114
+ if [[ "$valid" == "true" ]]; then
115
+ echo "> **Coverage:** ${mapped}/${required} required components (${pct}%)."
116
+ else
117
+ echo "> **⚠ INCOMPLETE — do not use for RCA:** only ${mapped}/${required} required"
118
+ echo "> components (${pct}%) are documented. Re-run \`/draft:init\` to fill the gaps."
119
+ fi
120
+ [[ -n "$VALIDATED_AT" ]] && echo "> Validated: ${VALIDATED_AT} — $([[ "$valid" == "true" ]] && echo PASS || echo FAIL)."
121
+ echo ""
122
+ }
123
+
95
124
  # --- 1. Render architecture.md ---
96
125
  render_architecture() {
97
126
  local out="$1"
@@ -109,6 +138,7 @@ render_architecture() {
109
138
  echo "> The bundle is the source of truth; this is the single-document linear"
110
139
  echo "> view for onboarding. Regenerate with \`okf-render-views.sh\`."
111
140
  echo ""
141
+ emit_coverage_banner
112
142
  echo "## Contents"
113
143
  echo ""
114
144
  # TOC from page titles.
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env bash
2
+ # okf-validate-all.sh — single promotion gate for the OKF emitter.
3
+ #
4
+ # Runs the three validation layers in order and aggregates one verdict. The init
5
+ # / refresh pipeline conditions the atomic `mv draft.tmp/ draft/` on this exit
6
+ # code, so a bundle that is structurally broken, full of stubs, or missing
7
+ # required components is never promoted.
8
+ #
9
+ # Layer 1 okf-validate.sh structure (frontmatter, types, links, index)
10
+ # Layer 2 okf-validate-quality.sh per-type anti-stub / depth / mermaid lint
11
+ # Layer 3 okf-coverage-check.sh every required plan entry has a real page
12
+ #
13
+ # Usage:
14
+ # okf-validate-all.sh <BUNDLE_DIR> [--plan FILE] [--path-index FILE]
15
+ # [--strict] [--report FILE] [--json]
16
+ #
17
+ # Layer 3 runs only when --plan is given (init/refresh always pass it). CI runs
18
+ # without a plan get layers 1–2.
19
+ #
20
+ # Exit codes: 0 all pass, 1 any layer failed, 2 bundle not found.
21
+ set -euo pipefail
22
+
23
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ # shellcheck source=scripts/tools/_lib.sh
25
+ source "$SCRIPT_DIR/_lib.sh"
26
+
27
+ BUNDLE=""
28
+ PLAN=""
29
+ PATH_INDEX=""
30
+ STRICT=0
31
+ REPORT=""
32
+ JSON=0
33
+
34
+ usage() {
35
+ cat <<'EOF'
36
+ okf-validate-all.sh — run all OKF validation layers as one promotion gate.
37
+
38
+ Usage:
39
+ okf-validate-all.sh <BUNDLE_DIR> [--plan FILE] [--path-index FILE]
40
+ [--strict] [--report FILE] [--json]
41
+
42
+ Flags:
43
+ --plan FILE concept-plan.json — enables Layer 3 (coverage).
44
+ --path-index FILE path-to-concept.json — enables index checks in Layer 1.
45
+ --strict Pass --strict to the quality layer (warnings → failures).
46
+ --report FILE Write an aggregated JSON report here.
47
+ --json Emit the aggregated JSON report to stdout.
48
+ --help Show this help.
49
+
50
+ Exit: 0 all pass, 1 any layer failed, 2 bundle not found.
51
+ EOF
52
+ }
53
+
54
+ while [[ $# -gt 0 ]]; do
55
+ case "$1" in
56
+ --plan) PLAN="$2"; shift 2;;
57
+ --path-index) PATH_INDEX="$2"; shift 2;;
58
+ --strict) STRICT=1; shift;;
59
+ --report) REPORT="$2"; shift 2;;
60
+ --json) JSON=1; shift;;
61
+ --help|-h) usage; exit 0;;
62
+ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
63
+ *) if [[ -z "$BUNDLE" ]]; then BUNDLE="$1"; else echo "Unexpected arg: $1" >&2; exit 1; fi; shift;;
64
+ esac
65
+ done
66
+
67
+ [[ -n "$BUNDLE" ]] || { usage >&2; exit 1; }
68
+ [[ -d "$BUNDLE" ]] || { echo "ERROR: bundle directory not found: $BUNDLE" >&2; exit 2; }
69
+
70
+ L1=skip; L2=skip; L3=skip
71
+ OVERALL=0
72
+
73
+ run_layer() {
74
+ local name="$1"; shift
75
+ set +e
76
+ "$@" >/dev/null 2>&1
77
+ local rc=$?
78
+ set -e
79
+ return $rc
80
+ }
81
+
82
+ # Layer 1: structure.
83
+ v1_args=("$BUNDLE")
84
+ [[ -n "$PATH_INDEX" ]] && v1_args+=(--path-index "$PATH_INDEX")
85
+ if run_layer structure "$SCRIPT_DIR/okf-validate.sh" "${v1_args[@]}"; then L1=pass; else L1=fail; OVERALL=1; fi
86
+
87
+ # Layer 2: quality.
88
+ q_args=("$BUNDLE"); [[ $STRICT -eq 1 ]] && q_args+=(--strict)
89
+ if run_layer quality "$SCRIPT_DIR/okf-validate-quality.sh" "${q_args[@]}"; then L2=pass; else L2=fail; OVERALL=1; fi
90
+
91
+ # Layer 3: coverage (only if a plan is supplied).
92
+ if [[ -n "$PLAN" ]]; then
93
+ if run_layer coverage "$SCRIPT_DIR/okf-coverage-check.sh" --plan "$PLAN" --bundle "$BUNDLE"; then
94
+ L3=pass
95
+ else
96
+ L3=fail; OVERALL=1
97
+ fi
98
+ fi
99
+
100
+ REPORT_JSON="$(printf '{"valid":%s,"bundle":"%s","layers":{"structure":"%s","quality":"%s","coverage":"%s"}}\n' \
101
+ "$([[ $OVERALL -eq 0 ]] && echo true || echo false)" "$(json_escape "$BUNDLE")" "$L1" "$L2" "$L3")"
102
+
103
+ [[ -n "$REPORT" ]] && { mkdir -p "$(dirname "$REPORT")"; printf '%s' "$REPORT_JSON" > "$REPORT"; }
104
+
105
+ if [[ $JSON -eq 1 ]]; then
106
+ printf '%s' "$REPORT_JSON"
107
+ else
108
+ echo "OKF validation — structure:$L1 quality:$L2 coverage:$L3 → $([[ $OVERALL -eq 0 ]] && echo PASS || echo FAIL)"
109
+ if [[ $OVERALL -ne 0 ]]; then
110
+ echo " Re-run the failing layer directly for detail:" >&2
111
+ [[ "$L1" == fail ]] && echo " okf-validate.sh $BUNDLE ${PATH_INDEX:+--path-index $PATH_INDEX}" >&2
112
+ [[ "$L2" == fail ]] && echo " okf-validate-quality.sh $BUNDLE" >&2
113
+ [[ "$L3" == fail ]] && echo " okf-coverage-check.sh --plan $PLAN --bundle $BUNDLE" >&2
114
+ fi
115
+ fi
116
+
117
+ exit $OVERALL