@drafthq/draft 3.4.0 → 3.5.1

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/scripts/lib.sh CHANGED
@@ -197,6 +197,11 @@ TOOLS=(
197
197
  # OKF taxonomy emitter (DRAFT_INIT_MODE=okf)
198
198
  "okf-validate.sh"
199
199
  "okf-render-views.sh"
200
+ # OKF completeness enforcement (deterministic plan + coverage/quality gates)
201
+ "okf-plan-concepts.sh"
202
+ "okf-validate-quality.sh"
203
+ "okf-coverage-check.sh"
204
+ "okf-validate-all.sh"
200
205
  )
201
206
 
202
207
  # ─────────────────────────────────────────────────────────
@@ -203,6 +203,73 @@ memory_project_for_repo() {
203
203
  | head -1
204
204
  }
205
205
 
206
+ # Total physical RAM in MB (portable). Echoes a positive integer, or nothing.
207
+ _total_ram_mb() {
208
+ if [[ -r /proc/meminfo ]]; then
209
+ awk '/^MemTotal:/{printf "%d", $2/1024; exit}' /proc/meminfo
210
+ elif command -v sysctl >/dev/null 2>&1; then # macOS / BSD
211
+ local bytes; bytes="$(sysctl -n hw.memsize 2>/dev/null || true)"
212
+ [[ -n "$bytes" ]] && printf '%d' "$(( bytes / 1024 / 1024 ))"
213
+ fi
214
+ }
215
+
216
+ # Cgroup memory args for a transient scope. MemoryHigh throttles (reclaim/swap →
217
+ # slower, never thrashes the host); MemoryMax is the hard ceiling a few % above.
218
+ # Pure (no I/O) so it is unit-testable. Usage: _mem_bound_args <total_mb> <pct>
219
+ _mem_bound_args() {
220
+ local total="$1" pct="$2"
221
+ printf 'MemoryHigh=%dM MemoryMax=%dM' \
222
+ "$(( total * pct / 100 ))" "$(( total * (pct + 5) / 100 ))"
223
+ }
224
+
225
+ # Whether this host can confine a process to a memory-bounded cgroup v2 scope.
226
+ # Probes once (starts a throwaway scope) and caches the verdict for the process.
227
+ _DRAFT_CGROUP_OK=""
228
+ _can_cgroup_bound() {
229
+ if [[ -z "$_DRAFT_CGROUP_OK" ]]; then
230
+ if command -v systemd-run >/dev/null 2>&1 \
231
+ && [[ -e /sys/fs/cgroup/cgroup.controllers ]] \
232
+ && systemd-run --user --scope -q -p MemoryMax=64M -- true >/dev/null 2>&1; then
233
+ _DRAFT_CGROUP_OK=yes
234
+ else
235
+ _DRAFT_CGROUP_OK=no
236
+ fi
237
+ fi
238
+ [[ "$_DRAFT_CGROUP_OK" == yes ]]
239
+ }
240
+
241
+ # Index a repository under a memory bound. The codebase-memory-mcp engine
242
+ # self-budgets ~50% of *physical* RAM and is not cgroup-aware, so a first index
243
+ # of a huge repo can exhaust the host (the original 30 GB hang). On Linux we
244
+ # confine it to a transient cgroup scope sized to DRAFT_INDEX_MEM_PCT (default
245
+ # 25) of total RAM; CBM_WORKERS caps the engine's parallel working set so the
246
+ # throttle has less transient pressure to absorb. Where cgroup v2 + systemd-run
247
+ # are unavailable (e.g. macOS) the worker cap is the only bound. Never falls back
248
+ # from a started scope to an unbounded run — a bounded OOM fails the index
249
+ # cleanly (host stays alive) rather than re-triggering the hang.
250
+ # Echoes the engine's JSON result on stdout (same contract as memory_cli).
251
+ memory_index_bounded() {
252
+ local repo_abs="$1"
253
+ local json="{\"repo_path\":\"$repo_abs\"}"
254
+ export CBM_WORKERS="${CBM_WORKERS:-4}"
255
+ local total pct
256
+ total="$(_total_ram_mb)"
257
+ pct="${DRAFT_INDEX_MEM_PCT:-25}"
258
+ if [[ "${total:-0}" -gt 0 ]] && _can_cgroup_bound; then
259
+ local high_arg max_arg
260
+ read -r high_arg max_arg <<< "$(_mem_bound_args "$total" "$pct")"
261
+ if [[ -n "${DRAFT_MEMORY_DEBUG:-}" ]]; then
262
+ systemd-run --user --scope -q -p "$high_arg" -p "$max_arg" \
263
+ -- "$MEMORY_BIN" cli index_repository "$json"
264
+ else
265
+ systemd-run --user --scope -q -p "$high_arg" -p "$max_arg" \
266
+ -- "$MEMORY_BIN" cli index_repository "$json" 2>/dev/null
267
+ fi
268
+ else
269
+ memory_cli index_repository "$json"
270
+ fi
271
+ }
272
+
206
273
  # Ensure a repository is indexed in the engine; echo its project name.
