@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/.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/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/lib.sh +5 -0
- package/scripts/tools/_lib.sh +68 -1
- 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
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
|
# ─────────────────────────────────────────────────────────
|
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
|
|
@@ -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.
|