@drafthq/draft 3.4.0 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/core/templates/okf/concept.md +9 -0
- package/integrations/agents/AGENTS.md +86 -20
- package/integrations/copilot/.github/copilot-instructions.md +86 -20
- package/package.json +1 -1
- package/scripts/lib.sh +5 -0
- package/scripts/tools/okf-coverage-check.sh +192 -0
- package/scripts/tools/okf-plan-concepts.sh +296 -0
- package/scripts/tools/okf-render-views.sh +30 -0
- package/scripts/tools/okf-validate-all.sh +117 -0
- package/scripts/tools/okf-validate-quality.sh +272 -0
- package/scripts/tools/okf-validate.sh +35 -1
- package/skills/init/SKILL.md +10 -0
- package/skills/init/references/okf-emitter.md +67 -20
|
@@ -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
|