207
274
  # Indexes on demand when absent. Returns 1 if the engine is unavailable.
208
275
  memory_ensure_index() {
@@ -212,7 +279,7 @@ memory_ensure_index() {
212
279
  local proj
213
280
  proj="$(memory_project_for_repo "$repo_abs" 2>/dev/null || true)"
214
281
  if [[ -z "$proj" ]]; then
215
- proj="$(memory_cli index_repository "{\"repo_path\":\"$repo_abs\"}" 2>/dev/null \
282
+ proj="$(memory_index_bounded "$repo_abs" \
216
283
  | jq -r '.project // empty' 2>/dev/null || true)"
217
284
  fi
218
285
  [[ -n "$proj" ]] || return 1
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env bash
2
+ # okf-coverage-check.sh — prove the OKF bundle documents EVERY required component.
3
+ #
4
+ # This is the gate that fixes "the wiki is not generated completely for all
5
+ # modules". It compares the deterministic expected set (concept-plan.json from
6
+ # okf-plan-concepts.sh) against the pages that actually exist in the bundle. A
7
+ # required concept with no page — or a present-but-empty page — fails the build,
8
+ # so a gappy bundle cannot be promoted (draft.tmp/ → draft/). Deferred entries
9
+ # must be reasoned in the generated coverage.md, never silently absent.
10
+ #
11
+ # Checks:
12
+ # C-PLAN every required plan entry has a bundle page (concept_id exists)
13
+ # C-STUB each satisfying page has real body content (>= --min-stub-lines)
14
+ # C-DEFER every deferred entry is recorded with a reason (always true: from plan)
15
+ #
16
+ # Side effect: regenerates <BUNDLE>/systems/coverage.md (tool-owned) unless
17
+ # --no-coverage-page is given.
18
+ #
19
+ # Usage:
20
+ # okf-coverage-check.sh --plan FILE --bundle DIR [--min-stub-lines N]
21
+ # [--no-coverage-page] [--json] [--report FILE]
22
+ #
23
+ # Exit codes: 0 complete, 1 incomplete (missing/stub required), 2 plan/bundle missing.
24
+ set -euo pipefail
25
+
26
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+ # shellcheck source=scripts/tools/_lib.sh
28
+ source "$SCRIPT_DIR/_lib.sh"
29
+
30
+ PLAN=""
31
+ BUNDLE=""
32
+ MIN_STUB_LINES=10
33
+ WRITE_PAGE=1
34
+ JSON=0
35
+ REPORT=""
36
+
37
+ usage() {
38
+ cat <<'EOF'
39
+ okf-coverage-check.sh — verify every required concept in the plan has a real page.
40
+
41
+ Usage:
42
+ okf-coverage-check.sh --plan FILE --bundle DIR [--min-stub-lines N]
43
+ [--no-coverage-page] [--json] [--report FILE]
44
+
45
+ Flags:
46
+ --plan FILE concept-plan.json from okf-plan-concepts.sh (required).
47
+ --bundle DIR The wiki/ bundle directory (required).
48
+ --min-stub-lines N Min non-blank body lines for a page to count as real (default 10).
49
+ --no-coverage-page Do not (re)write <BUNDLE>/systems/coverage.md.
50
+ --json Emit a JSON summary.
51
+ --report FILE Also write the JSON summary to FILE.
52
+ --help Show this help.
53
+
54
+ Exit: 0 complete, 1 incomplete, 2 plan/bundle not found.
55
+ EOF
56
+ }
57
+
58
+ while [[ $# -gt 0 ]]; do
59
+ case "$1" in
60
+ --plan) PLAN="$2"; shift 2;;
61
+ --bundle) BUNDLE="$2"; shift 2;;
62
+ --min-stub-lines) MIN_STUB_LINES="$2"; shift 2;;
63
+ --no-coverage-page) WRITE_PAGE=0; shift;;
64
+ --json) JSON=1; shift;;
65
+ --report) REPORT="$2"; shift 2;;
66
+ --help|-h) usage; exit 0;;
67
+ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
68
+ *) echo "Unexpected arg: $1" >&2; usage >&2; exit 1;;
69
+ esac
70
+ done
71
+
72
+ [[ -n "$PLAN" ]] || { usage >&2; exit 1; }
73
+ [[ -n "$BUNDLE" ]] || { usage >&2; exit 1; }
74
+ [[ -f "$PLAN" ]] || { echo "ERROR: plan not found: $PLAN" >&2; exit 2; }
75
+ [[ -d "$BUNDLE" ]] || { echo "ERROR: bundle directory not found: $BUNDLE" >&2; exit 2; }
76
+ command -v jq >/dev/null 2>&1 || { echo "ERROR: jq required" >&2; exit 2; }
77
+ BUNDLE="${BUNDLE%/}"
78
+
79
+ jq -e '.expected' "$PLAN" >/dev/null 2>&1 || { echo "ERROR: plan has no .expected array: $PLAN" >&2; exit 2; }
80
+
81
+ # Non-blank body line count for a page.
82
+ body_lines() {
83
+ awk 'NR==1&&/^---$/{fm=1;next} fm&&/^---$/{fm=0;next} !fm{print}' "$1" | grep -cE '.' || true
84
+ }
85
+
86
+ MISSING=() # concept_id (required, no page)
87
+ STUB=() # concept_id\tlines (required, page too thin)
88
+ FULL=() # concept_id
89
+ DEFERRED=() # concept_id\treason
90
+ EXPECTED_TOTAL=0; REQUIRED=0
91
+
92
+ # Iterate expected entries.
93
+ while IFS=$'\t' read -r cid required reason ftype fanin; do
94
+ [[ -z "$cid" ]] && continue
95
+ EXPECTED_TOTAL=$((EXPECTED_TOTAL + 1))
96
+ if [[ "$required" == "true" ]]; then
97
+ REQUIRED=$((REQUIRED + 1))
98
+ if [[ -f "$BUNDLE/$cid" ]]; then
99
+ bl="$(body_lines "$BUNDLE/$cid")"
100
+ if [[ "$bl" -ge "$MIN_STUB_LINES" ]]; then
101
+ FULL+=("$cid")
102
+ else
103
+ STUB+=("$cid"$'\t'"$bl")
104
+ fi
105
+ else
106
+ MISSING+=("$cid")
107
+ fi
108
+ else
109
+ DEFERRED+=("$cid"$'\t'"${reason:-unspecified}")
110
+ fi
111
+ done < <(jq -r '.expected[] | [.concept_id, (.required|tostring), (.reason_if_deferred // "-"), (.type // "Module"), (.fan_in // 0 | tostring)] | @tsv' "$PLAN")
112
+
113
+ MAPPED=$(( ${#FULL[@]} ))
114
+ PASS=1
115
+ { [[ ${#MISSING[@]} -gt 0 ]] || [[ ${#STUB[@]} -gt 0 ]]; } && PASS=0
116
+ PCT=100
117
+ [[ $REQUIRED -gt 0 ]] && PCT=$(( MAPPED * 100 / REQUIRED ))
118
+
119
+ # --- Generate coverage.md (tool-owned) ---
120
+ write_coverage_page() {
121
+ local out="$BUNDLE/systems/coverage.md"
122
+ mkdir -p "$BUNDLE/systems"
123
+ local tmp; tmp="$(mktemp)"
124
+ {
125
+ echo "<!-- okf:coverage-generated -->"
126
+ echo "# Component Coverage"
127
+ echo ""
128
+ echo "> Generated by \`okf-coverage-check.sh\` — do not hand-edit (except deferral reasons in the manifest)."
129
+ echo "> Required components documented: ${MAPPED}/${REQUIRED} (${PCT}%)."
130
+ echo ""
131
+ echo "| Component | Wiki page | Status | Fan-in |"
132
+ echo "|-----------|-----------|--------|--------|"
133
+ local cid status fanin
134
+ while IFS=$'\t' read -r cid required reason ftype fanin; do
135
+ [[ -z "$cid" ]] && continue
136
+ if [[ "$required" == "true" ]]; then
137
+ if [[ -f "$BUNDLE/$cid" ]]; then
138
+ bl="$(body_lines "$BUNDLE/$cid")"
139
+ if [[ "$bl" -ge "$MIN_STUB_LINES" ]]; then
140
+ echo "| \`${cid%.md}\` | [page](${cid#systems/}) | Full | ${fanin} |"
141
+ else
142
+ echo "| \`${cid%.md}\` | ${cid} | **STUB (${bl} lines)** | ${fanin} |"
143
+ fi
144
+ else
145
+ echo "| \`${cid%.md}\` | — | **MISSING** | ${fanin} |"
146
+ fi
147
+ else
148
+ echo "| \`${cid%.md}\` | — | Deferred (${reason:-unspecified}) | ${fanin} |"
149
+ fi
150
+ done < <(jq -r '.expected[] | [.concept_id, (.required|tostring), (.reason_if_deferred // "-"), (.type // "Module"), (.fan_in // 0 | tostring)] | @tsv' "$PLAN")
151
+ } > "$tmp"
152
+ mv "$tmp" "$out"
153
+ }
154
+ [[ $WRITE_PAGE -eq 1 ]] && write_coverage_page
155
+
156
+ # --- Report ---
157
+ emit_json() {
158
+ printf '{"valid":%s,"bundle":"%s","plan":"%s","required":%d,"mapped":%d,"coverage_pct":%d,"missing":[' \
159
+ "$([[ $PASS -eq 1 ]] && echo true || echo false)" \
160
+ "$(json_escape "$BUNDLE")" "$(json_escape "$PLAN")" "$REQUIRED" "$MAPPED" "$PCT"
161
+ for i in "${!MISSING[@]}"; do [[ $i -gt 0 ]] && printf ','; printf '"%s"' "$(json_escape "${MISSING[$i]}")"; done
162
+ printf '],"stub":['
163
+ local first=1
164
+ for s in "${STUB[@]:-}"; do
165
+ [[ -z "$s" ]] && continue
166
+ IFS=$'\t' read -r cid bl <<< "$s"
167
+ [[ $first -eq 1 ]] && first=0 || printf ','
168
+ printf '{"concept_id":"%s","lines":%d}' "$(json_escape "$cid")" "$bl"
169
+ done
170
+ printf '],"deferred":%d}\n' "${#DEFERRED[@]}"
171
+ }
172
+
173
+ if [[ -n "$REPORT" ]]; then mkdir -p "$(dirname "$REPORT")"; emit_json > "$REPORT"; fi
174
+
175
+ if [[ $JSON -eq 1 ]]; then
176
+ emit_json
177
+ else
178
+ if [[ $PASS -eq 1 ]]; then
179
+ echo "OKF coverage complete: ${MAPPED}/${REQUIRED} required components (${PCT}%), ${#DEFERRED[@]} deferred"
180
+ else
181
+ echo "OKF coverage INCOMPLETE: ${MAPPED}/${REQUIRED} required (${PCT}%)" >&2
182
+ for m in "${MISSING[@]:-}"; do [[ -n "$m" ]] && echo " - MISSING: $m" >&2; done
183
+ for s in "${STUB[@]:-}"; do
184
+ [[ -z "$s" ]] && continue
185
+ IFS=$'\t' read -r cid bl <<< "$s"
186
+ echo " - STUB: $cid ($bl body lines < $MIN_STUB_LINES)" >&2
187
+ done
188
+ fi
189
+ fi
190
+
191
+ [[ $PASS -eq 1 ]] || exit 1
192
+ exit 0
@@ -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.