@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,272 @@
1
+ #!/usr/bin/env bash
2
+ # okf-validate-quality.sh — deterministic per-page SEMANTIC checks for an OKF bundle.
3
+ #
4
+ # okf-validate.sh proves a page is structurally sound (frontmatter, type, links).
5
+ # This proves a page is actually WRITTEN — not a stub, redirect, or template
6
+ # leftover — so that "every module has a page" (okf-coverage-check.sh) cannot be
7
+ # satisfied with placeholder content. Thresholds are per concept TYPE: a
8
+ # Subsystem must carry a diagram and real depth; an ADR or Dependency legitimately
9
+ # does not, so applying one global bar would false-fail them.
10
+ #
11
+ # Pages in scope: any *.md whose frontmatter declares a frozen `type:`. Section
12
+ # index.md pages, log.md, and the generated coverage page are excluded.
13
+ #
14
+ # Checks (per type): required H2 sections, min body lines, >=1 mermaid block
15
+ # (diagram types only), min x-grounded-paths, anti-stub patterns, unreplaced
16
+ # template tokens, duplicate "What it is" paragraphs, and a syntax-only mermaid
17
+ # lint (no Node, no headless browser).
18
+ #
19
+ # Usage:
20
+ # okf-validate-quality.sh <BUNDLE_DIR> [--strict] [--json]
21
+ #
22
+ # Exit codes: 0 pass, 1 fail, 2 bundle not found.
23
+ set -euo pipefail
24
+
25
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ # shellcheck source=scripts/tools/_lib.sh
27
+ source "$SCRIPT_DIR/_lib.sh"
28
+
29
+ BUNDLE=""
30
+ STRICT=0
31
+ JSON=0
32
+
33
+ usage() {
34
+ cat <<'EOF'
35
+ okf-validate-quality.sh — per-page semantic / anti-stub checks for an OKF bundle.
36
+
37
+ Usage:
38
+ okf-validate-quality.sh <BUNDLE_DIR> [--strict] [--json]
39
+
40
+ Flags:
41
+ --strict Treat warnings (e.g. duplicate paragraphs) as failures.
42
+ --json Emit a JSON summary instead of human diagnostics.
43
+ --help Show this help.
44
+
45
+ Exit: 0 pass, 1 fail, 2 bundle not found.
46
+ EOF
47
+ }
48
+
49
+ while [[ $# -gt 0 ]]; do
50
+ case "$1" in
51
+ --strict) STRICT=1; shift;;
52
+ --json) JSON=1; shift;;
53
+ --help|-h) usage; exit 0;;
54
+ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
55
+ *) if [[ -z "$BUNDLE" ]]; then BUNDLE="$1"; else echo "Unexpected arg: $1" >&2; exit 1; fi; shift;;
56
+ esac
57
+ done
58
+
59
+ [[ -n "$BUNDLE" ]] || { usage >&2; exit 1; }
60
+ [[ -d "$BUNDLE" ]] || { echo "ERROR: bundle directory not found: $BUNDLE" >&2; exit 2; }
61
+ BUNDLE="${BUNDLE%/}"
62
+
63
+ FAILURES=() # page\tcheck\tdetail
64
+ WARNINGS=()
65
+ CHECKED=0
66
+
67
+ fail() { FAILURES+=("$1"$'\t'"$2"$'\t'"$3"); }
68
+ warn() { WARNINGS+=("$1"$'\t'"$2"$'\t'"$3"); }
69
+
70
+ # Body (everything after the frontmatter block).
71
+ body_of() {
72
+ awk 'NR==1&&/^---$/{fm=1;next} fm&&/^---$/{fm=0;next} !fm{print}' "$1"
73
+ }
74
+
75
+ # Count body lines after the frontmatter close (Q-LEN = "lines after frontmatter").
76
+ body_lines() { body_of "$1" | wc -l | tr -d ' '; }
77
+
78
+ # x-grounded-paths array length (entries inside [ ... ]).
79
+ grounded_count() {
80
+ local arr
81
+ arr="$(grep -m1 -E '^x-grounded-paths:' "$1" 2>/dev/null || true)"
82
+ [[ -z "$arr" ]] && { echo 0; return; }
83
+ arr="${arr#*[}"; arr="${arr%]*}"
84
+ arr="$(printf '%s' "$arr" | tr -d ' ')"
85
+ [[ -z "$arr" ]] && { echo 0; return; }
86
+ awk -F',' '{print NF}' <<< "$arr"
87
+ }
88
+
89
+ has_section() { grep -qE "^##[[:space:]]+$1([[:space:]]|$)" "$2"; }
90
+
91
+ mermaid_block_count() { grep -cE '^[[:space:]]*```mermaid' "$1" || true; }
92
+
93
+ # Syntax-only mermaid lint: catch the breakers that silently fail previewers
94
+ # without spawning Node. Returns 0 clean, 1 with a reason on stdout.
95
+ mermaid_lint() {
96
+ local file="$1"
97
+ # Extract content of every ```mermaid ... ``` block.
98
+ local blocks; blocks="$(awk '
99
+ /^[[:space:]]*```mermaid/ {inb=1; next}
100
+ inb && /^[[:space:]]*```/ {inb=0; next}
101
+ inb {print}
102
+ ' "$file")"
103
+ [[ -z "$blocks" ]] && return 0
104
+ # Unicode arrows.
105
+ if printf '%s' "$blocks" | grep -qE '→|⟶|⇒|←'; then
106
+ echo "unicode arrow in mermaid (use --> not →)"; return 1
107
+ fi
108
+ # '&' node chaining (common breaker).
109
+ if printf '%s' "$blocks" | grep -qE '[A-Za-z0-9_]\s*&\s*[A-Za-z0-9_]'; then
110
+ echo "'&' node chaining in mermaid"; return 1
111
+ fi
112
+ # Reserved bareword node ids.
113
+ if printf '%s' "$blocks" | grep -qE '(^|[[:space:]])(end|class|click|graph|subgraph)[[:space:]]*(--|==|-\.)'; then
114
+ echo "reserved word used as node id in mermaid"; return 1
115
+ fi
116
+ # Unbalanced subgraph/end.
117
+ local sg en
118
+ sg="$(printf '%s' "$blocks" | grep -cE '^[[:space:]]*subgraph' || true)"
119
+ en="$(printf '%s' "$blocks" | grep -cE '^[[:space:]]*end[[:space:]]*$' || true)"
120
+ if [[ "$sg" -gt 0 && "$sg" != "$en" ]]; then
121
+ echo "unbalanced subgraph/end ($sg subgraph, $en end)"; return 1
122
+ fi
123
+ return 0
124
+ }
125
+
126
+ # Normalized hash of the first paragraph under "## What it is" (dup detection).
127
+ whatitis_hash() {
128
+ local p
129
+ p="$(awk '
130
+ /^##[[:space:]]+What it is/ {grab=1; next}
131
+ grab && /^##[[:space:]]/ {exit}
132
+ grab && /^[[:space:]]*$/ { if (seen) exit; else next }
133
+ grab {seen=1; print}
134
+ ' "$1")"
135
+ [[ -z "$p" ]] && return 0
136
+ printf '%s' "$p" | tr '[:upper:]' '[:lower:]' | tr -s ' \t' ' ' | cksum | awk '{print $1}'
137
+ }
138
+
139
+ ANTI_STUB='see architecture\.md|deferred to ref-docs|\bTBD\b|TODO:[[:space:]]*document|stub page|placeholder page'
140
+ TOKEN_RE='\{[A-Z_]+\}'
141
+
142
+ # Per-type policy. echoes: sections|min_lines|need_mermaid|min_grounded
143
+ type_policy() {
144
+ case "$1" in
145
+ Subsystem|Module|Feature|Entrypoint)
146
+ echo "What it is;How it works;Used by;Blast radius;See also|25|1|2";;
147
+ API|DataModel)
148
+ echo "What it is;How it works;See also|18|0|1";;
149
+ Dependency)
150
+ echo "What it is;Used by|10|0|0";;
151
+ ADR|Runbook)
152
+ echo "|8|0|0";;
153
+ *)
154
+ echo "|8|0|0";;
155
+ esac
156
+ }
157
+
158
+ # Seen "What it is" hashes (hash<TAB>rel), as a temp file for bash 3.2 portability.
159
+ WHATIS_SEEN="$(mktemp)"
160
+ trap 'rm -f "$WHATIS_SEEN"' EXIT
161
+
162
+ while IFS= read -r -d '' page; do
163
+ rel="${page#"$BUNDLE/"}"
164
+ base="$(basename "$rel")"
165
+ # Exclusions: section/root index, log, generated coverage page.
166
+ [[ "$base" == "index.md" ]] && continue
167
+ [[ "$base" == "log.md" ]] && continue
168
+ [[ "$base" == "coverage.md" ]] && continue
169
+ grep -q '<!-- okf:coverage-generated -->' "$page" 2>/dev/null && continue
170
+
171
+ type_val="$(get_yaml_field "$page" type)"
172
+ [[ -z "$type_val" ]] && continue # not a concept page
173
+ CHECKED=$((CHECKED + 1))
174
+
175
+ IFS='|' read -r sections min_lines need_mermaid min_grounded <<< "$(type_policy "$type_val")"
176
+
177
+ # Q-SEC: required sections.
178
+ if [[ -n "$sections" ]]; then
179
+ IFS=';' read -ra secs <<< "$sections"
180
+ for s in "${secs[@]}"; do
181
+ has_section "$s" "$page" || fail "$rel" "Q-SEC" "missing required section '## $s'"
182
+ done
183
+ fi
184
+
185
+ # Q-LEN: body length.
186
+ bl="$(body_lines "$page")"
187
+ [[ "$bl" -ge "$min_lines" ]] || fail "$rel" "Q-LEN" "body $bl lines < $min_lines for type $type_val"
188
+
189
+ # Q-DIAG: mermaid presence for diagram types.
190
+ if [[ "$need_mermaid" == "1" ]]; then
191
+ mc="$(mermaid_block_count "$page")"
192
+ [[ "$mc" -ge 1 ]] || fail "$rel" "Q-DIAG" "no mermaid block (required for $type_val)"
193
+ fi
194
+
195
+ # Q-MERMAID: syntax lint on any present blocks.
196
+ if [[ "$(mermaid_block_count "$page")" -ge 1 ]]; then
197
+ if reason="$(mermaid_lint "$page")"; [[ -n "$reason" ]]; then
198
+ fail "$rel" "Q-MERMAID" "$reason"
199
+ fi
200
+ fi
201
+
202
+ # Q-GROUND: grounded paths count.
203
+ if [[ "$min_grounded" -gt 0 ]]; then
204
+ gc="$(grounded_count "$page")"
205
+ [[ "$gc" -ge "$min_grounded" ]] || fail "$rel" "Q-GROUND" "x-grounded-paths $gc < $min_grounded"
206
+ fi
207
+
208
+ # Q-STUB: anti-stub patterns in body.
209
+ if body_of "$page" | grep -qiE "$ANTI_STUB"; then
210
+ fail "$rel" "Q-STUB" "matches anti-stub pattern"
211
+ fi
212
+
213
+ # Q-TEMPLATE: unreplaced {TOKEN} placeholders.
214
+ if body_of "$page" | grep -qE "$TOKEN_RE"; then
215
+ fail "$rel" "Q-TEMPLATE" "unreplaced template token {PLACEHOLDER}"
216
+ fi
217
+
218
+ # Q-DUP: duplicate "What it is" opening paragraph (warning unless --strict).
219
+ h="$(whatitis_hash "$page")"
220
+ if [[ -n "$h" ]]; then
221
+ prev="$(awk -F'\t' -v h="$h" '$1==h{print $2; exit}' "$WHATIS_SEEN")"
222
+ if [[ -n "$prev" ]]; then
223
+ warn "$rel" "Q-DUP" "duplicate 'What it is' paragraph (matches $prev)"
224
+ else
225
+ printf '%s\t%s\n' "$h" "$rel" >> "$WHATIS_SEEN"
226
+ fi
227
+ fi
228
+ done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
229
+
230
+ # In --strict, warnings become failures.
231
+ if [[ $STRICT -eq 1 && ${#WARNINGS[@]} -gt 0 ]]; then
232
+ for w in "${WARNINGS[@]}"; do FAILURES+=("$w"); done
233
+ WARNINGS=()
234
+ fi
235
+
236
+ if [[ $JSON -eq 1 ]]; then
237
+ valid=true; [[ ${#FAILURES[@]} -eq 0 ]] || valid=false
238
+ printf '{"valid":%s,"bundle":"%s","concepts_checked":%d,"failures":[' \
239
+ "$valid" "$(json_escape "$BUNDLE")" "$CHECKED"
240
+ for i in "${!FAILURES[@]}"; do
241
+ [[ $i -gt 0 ]] && printf ','
242
+ IFS=$'\t' read -r p c d <<< "${FAILURES[$i]}"
243
+ printf '{"page":"%s","check":"%s","detail":"%s"}' \
244
+ "$(json_escape "$p")" "$(json_escape "$c")" "$(json_escape "$d")"
245
+ done
246
+ printf '],"warnings":['
247
+ for i in "${!WARNINGS[@]}"; do
248
+ [[ $i -gt 0 ]] && printf ','
249
+ IFS=$'\t' read -r p c d <<< "${WARNINGS[$i]}"
250
+ printf '{"page":"%s","check":"%s","detail":"%s"}' \
251
+ "$(json_escape "$p")" "$(json_escape "$c")" "$(json_escape "$d")"
252
+ done
253
+ printf ']}\n'
254
+ else
255
+ if [[ ${#FAILURES[@]} -gt 0 ]]; then
256
+ echo "OKF quality FAIL: $BUNDLE ($CHECKED concepts checked)" >&2
257
+ for f in "${FAILURES[@]}"; do
258
+ IFS=$'\t' read -r p c d <<< "$f"
259
+ echo " - [$c] $p: $d" >&2
260
+ done
261
+ else
262
+ echo "OKF quality pass: $BUNDLE ($CHECKED concepts checked)"
263
+ fi
264
+ for w in "${WARNINGS[@]:-}"; do
265
+ [[ -z "$w" ]] && continue
266
+ IFS=$'\t' read -r p c d <<< "$w"
267
+ echo " warn [$c] $p: $d" >&2
268
+ done
269
+ fi
270
+
271
+ [[ ${#FAILURES[@]} -eq 0 ]] || exit 1
272
+ exit 0
@@ -34,6 +34,8 @@ OKF_TYPES="Subsystem Module Feature Entrypoint API DataModel Dependency ADR Runb
34
34
  BUNDLE=""
35
35
  PATH_INDEX=""
36
36
  JSON=0
37
+ REVERSE=0
38
+ STRUCTURE_ONLY=0
37
39
 
38
40
  usage() {
39
41
  cat <<'EOF'
@@ -44,7 +46,12 @@ Usage:
44
46
 
45
47
  Flags:
46
48
  --path-index FILE Validate a path→concept index (JSON): every concept page it
47
- names must exist in the bundle.
49
+ names must exist in the bundle (forward check).
50
+ --reverse Also require every concept page (excluding section index.md)
51
+ to appear in at least one value array of the path-index, so
52
+ no page is orphaned from its source grounding. Needs --path-index.
53
+ --structure-only Run only the original structural checks (disables --reverse);
54
+ for backward-compatible Layer-1 callers.
48
55
  --json Emit a JSON summary instead of human diagnostics.
49
56
  --help Show this help.
50
57
 
@@ -55,6 +62,8 @@ EOF
55
62
  while [[ $# -gt 0 ]]; do
56
63
  case "$1" in
57
64
  --path-index) PATH_INDEX="$2"; shift 2;;
65
+ --reverse) REVERSE=1; shift;;
66
+ --structure-only) STRUCTURE_ONLY=1; shift;;
58
67
  --json) JSON=1; shift;;
59
68
  --help|-h) usage; exit 0;;
60
69
  -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
@@ -176,6 +185,31 @@ if [[ -n "$PATH_INDEX" ]]; then
176
185
  fi
177
186
  fi
178
187
 
188
+ # --- 6. Reverse index: every concept page is grounded by the index (optional) ---
189
+ # Forward proves the index doesn't name ghosts; reverse proves no page is an
190
+ # orphan with no source mapping (a symptom of hand-written / off-plan pages).
191
+ if [[ $REVERSE -eq 1 && $STRUCTURE_ONLY -eq 0 && -n "$PATH_INDEX" && -f "$PATH_INDEX" ]]; then
192
+ # All pages the index maps to (its array values).
193
+ INDEXED_FILE="$(mktemp)"
194
+ grep -oE '\[[^]]*\]' "$PATH_INDEX" 2>/dev/null \
195
+ | grep -oE '"[^"]+\.md"' | tr -d '"' | sort -u > "$INDEXED_FILE" || true
196
+ while IFS= read -r -d '' page; do
197
+ rel="${page#"$BUNDLE/"}"
198
+ base="$(basename "$rel")"
199
+ # Section/root indexes and generated meta pages are not concept pages.
200
+ [[ "$base" == "index.md" ]] && continue
201
+ [[ "$base" == "log.md" ]] && continue
202
+ [[ "$base" == "coverage.md" ]] && continue
203
+ grep -q '<!-- okf:coverage-generated -->' "$page" 2>/dev/null && continue
204
+ # Only pages that declare a type are concepts.
205
+ [[ -z "$(get_yaml_field "$page" "type")" ]] && continue
206
+ if ! grep -qxF "$rel" "$INDEXED_FILE"; then
207
+ add_error "concept page not grounded by path-index (orphan): $rel"
208
+ fi
209
+ done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
210
+ rm -f "$INDEXED_FILE"
211
+ fi
212
+
179
213
  # --- Report ---
180
214
  if [[ $JSON -eq 1 ]]; then
181
215
  valid=true
@@ -216,6 +216,16 @@ DRAFT_INIT_MODE="${DRAFT_INIT_MODE:-auto}"
216
216
 
217
217
  The tier-gated default rests on **maintainability/readability** (one navigable concept per file, cleaner PRs, a generated `architecture.md` preserved for linear onboarding) — not on the A/B benchmark, which was accuracy-parity (`docs/audit/okf-benchmark.md`). `monolith` is **retained, not retired**: it is the tier-1/2 default, the A/B baseline, and the fallback. If `DRAFT_INIT_MODE` is unset, do not commit to a mode until **Step 1.4.5** has computed the tier.
218
218
 
219
+ **OKF Completeness Verification (blocking — tier 3+ `okf` mode).** Completeness is enforced by tooling, not honor system, so the wiki is generated completely for every module/sub-module/component on every run. Before promoting `draft.tmp/` → `draft/`, ALL must hold (see `references/okf-emitter.md` for the pipeline):
220
+
221
+ 1. `okf-plan-concepts.sh` ran and the expected/required/deferred counts were logged **before** any page was written (the concept boundary is a tool output, not an in-context guess).
222
+ 2. Every `required` entry in `concept-plan.json` has a non-stub page.
223
+ 3. `okf-validate-all.sh … --plan … --strict` exits 0 (structure + per-type quality + coverage all pass).
224
+ 4. `systems/coverage.md` was generated by tooling (verify the `<!-- okf:coverage-generated -->` marker) — no required package with `fan_in ≥ floor` is **MISSING**.
225
+ 5. On any failure: **do not** atomic-rename; surface `.state/validation-report.json`.
226
+
227
+ > **Red flag:** writing concept pages without first running `okf-plan-concepts.sh`, or finishing generation while any `required` plan entry is unwritten, is a **completeness failure** — not a stylistic one.
228
+
219
229
  ### Route Explicit Modes Before Initialization
220
230
 
221
231
  If the user explicitly invoked a specialist mode, route directly:
@@ -25,7 +25,10 @@ esac
25
25
  Everything else in `/draft:init` (5-phase analysis, graph snapshot, `.state/`
26
26
  hashing, scope detection, atomic staging) is **reused unchanged**. This mode adds
27
27
  a decomposition + serialization stage. It introduces **no new LLM analysis
28
- engine** and exactly **one** new deterministic helper, `okf-validate.sh`.
28
+ engine**; its deterministic helpers are `okf-plan-concepts.sh` (expected-concept
29
+ set), `okf-validate.sh` (structure), `okf-validate-quality.sh` (per-type
30
+ anti-stub), `okf-coverage-check.sh` (completeness), and `okf-validate-all.sh`
31
+ (the single promotion gate that runs all three), plus `okf-render-views.sh`.
29
32
 
30
33
  ## Target layout
31
34
 
@@ -118,25 +121,64 @@ Derive concepts from the graph, not by hand:
118
121
 
119
122
  ```
120
123
  1. Survey → existing /draft:init 5-phase + graph snapshot (graph-snapshot.sh)
121
- 2. Plan → derive the concept list (above) from graph clusters +
122
- entrypoints + features. Topo-sort by dependency so pages that
123
- others link to (overview, core subsystems) generate FIRST —
124
- forward cross-links resolve.
125
- 3. Generate → per concept, pull grounding from the graph and write the page:
124
+ 2. Plan → DETERMINISTIC. okf-plan-concepts.sh derives the expected-concept
125
+ set from the graph (every package with fan_in ≥ floor → required
126
+ Subsystem/Module; entrypoints required; below-floor / allow-defer
127
+ deferred with a reason) and writes draft.tmp/.state/concept-plan.json.
128
+ okf-plan-concepts.sh --repo . [--scope PATH] \
129
+ [--manifest FILE] [--min-fan-in 2] [--allow-defer GLOB]... \
130
+ --out draft.tmp/.state/concept-plan.json
131
+ This replaces the old in-context concept enumeration — the boundary
132
+ of the work is now a tool output, not an LLM judgment, so modules
133
+ cannot be silently dropped. LOG the counts (expected/required/
134
+ deferred) BEFORE writing any page. `generated_order` is topo-ish
135
+ (required + high-fan-in first) so forward cross-links resolve.
136
+ 3. Generate → iterate concept-plan.generated_order; write ONE page per REQUIRED
137
+ entry, grounding each from the graph:
126
138
  x-callers ← graph-callers.sh --symbol <c>
127
139
  x-grounded-paths ← graph-impact.sh --symbol <c> (blast radius)
128
140
  x-hotspot-score ← hotspot-rank.sh
129
141
  overview diagrams ← mermaid-from-graph.sh
130
142
  Record each source path → page in .state/path-to-concept.json.
143
+ Loop post-condition: every required concept_id has an output file.
144
+ ⚠ Writing pages via shell heredoc without reading x-grounded-paths
145
+ sources, or finishing while any required entry is unwritten, is a
146
+ completeness failure — not a stylistic one.
131
147
  4. Render views → ai-context.md (synopsis + Concept Map), architecture.md
132
- (concatenated view), wiki/log.md (see M4).
133
- 5. Validate → okf-validate.sh draft/wiki \
134
- --path-index draft/.state/path-to-concept.json
135
- FAIL the build (do not atomic-rename) on any dangle, missing
136
- field, bad type, or path-index gap.
137
- 6. Emit → mv draft.tmp/ draft/ ; update .state/.
148
+ (concatenated view + coverage banner), wiki/log.md (see M4).
149
+ 5. Validate → the promotion gate. Run all layers via the orchestrator:
150
+ 5a. okf-validate-all.sh draft.tmp/wiki \
151
+ --repo . \
152
+ --plan draft.tmp/.state/concept-plan.json \
153
+ --path-index draft.tmp/.state/path-to-concept.json \
154
+ --strict --report draft.tmp/.state/validation-report.json
155
+ It runs, in order: okf-validate.sh (structure + reverse index),
156
+ okf-validate-quality.sh (per-type anti-stub / depth / mermaid lint),
157
+ okf-coverage-check.sh (every required plan entry → real page).
158
+ ANY layer failing ⇒ exit non-zero ⇒ DO NOT atomic-rename.
159
+ coverage.md (systems/coverage.md) is regenerated by the coverage
160
+ layer; it is tool-owned (marker <!-- okf:coverage-generated -->) —
161
+ never hand-author it except deferral reasons in the manifest.
162
+ 6. Emit → mv draft.tmp/ draft/ ONLY IF step 5 exit 0 ; update .state/.
163
+ On failure keep draft.tmp/ and surface validation-report.json.
138
164
  ```
139
165
 
166
+ ### Validation report schema (`.state/validation-report.json`)
167
+
168
+ ```json
169
+ { "valid": false, "bundle": "draft.tmp/wiki",
170
+ "layers": { "structure": "pass", "quality": "pass", "coverage": "fail" } }
171
+ ```
172
+
173
+ ### Component manifest (optional — `--manifest FILE`)
174
+
175
+ When the graph engine is unavailable (or a repo wants an authoritative list), pass
176
+ a plain-text manifest: one component name per line, `#` comments and blanks ignored.
177
+ Every listed component becomes a REQUIRED concept; `--allow-defer GLOB` still moves
178
+ matches to deferred. Without a manifest the plan comes from the graph, and only if
179
+ both are unavailable does it fall back to a heuristic top-level-dir scan (which it
180
+ marks `degraded: true`).
181
+
140
182
  Page bodies are LLM-narrated for readability **but** the graph-derived
141
183
  frontmatter and the `Blast radius`/`Used by` sections are deterministic. To keep
142
184
  incremental carry-forward byte-identical (open decision 2), cache the narrated
@@ -190,17 +232,22 @@ section `index.md` tables are the injection slots for the routing tables.
190
232
  `/draft:init refresh` under `okf` mode:
191
233
 
192
234
  ```
193
- 1. Diff hashes.json vs working tree → changed source paths
194
- 2. path-to-concept.json → affected concept pages
195
- 3. Regenerate ONLY affected concepts; carry the rest verbatim (cached narration)
196
- 4. Re-render ai-context.md / architecture.md / log.md (cheap; always regenerated)
197
- 5. Re-validate: okf-validate.sh on the bundle + path-index (cross-links touching
198
- changed concepts must still resolve)
199
- 6. Append log.md; update hashes.json + path-to-concept.json
235
+ 1. Re-derive the plan: okf-plan-concepts.sh (modules added since last run become
236
+ REQUIRED a new package can't slip through a refresh either)
237
+ 2. Diff hashes.json vs working tree → changed source paths
238
+ 3. path-to-concept.json → affected concept pages
239
+ 4. Regenerate ONLY affected concepts; carry the rest verbatim (cached narration)
240
+ 5. Re-render ai-context.md / architecture.md / log.md (cheap; always regenerated)
241
+ 6. Re-validate (full gate): okf-validate-all.sh on the bundle with --plan and
242
+ --path-index. Refresh re-runs structure + quality + coverage — a changed
243
+ concept must still clear the quality bar, and a newly-required module must
244
+ still be present.
245
+ 7. Append log.md; update hashes.json + path-to-concept.json
200
246
  ```
201
247
 
202
248
  A 1-file change regenerates only the concept(s) that file grounds. Unchanged
203
- concepts are byte-identical across runs.
249
+ concepts are byte-identical across runs. The full gate still runs, so refresh
250
+ cannot promote a bundle that a newly-added module left incomplete.
204
251
 
205
252
  ## Backward compatibility (§9)
206
253