@drafthq/draft 3.5.0 → 3.5.2
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/ai-context-index.md +1 -1
- package/core/templates/okf/index.md +5 -5
- package/core/templates/okf/section-index.md +8 -8
- package/integrations/agents/AGENTS.md +36 -24
- package/integrations/copilot/.github/copilot-instructions.md +36 -24
- package/integrations/copilot/.github/copilot-instructions.md.7iDz8X +91 -0
- package/integrations/copilot/.github/copilot-instructions.md.DoBdtd +91 -0
- package/integrations/copilot/.github/copilot-instructions.md.McGoBW +122 -0
- package/integrations/copilot/.github/copilot-instructions.md.VsPyLB +91 -0
- package/integrations/copilot/.github/copilot-instructions.md.XAVr7D +91 -0
- package/integrations/copilot/.github/copilot-instructions.md.YoFVFa +91 -0
- package/integrations/copilot/.github/copilot-instructions.md.a9DeW0 +91 -0
- package/integrations/copilot/.github/copilot-instructions.md.oxQs3B +91 -0
- package/integrations/copilot/.github/copilot-instructions.md.ww33Ly +91 -0
- package/package.json +1 -1
- package/scripts/tools/_lib.sh +68 -1
- package/scripts/tools/okf-plan-concepts.sh +14 -3
- package/scripts/tools/okf-render-views.sh +58 -2
- package/scripts/tools/okf-validate-quality.sh +40 -10
- package/scripts/tools/okf-validate.sh +57 -2
- package/skills/init/SKILL.md +1 -1
- package/skills/init/references/okf-emitter.md +21 -9
package/scripts/tools/_lib.sh
CHANGED
|
@@ -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="$(
|
|
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
|
|
@@ -38,6 +38,7 @@ MANIFEST=""
|
|
|
38
38
|
MIN_FAN_IN=2
|
|
39
39
|
OUT=""
|
|
40
40
|
JSON=0
|
|
41
|
+
DEFER_BELOW_FLOOR=0
|
|
41
42
|
ALLOW_DEFER=()
|
|
42
43
|
|
|
43
44
|
usage() {
|
|
@@ -54,8 +55,12 @@ Flags:
|
|
|
54
55
|
--manifest FILE Component list (one component per line; '#' comments; blanks
|
|
55
56
|
ignored). When present it is authoritative — every entry is
|
|
56
57
|
required and the graph is not consulted.
|
|
57
|
-
--min-fan-in N
|
|
58
|
-
|
|
58
|
+
--min-fan-in N Fan-in threshold that types a package as a Subsystem (>=N)
|
|
59
|
+
vs a Module (<N) and orders it first (default: 2). By default
|
|
60
|
+
EVERY graph package is required regardless of fan-in — the
|
|
61
|
+
floor no longer exempts anything.
|
|
62
|
+
--defer-below-floor Restore the old behavior: packages with fan_in < --min-fan-in
|
|
63
|
+
are deferred (not required) instead of required-as-Module.
|
|
59
64
|
--allow-defer GLOB Defer (don't require) components whose name matches GLOB.
|
|
60
65
|
Repeatable. Deferred entries still appear in the plan.
|
|
61
66
|
--out FILE Write the plan JSON here (default: stdout).
|
|
@@ -72,6 +77,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
72
77
|
--scope) SCOPE="$2"; shift 2;;
|
|
73
78
|
--manifest) MANIFEST="$2"; shift 2;;
|
|
74
79
|
--min-fan-in) MIN_FAN_IN="$2"; shift 2;;
|
|
80
|
+
--defer-below-floor) DEFER_BELOW_FLOOR=1; shift;;
|
|
75
81
|
--allow-defer) ALLOW_DEFER+=("$2"); shift 2;;
|
|
76
82
|
--out) OUT="$2"; shift 2;;
|
|
77
83
|
--json) JSON=1; shift;;
|
|
@@ -150,8 +156,13 @@ plan_from_graph() {
|
|
|
150
156
|
required=false; reason="allow-defer match"; type=Module
|
|
151
157
|
elif (( fan_in >= MIN_FAN_IN )); then
|
|
152
158
|
required=true; reason=""; type=Subsystem
|
|
153
|
-
|
|
159
|
+
elif [[ $DEFER_BELOW_FLOOR -eq 1 ]]; then
|
|
160
|
+
# Opt-in legacy behavior: low-fan-in packages are exempted.
|
|
154
161
|
required=false; reason="fan_in $fan_in < floor $MIN_FAN_IN"; type=Module
|
|
162
|
+
else
|
|
163
|
+
# Default: every package the graph knows about is documented. Fan-in
|
|
164
|
+
# below the floor only demotes Subsystem→Module; it never exempts.
|
|
165
|
+
required=true; reason=""; type=Module
|
|
155
166
|
fi
|
|
156
167
|
add_concept "$name" systems "$type" "$name" "$fan_in" "$required" "$reason"
|
|
157
168
|
done < <(echo "$arch" | jq -r '.packages[]? | [.name, (.fan_in // 0)] | @tsv')
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
# 2. Concept Map — a routing table injected between the
|
|
11
11
|
# <!-- CONCEPT-MAP:START --> / <!-- CONCEPT-MAP:END --> markers in
|
|
12
12
|
# wiki/index.md (and optionally another index-root file).
|
|
13
|
+
# 3. Section indexes — (--section-indexes) each <section>/index.md concept
|
|
14
|
+
# table rebuilt from the pages that actually exist in that directory, so its
|
|
15
|
+
# links can never dangle (no more hand-authored, link-rotting indexes).
|
|
13
16
|
#
|
|
14
17
|
# Usage:
|
|
15
18
|
# okf-render-views.sh <BUNDLE_DIR> --arch-out <FILE> [--concept-map-into <FILE>]
|
|
@@ -25,6 +28,7 @@ BUNDLE=""
|
|
|
25
28
|
ARCH_OUT=""
|
|
26
29
|
WEB_OUT=""
|
|
27
30
|
CMAP_INTO=()
|
|
31
|
+
SECTION_INDEXES=0
|
|
28
32
|
COVERAGE_REPORT=""
|
|
29
33
|
VALIDATED_AT=""
|
|
30
34
|
|
|
@@ -39,6 +43,10 @@ Flags:
|
|
|
39
43
|
--arch-out FILE Write the rendered linear architecture.md here.
|
|
40
44
|
--concept-map-into FILE Inject the Concept Map between the CONCEPT-MAP markers
|
|
41
45
|
in FILE (repeatable: e.g. wiki/index.md and ai-context.md).
|
|
46
|
+
--section-indexes Regenerate each section's <section>/index.md concept
|
|
47
|
+
table (between its CONCEPT-MAP markers) from the pages
|
|
48
|
+
that actually exist in that directory. Eliminates
|
|
49
|
+
hand-authored, link-rotting section indexes.
|
|
42
50
|
--web FILE Write a self-contained, offline HTML viewer (single file:
|
|
43
51
|
all pages inlined, built-in markdown renderer, sidebar +
|
|
44
52
|
search). Double-click to open — no server, no internet.
|
|
@@ -56,6 +64,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
56
64
|
case "$1" in
|
|
57
65
|
--arch-out) ARCH_OUT="$2"; shift 2;;
|
|
58
66
|
--concept-map-into) CMAP_INTO+=("$2"); shift 2;;
|
|
67
|
+
--section-indexes) SECTION_INDEXES=1; shift;;
|
|
59
68
|
--web) WEB_OUT="$2"; shift 2;;
|
|
60
69
|
--coverage-report) COVERAGE_REPORT="$2"; shift 2;;
|
|
61
70
|
--validated-at) VALIDATED_AT="$2"; shift 2;;
|
|
@@ -134,7 +143,7 @@ render_architecture() {
|
|
|
134
143
|
echo ""
|
|
135
144
|
echo "# Architecture (Rendered View)"
|
|
136
145
|
echo ""
|
|
137
|
-
echo "> **Generated** from the \`wiki/\`
|
|
146
|
+
echo "> **Generated** from the \`wiki/\` bundle — do not edit by hand."
|
|
138
147
|
echo "> The bundle is the source of truth; this is the single-document linear"
|
|
139
148
|
echo "> view for onboarding. Regenerate with \`okf-render-views.sh\`."
|
|
140
149
|
echo ""
|
|
@@ -192,6 +201,50 @@ build_concept_map() {
|
|
|
192
201
|
done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
|
|
193
202
|
}
|
|
194
203
|
|
|
204
|
+
# First non-empty line of a page's `description` frontmatter (handles folded `>`).
|
|
205
|
+
page_desc() {
|
|
206
|
+
awk '
|
|
207
|
+
NR==1&&/^---$/{fm=1;next} fm&&/^---$/{exit}
|
|
208
|
+
fm && /^description:/ { collect=1; sub(/^description:[[:space:]]*>?[[:space:]]*/,""); if($0!=""){print; exit} next }
|
|
209
|
+
fm && collect { sub(/^[[:space:]]+/,""); if($0!=""){print; exit} }
|
|
210
|
+
' "$1"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# Build the per-section concept table (stdout) for a single section directory.
|
|
214
|
+
# Links are bundle-section-relative (just the filename) so they resolve from the
|
|
215
|
+
# section's own index.md. Only pages that actually exist are listed — so the
|
|
216
|
+
# table can never point at a missing file.
|
|
217
|
+
build_section_map() {
|
|
218
|
+
local dir="$1" f rel base type title desc
|
|
219
|
+
echo "| Concept | Type | Routing description |"
|
|
220
|
+
echo "|---------|------|---------------------|"
|
|
221
|
+
while IFS= read -r f; do
|
|
222
|
+
base="$(basename "$f")"
|
|
223
|
+
[[ "$base" == "index.md" ]] && continue
|
|
224
|
+
[[ "$base" == "coverage.md" ]] && continue
|
|
225
|
+
grep -q '<!-- okf:coverage-generated -->' "$f" 2>/dev/null && continue
|
|
226
|
+
type="$(get_yaml_field "$f" type)"
|
|
227
|
+
[[ -n "$type" ]] || continue
|
|
228
|
+
title="$(get_yaml_field "$f" title)"; [[ -n "$title" ]] || title="$base"
|
|
229
|
+
desc="$(page_desc "$f")"
|
|
230
|
+
echo "| [${title}](${base}) | ${type} | ${desc} |"
|
|
231
|
+
done < <(find "$dir" -maxdepth 1 -type f -name '*.md' | sort)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Regenerate every <section>/index.md concept table from real pages.
|
|
235
|
+
render_section_indexes() {
|
|
236
|
+
local sec dir idx
|
|
237
|
+
for sec in "${SECTIONS[@]}"; do
|
|
238
|
+
dir="$BUNDLE/$sec"
|
|
239
|
+
idx="$dir/index.md"
|
|
240
|
+
[[ -d "$dir" && -f "$idx" ]] || continue
|
|
241
|
+
local map_tmp; map_tmp="$(mktemp)"
|
|
242
|
+
build_section_map "$dir" >"$map_tmp"
|
|
243
|
+
inject_concept_map "$idx" "$map_tmp"
|
|
244
|
+
rm -f "$map_tmp"
|
|
245
|
+
done
|
|
246
|
+
}
|
|
247
|
+
|
|
195
248
|
# Inject the Concept Map between markers in a target file (path may be relative
|
|
196
249
|
# to BUNDLE: links in the map are bundle-relative, so the target should resolve
|
|
197
250
|
# them — wiki/index.md works directly; an index root above wiki/ should prefix).
|
|
@@ -397,7 +450,10 @@ if [[ ${#CMAP_INTO[@]} -gt 0 ]]; then
|
|
|
397
450
|
rm -f "$MAP_TMP"
|
|
398
451
|
fi
|
|
399
452
|
|
|
453
|
+
[[ $SECTION_INDEXES -eq 1 ]] && render_section_indexes
|
|
454
|
+
|
|
400
455
|
[[ -n "$WEB_OUT" ]] && render_web "$WEB_OUT"
|
|
401
456
|
|
|
402
|
-
[[ -n "$ARCH_OUT" || -n "$WEB_OUT" || ${#CMAP_INTO[@]} -gt 0
|
|
457
|
+
[[ -n "$ARCH_OUT" || -n "$WEB_OUT" || ${#CMAP_INTO[@]} -gt 0 || $SECTION_INDEXES -eq 1 ]] \
|
|
458
|
+
|| { echo "ERROR: nothing to do (pass --arch-out, --web, --section-indexes, and/or --concept-map-into)" >&2; exit 1; }
|
|
403
459
|
exit 0
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
# Pages in scope: any *.md whose frontmatter declares a frozen `type:`. Section
|
|
12
12
|
# index.md pages, log.md, and the generated coverage page are excluded.
|
|
13
13
|
#
|
|
14
|
-
# Checks (per type): required H2 sections
|
|
14
|
+
# Checks (per type): required H2 sections AND real content beneath each (so a
|
|
15
|
+
# present-but-empty heading fails as "shallow"), min body lines, >=1 mermaid block
|
|
15
16
|
# (diagram types only), min x-grounded-paths, anti-stub patterns, unreplaced
|
|
16
17
|
# template tokens, duplicate "What it is" paragraphs, and a syntax-only mermaid
|
|
17
18
|
# lint (no Node, no headless browser).
|
|
@@ -88,6 +89,19 @@ grounded_count() {
|
|
|
88
89
|
|
|
89
90
|
has_section() { grep -qE "^##[[:space:]]+$1([[:space:]]|$)" "$2"; }
|
|
90
91
|
|
|
92
|
+
# Count non-blank content lines under a given H2 section (heading itself
|
|
93
|
+
# excluded), stopping at the next H2. Used to fail a section that is present as a
|
|
94
|
+
# heading but carries no real content — the classic "shallow" page.
|
|
95
|
+
section_content_lines() {
|
|
96
|
+
local sec="$1" file="$2"
|
|
97
|
+
awk -v sec="$sec" '
|
|
98
|
+
$0 ~ "^##[[:space:]]+" sec "([[:space:]]|$)" { grab=1; next }
|
|
99
|
+
grab && /^##[[:space:]]/ { exit }
|
|
100
|
+
grab && /[^[:space:]]/ { n++ }
|
|
101
|
+
END { print n+0 }
|
|
102
|
+
' "$file"
|
|
103
|
+
}
|
|
104
|
+
|
|
91
105
|
mermaid_block_count() { grep -cE '^[[:space:]]*```mermaid' "$1" || true; }
|
|
92
106
|
|
|
93
107
|
# Syntax-only mermaid lint: catch the breakers that silently fail previewers
|
|
@@ -139,19 +153,22 @@ whatitis_hash() {
|
|
|
139
153
|
ANTI_STUB='see architecture\.md|deferred to ref-docs|\bTBD\b|TODO:[[:space:]]*document|stub page|placeholder page'
|
|
140
154
|
TOKEN_RE='\{[A-Z_]+\}'
|
|
141
155
|
|
|
142
|
-
# Per-type policy. echoes: sections|min_lines|need_mermaid|min_grounded
|
|
156
|
+
# Per-type policy. echoes: sections|min_lines|need_mermaid|min_grounded|min_section_lines
|
|
157
|
+
# min_section_lines: minimum non-blank content lines required UNDER each named
|
|
158
|
+
# section (0 disables the per-section depth check). This is what turns a
|
|
159
|
+
# present-but-empty heading — the hallmark of a shallow page — into a failure.
|
|
143
160
|
type_policy() {
|
|
144
161
|
case "$1" in
|
|
145
162
|
Subsystem|Module|Feature|Entrypoint)
|
|
146
|
-
echo "What it is;How it works;Used by;Blast radius;See also|
|
|
163
|
+
echo "What it is;How it works;Used by;Blast radius;See also|35|1|2|2";;
|
|
147
164
|
API|DataModel)
|
|
148
|
-
echo "What it is;How it works;See also|
|
|
165
|
+
echo "What it is;How it works;See also|22|0|1|2";;
|
|
149
166
|
Dependency)
|
|
150
|
-
echo "What it is;Used by|
|
|
167
|
+
echo "What it is;Used by|12|0|0|1";;
|
|
151
168
|
ADR|Runbook)
|
|
152
|
-
echo "|8|0|0";;
|
|
169
|
+
echo "|8|0|0|0";;
|
|
153
170
|
*)
|
|
154
|
-
echo "|8|0|0";;
|
|
171
|
+
echo "|8|0|0|0";;
|
|
155
172
|
esac
|
|
156
173
|
}
|
|
157
174
|
|
|
@@ -172,13 +189,26 @@ while IFS= read -r -d '' page; do
|
|
|
172
189
|
[[ -z "$type_val" ]] && continue # not a concept page
|
|
173
190
|
CHECKED=$((CHECKED + 1))
|
|
174
191
|
|
|
175
|
-
IFS='|' read -r sections min_lines need_mermaid min_grounded <<< "$(type_policy "$type_val")"
|
|
192
|
+
IFS='|' read -r sections min_lines need_mermaid min_grounded min_section_lines <<< "$(type_policy "$type_val")"
|
|
176
193
|
|
|
177
|
-
# Q-SEC: required sections.
|
|
194
|
+
# Q-SEC: required sections must be present. Q-SECLEN: the NARRATIVE sections
|
|
195
|
+
# ("What it is" / "How it works") must carry real prose beneath the heading —
|
|
196
|
+
# a heading with nothing under it is the classic shallow stub. List sections
|
|
197
|
+
# ("Used by", "See also", "Blast radius") are legitimately terse, so the
|
|
198
|
+
# depth bar only applies to the narrative ones.
|
|
178
199
|
if [[ -n "$sections" ]]; then
|
|
179
200
|
IFS=';' read -ra secs <<< "$sections"
|
|
180
201
|
for s in "${secs[@]}"; do
|
|
181
|
-
has_section "$s" "$page"
|
|
202
|
+
if ! has_section "$s" "$page"; then
|
|
203
|
+
fail "$rel" "Q-SEC" "missing required section '## $s'"
|
|
204
|
+
elif [[ "${min_section_lines:-0}" -gt 0 ]]; then
|
|
205
|
+
case "$s" in
|
|
206
|
+
"What it is"|"How it works")
|
|
207
|
+
scl="$(section_content_lines "$s" "$page")"
|
|
208
|
+
[[ "$scl" -ge "$min_section_lines" ]] \
|
|
209
|
+
|| fail "$rel" "Q-SECLEN" "section '## $s' has $scl content lines < $min_section_lines (shallow)";;
|
|
210
|
+
esac
|
|
211
|
+
fi
|
|
182
212
|
done
|
|
183
213
|
fi
|
|
184
214
|
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
# 2. Every concept page (any *.md whose frontmatter declares `type:`) carries
|
|
12
12
|
# all required OKF frontmatter keys: type, title, description, resource.
|
|
13
13
|
# 3. Every declared `type` is in the frozen code-repo vocabulary (§4 of HLD).
|
|
14
|
+
# 3a. Every non-meta *.md is a real concept: an empty or frontmatter-less page
|
|
15
|
+
# (a blank placeholder) fails — it would otherwise be invisible to the
|
|
16
|
+
# quality + coverage layers, which both key on `type`.
|
|
17
|
+
# 3b. No page carries an unreplaced {ALL_CAPS} template token (catches leftover
|
|
18
|
+
# placeholders in hand-seeded index pages, which the quality layer skips).
|
|
14
19
|
# 4. Every relative markdown cross-link ( ](path.md) ) resolves to a file that
|
|
15
20
|
# exists inside the bundle. External (http/https/mailto) and pure-anchor
|
|
16
21
|
# (#frag) links are ignored.
|
|
@@ -105,14 +110,46 @@ if [[ ! -f "$BUNDLE/index.md" ]]; then
|
|
|
105
110
|
add_error "missing bundle root: $BUNDLE/index.md"
|
|
106
111
|
fi
|
|
107
112
|
|
|
113
|
+
# A page is "meta" (not a concept) if it is a section/root index, the change log,
|
|
114
|
+
# or the tool-generated coverage page. Everything else MUST be a real concept —
|
|
115
|
+
# an empty or frontmatter-less .md placeholder is a completeness failure, not an
|
|
116
|
+
# invisible non-concept.
|
|
117
|
+
is_meta_page() {
|
|
118
|
+
local base="$1" page="$2"
|
|
119
|
+
case "$base" in
|
|
120
|
+
index.md|log.md|coverage.md) return 0;;
|
|
121
|
+
esac
|
|
122
|
+
grep -q '<!-- okf:coverage-generated -->' "$page" 2>/dev/null && return 0
|
|
123
|
+
return 1
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Non-blank body line count (everything after the frontmatter block).
|
|
127
|
+
nonblank_body_lines() {
|
|
128
|
+
awk 'NR==1&&/^---$/{fm=1;next} fm&&/^---$/{fm=0;next} !fm{print}' "$1" \
|
|
129
|
+
| grep -cE '[^[:space:]]' || true
|
|
130
|
+
}
|
|
131
|
+
|
|
108
132
|
# --- 2/3. Per-page frontmatter + type vocabulary ---
|
|
109
133
|
while IFS= read -r -d '' page; do
|
|
110
134
|
PAGE_COUNT=$((PAGE_COUNT + 1))
|
|
111
135
|
rel="${page#"$BUNDLE/"}"
|
|
136
|
+
base="$(basename "$rel")"
|
|
112
137
|
|
|
113
|
-
# A page is a "concept" only if its frontmatter declares a type.
|
|
114
138
|
type_val="$(get_yaml_field "$page" "type")"
|
|
115
|
-
|
|
139
|
+
|
|
140
|
+
# Empty / placeholder pages: a non-meta page with no type (or no body) slips
|
|
141
|
+
# past every downstream check (quality + coverage both key on `type`). Catch
|
|
142
|
+
# it here so a blank or stub file can never ship.
|
|
143
|
+
if [[ -z "$type_val" ]]; then
|
|
144
|
+
if ! is_meta_page "$base" "$page"; then
|
|
145
|
+
if [[ "$(nonblank_body_lines "$page")" -eq 0 ]]; then
|
|
146
|
+
add_error "$rel: empty page (no frontmatter type, no body) — every wiki page must be a real concept"
|
|
147
|
+
else
|
|
148
|
+
add_error "$rel: untyped page (missing frontmatter 'type:') — not a valid concept page"
|
|
149
|
+
fi
|
|
150
|
+
fi
|
|
151
|
+
continue
|
|
152
|
+
fi
|
|
116
153
|
CONCEPT_COUNT=$((CONCEPT_COUNT + 1))
|
|
117
154
|
|
|
118
155
|
for key in title description resource; do
|
|
@@ -124,6 +161,24 @@ while IFS= read -r -d '' page; do
|
|
|
124
161
|
if ! is_known_type "$type_val"; then
|
|
125
162
|
add_error "$rel: unknown concept type '$type_val' (frozen vocab: $OKF_TYPES)"
|
|
126
163
|
fi
|
|
164
|
+
|
|
165
|
+
# A typed concept page with no body is still a stub — fail it even though the
|
|
166
|
+
# depth/anti-stub bars live in the quality layer (this layer must stand alone).
|
|
167
|
+
if [[ "$(nonblank_body_lines "$page")" -eq 0 ]]; then
|
|
168
|
+
add_error "$rel: concept page has empty body"
|
|
169
|
+
fi
|
|
170
|
+
done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
|
|
171
|
+
|
|
172
|
+
# --- 3b. Unreplaced template tokens (any page, including index pages) ---
|
|
173
|
+
# Quality checks skip index.md pages, so a leftover {SECTION_TITLE}/{PROJECT_NAME}
|
|
174
|
+
# placeholder in a hand-seeded index would otherwise survive. Match {ALL_CAPS}
|
|
175
|
+
# tokens (safe: real prose almost never contains them).
|
|
176
|
+
while IFS= read -r -d '' page; do
|
|
177
|
+
prel="${page#"$BUNDLE/"}"
|
|
178
|
+
if grep -qE '\{[A-Z][A-Z0-9_]+\}' "$page" 2>/dev/null; then
|
|
179
|
+
tok="$(grep -oE '\{[A-Z][A-Z0-9_]+\}' "$page" | head -1)"
|
|
180
|
+
add_error "$prel: unreplaced template token '$tok'"
|
|
181
|
+
fi
|
|
127
182
|
done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
|
|
128
183
|
|
|
129
184
|
# --- 4. Cross-link resolution ---
|
package/skills/init/SKILL.md
CHANGED
|
@@ -221,7 +221,7 @@ The tier-gated default rests on **maintainability/readability** (one navigable c
|
|
|
221
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
222
|
2. Every `required` entry in `concept-plan.json` has a non-stub page.
|
|
223
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) —
|
|
224
|
+
4. `systems/coverage.md` was generated by tooling (verify the `<!-- okf:coverage-generated -->` marker) — **every** package/module the graph found is required by default (the fan-in floor only types Subsystem-vs-Module, it does not exempt), so none may be **MISSING**. Section `index.md` tables are regenerated by `okf-render-views.sh --section-indexes`, never hand-authored, so their links cannot dangle.
|
|
225
225
|
5. On any failure: **do not** atomic-rename; surface `.state/validation-report.json`.
|
|
226
226
|
|
|
227
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.
|
|
@@ -122,17 +122,21 @@ Derive concepts from the graph, not by hand:
|
|
|
122
122
|
```
|
|
123
123
|
1. Survey → existing /draft:init 5-phase + graph snapshot (graph-snapshot.sh)
|
|
124
124
|
2. Plan → DETERMINISTIC. okf-plan-concepts.sh derives the expected-concept
|
|
125
|
-
set from the graph
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
set from the graph. EVERY package the graph knows about is
|
|
126
|
+
required (fan_in ≥ floor → Subsystem; below floor → Module — the
|
|
127
|
+
floor only types/orders, it never exempts); entrypoints → required;
|
|
128
|
+
only --allow-defer matches are deferred (with a reason). Writes
|
|
129
|
+
draft.tmp/.state/concept-plan.json.
|
|
128
130
|
okf-plan-concepts.sh --repo . [--scope PATH] \
|
|
129
131
|
[--manifest FILE] [--min-fan-in 2] [--allow-defer GLOB]... \
|
|
130
132
|
--out draft.tmp/.state/concept-plan.json
|
|
131
133
|
This replaces the old in-context concept enumeration — the boundary
|
|
132
134
|
of the work is now a tool output, not an LLM judgment, so modules
|
|
133
|
-
cannot be silently dropped.
|
|
134
|
-
|
|
135
|
-
(required
|
|
135
|
+
and sub-modules cannot be silently dropped. (Legacy fan-in
|
|
136
|
+
exemption is opt-in via --defer-below-floor.) LOG the counts
|
|
137
|
+
(expected/required/deferred) BEFORE writing any page.
|
|
138
|
+
`generated_order` is topo-ish (required + high-fan-in first) so
|
|
139
|
+
forward cross-links resolve.
|
|
136
140
|
3. Generate → iterate concept-plan.generated_order; write ONE page per REQUIRED
|
|
137
141
|
entry, grounding each from the graph:
|
|
138
142
|
x-callers ← graph-callers.sh --symbol <c>
|
|
@@ -152,9 +156,11 @@ Derive concepts from the graph, not by hand:
|
|
|
152
156
|
--plan draft.tmp/.state/concept-plan.json \
|
|
153
157
|
--path-index draft.tmp/.state/path-to-concept.json \
|
|
154
158
|
--strict --report draft.tmp/.state/validation-report.json
|
|
155
|
-
It runs, in order: okf-validate.sh (structure + reverse index
|
|
156
|
-
|
|
157
|
-
okf-
|
|
159
|
+
It runs, in order: okf-validate.sh (structure + reverse index +
|
|
160
|
+
empty/untyped-page + leftover-template-token + dangling-link checks),
|
|
161
|
+
okf-validate-quality.sh (per-type anti-stub / depth / per-section
|
|
162
|
+
content / mermaid lint), okf-coverage-check.sh (every required plan
|
|
163
|
+
entry → real page).
|
|
158
164
|
ANY layer failing ⇒ exit non-zero ⇒ DO NOT atomic-rename.
|
|
159
165
|
coverage.md (systems/coverage.md) is regenerated by the coverage
|
|
160
166
|
layer; it is tool-owned (marker <!-- okf:coverage-generated -->) —
|
|
@@ -193,6 +199,7 @@ regenerated on every init/refresh so they never drift from the bundle:
|
|
|
193
199
|
```bash
|
|
194
200
|
okf-render-views.sh draft/wiki \
|
|
195
201
|
--arch-out draft/architecture.md \
|
|
202
|
+
--section-indexes \
|
|
196
203
|
--concept-map-into draft/wiki/index.md \
|
|
197
204
|
--concept-map-into draft/.ai-context.md \
|
|
198
205
|
--web draft/wiki/web/index.html
|
|
@@ -200,6 +207,11 @@ okf-render-views.sh draft/wiki \
|
|
|
200
207
|
|
|
201
208
|
- `--arch-out` renders the linear `architecture.md` (banner + TOC + every concept
|
|
202
209
|
page in canonical section order, frontmatter stripped, Mermaid preserved).
|
|
210
|
+
- `--section-indexes` rebuilds each `<section>/index.md` concept table (between its
|
|
211
|
+
`CONCEPT-MAP` markers) from the pages that actually exist in that directory. This
|
|
212
|
+
is mandatory: section indexes are NOT hand-authored — building them from real
|
|
213
|
+
files is what makes their links impossible to dangle. Never write a section
|
|
214
|
+
index "Concepts" table by hand.
|
|
203
215
|
- `--concept-map-into` rebuilds the routing table between the
|
|
204
216
|
`<!-- CONCEPT-MAP:START -->` / `:END` markers from each concept's `title` +
|
|
205
217
|
`type` + `description` (section `index.md` pages excluded).
|