@drafthq/draft 3.5.2 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/cli/src/hosts/cursor.js +3 -0
- package/cli/src/installer.js +7 -3
- package/cli/src/lib/fsx.js +3 -0
- package/cli/src/lib/marker.js +8 -3
- package/core/shared/condensation.md +1 -1
- package/core/shared/git-report-metadata.md +1 -1
- package/core/shared/graph-query.md +24 -24
- package/core/shared/tool-resolver.md +20 -15
- package/core/templates/okf/index.md +6 -10
- package/core/templates/okf/section-index.md +2 -11
- package/core/templates/plan.md +1 -1
- package/integrations/agents/AGENTS.md +142 -492
- package/integrations/copilot/.github/copilot-instructions.md +142 -492
- package/package.json +1 -1
- package/scripts/fetch-memory-engine.sh +1 -1
- package/scripts/lib.sh +79 -2
- package/scripts/tools/_graph_queries.sh +12 -5
- package/scripts/tools/_lib.sh +56 -8
- package/scripts/tools/adr-index.sh +1 -1
- package/scripts/tools/check-skill-line-caps.sh +2 -2
- package/scripts/tools/check-track-hygiene.sh +41 -21
- package/scripts/tools/classify-files.sh +1 -1
- package/scripts/tools/cycle-detect.sh +2 -9
- package/scripts/tools/detect-test-framework.sh +1 -1
- package/scripts/tools/emit-skill-metrics.sh +7 -2
- package/scripts/tools/freshness-check.sh +2 -2
- package/scripts/tools/git-metadata.sh +6 -6
- package/scripts/tools/graph-arch.sh +4 -10
- package/scripts/tools/graph-callers.sh +4 -12
- package/scripts/tools/graph-deps.sh +3 -10
- package/scripts/tools/graph-errors.sh +4 -11
- package/scripts/tools/graph-hierarchy.sh +4 -11
- package/scripts/tools/graph-impact.sh +17 -17
- package/scripts/tools/graph-init.sh +1 -1
- package/scripts/tools/graph-preflight.sh +1 -1
- package/scripts/tools/graph-query.sh +5 -12
- package/scripts/tools/graph-risk.sh +3 -10
- package/scripts/tools/graph-search.sh +4 -11
- package/scripts/tools/graph-snapshot.sh +9 -4
- package/scripts/tools/graph-snippet.sh +3 -10
- package/scripts/tools/graph-tests.sh +3 -10
- package/scripts/tools/graph-traces.sh +3 -10
- package/scripts/tools/hotspot-rank.sh +4 -11
- package/scripts/tools/mermaid-from-graph.sh +3 -10
- package/scripts/tools/migrate-track-frontmatter.sh +3 -1
- package/scripts/tools/okf-coverage-check.sh +24 -6
- package/scripts/tools/okf-emit-catalog.sh +256 -0
- package/scripts/tools/okf-fix-links.sh +326 -0
- package/scripts/tools/okf-plan-concepts.sh +245 -14
- package/scripts/tools/okf-render-views.sh +65 -13
- package/scripts/tools/okf-validate-all.sh +20 -9
- package/scripts/tools/okf-validate-quality.sh +2 -8
- package/scripts/tools/okf-validate.sh +20 -10
- package/scripts/tools/parse-git-log.sh +4 -4
- package/scripts/tools/parse-reports.sh +1 -1
- package/scripts/tools/render-track.sh +1 -1
- package/scripts/tools/run-coverage.sh +1 -1
- package/scripts/tools/scan-markers.sh +4 -4
- package/scripts/tools/validate-frontmatter.sh +2 -2
- package/scripts/tools/verify-citations.sh +2 -2
- package/scripts/tools/verify-doc-anchors.sh +1 -1
- package/scripts/tools/verify-graph-binary.sh +2 -2
- package/skills/adr/SKILL.md +1 -1
- package/skills/bughunt/SKILL.md +1 -1
- package/skills/coverage/SKILL.md +2 -2
- package/skills/debug/SKILL.md +1 -1
- package/skills/decompose/SKILL.md +3 -3
- package/skills/deep-review/SKILL.md +2 -2
- package/skills/deploy-checklist/SKILL.md +1 -1
- package/skills/graph/SKILL.md +1 -1
- package/skills/impact/SKILL.md +1 -1
- package/skills/implement/SKILL.md +2 -2
- package/skills/init/SKILL.md +10 -7
- package/skills/init/references/architecture-spec.md +1 -1
- package/skills/init/references/okf-emitter.md +51 -76
- package/skills/learn/SKILL.md +1 -1
- package/skills/quick-review/SKILL.md +1 -1
- package/skills/review/SKILL.md +3 -3
- package/skills/standup/SKILL.md +1 -1
- package/skills/status/SKILL.md +1 -1
- package/skills/tech-debt/SKILL.md +2 -2
- package/skills/upload/SKILL.md +1 -1
- package/core/templates/track-architecture.md +0 -311
|
@@ -73,13 +73,13 @@ EOF
|
|
|
73
73
|
|
|
74
74
|
while [[ $# -gt 0 ]]; do
|
|
75
75
|
case "$1" in
|
|
76
|
-
--repo) REPO="$2"; shift 2;;
|
|
77
|
-
--scope) SCOPE="$2"; shift 2;;
|
|
78
|
-
--manifest) MANIFEST="$2"; shift 2;;
|
|
79
|
-
--min-fan-in) MIN_FAN_IN="$2"; shift 2;;
|
|
76
|
+
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
|
|
77
|
+
--scope) SCOPE="${2:?--scope requires a value}"; shift 2;;
|
|
78
|
+
--manifest) MANIFEST="${2:?--manifest requires a value}"; shift 2;;
|
|
79
|
+
--min-fan-in) MIN_FAN_IN="${2:?--min-fan-in requires a value}"; shift 2;;
|
|
80
80
|
--defer-below-floor) DEFER_BELOW_FLOOR=1; shift;;
|
|
81
81
|
--allow-defer) ALLOW_DEFER+=("$2"); shift 2;;
|
|
82
|
-
--out) OUT="$2"; shift 2;;
|
|
82
|
+
--out) OUT="${2:?--out requires a value}"; shift 2;;
|
|
83
83
|
--json) JSON=1; shift;;
|
|
84
84
|
--help|-h) usage; exit 0;;
|
|
85
85
|
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
|
|
@@ -111,13 +111,38 @@ is_deferred_name() {
|
|
|
111
111
|
E_ID=(); E_TYPE=(); E_RES=(); E_FANIN=(); E_REQ=(); E_REASON=()
|
|
112
112
|
SOURCE="heuristic"
|
|
113
113
|
DEGRADED="false"
|
|
114
|
+
DISCOVERY_META=() # e.g. cargo, npm, go, graph, heuristic
|
|
115
|
+
|
|
116
|
+
# Graph package names that are almost always tokenizer/type noise, not modules.
|
|
117
|
+
is_noise_package_name() {
|
|
118
|
+
local n="$1"
|
|
119
|
+
case "$n" in
|
|
120
|
+
str|list|dict|int|bool|float|string|type|mod|Cargo|cargo|profile|int64|uint|byte|char|void|null|None|true|false|i32|i64|u32|u64|f32|f64|Option|Result|Error|Self|self)
|
|
121
|
+
return 0;;
|
|
122
|
+
esac
|
|
123
|
+
return 1
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# True if concept_id already recorded.
|
|
127
|
+
has_concept_id() {
|
|
128
|
+
local want="$1" id
|
|
129
|
+
for id in "${E_ID[@]:-}"; do
|
|
130
|
+
[[ "$id" == "$want" ]] && return 0
|
|
131
|
+
done
|
|
132
|
+
return 1
|
|
133
|
+
}
|
|
114
134
|
|
|
115
135
|
add_concept() {
|
|
116
136
|
# name section type resource fan_in required reason
|
|
117
137
|
local name="$1" section="$2" type="$3" resource="$4" fan_in="$5" required="$6" reason="$7"
|
|
118
138
|
local stem; stem="$(slug "$name")"
|
|
119
139
|
[[ -n "$stem" ]] || stem="component"
|
|
120
|
-
|
|
140
|
+
local cid="$section/$stem.md"
|
|
141
|
+
# Dedupe by concept_id (cargo + graph may both see the same crate).
|
|
142
|
+
if has_concept_id "$cid"; then
|
|
143
|
+
return 0
|
|
144
|
+
fi
|
|
145
|
+
E_ID+=("$cid")
|
|
121
146
|
E_TYPE+=("$type")
|
|
122
147
|
E_RES+=("$resource")
|
|
123
148
|
E_FANIN+=("$fan_in")
|
|
@@ -141,29 +166,195 @@ plan_from_manifest() {
|
|
|
141
166
|
SOURCE="manifest"
|
|
142
167
|
}
|
|
143
168
|
|
|
144
|
-
# ---
|
|
169
|
+
# --- 2a. Language-aware workspace inventories (Cargo / npm / Go) ---
|
|
170
|
+
# These produce crate/package-level concepts that graph .packages often collapse
|
|
171
|
+
# or mis-label (especially Rust monorepos). Prefer path-grounded resources.
|
|
172
|
+
|
|
173
|
+
plan_from_cargo_workspace() {
|
|
174
|
+
local cargo="$REPO/Cargo.toml"
|
|
175
|
+
[[ -f "$cargo" ]] || return 1
|
|
176
|
+
# Only treat as workspace if [workspace] present with members.
|
|
177
|
+
grep -qE '^\[workspace\]' "$cargo" || return 1
|
|
178
|
+
grep -qE 'members\s*=' "$cargo" || return 1
|
|
179
|
+
|
|
180
|
+
local members=()
|
|
181
|
+
# Extract quoted paths inside the members = [ ... ] array (possibly multi-line).
|
|
182
|
+
local in_members=0 line m
|
|
183
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
184
|
+
if [[ "$line" =~ members[[:space:]]*=[[:space:]]*\[ ]]; then
|
|
185
|
+
in_members=1
|
|
186
|
+
fi
|
|
187
|
+
if [[ $in_members -eq 1 ]]; then
|
|
188
|
+
while [[ "$line" =~ \"([^\"]+)\" ]]; do
|
|
189
|
+
m="${BASH_REMATCH[1]}"
|
|
190
|
+
members+=("$m")
|
|
191
|
+
line="${line#*\"$m\"}"
|
|
192
|
+
done
|
|
193
|
+
[[ "$line" == *"]"* ]] && in_members=0
|
|
194
|
+
fi
|
|
195
|
+
done < "$cargo"
|
|
196
|
+
|
|
197
|
+
[[ ${#members[@]} -gt 0 ]] || return 1
|
|
198
|
+
|
|
199
|
+
local path name ct required reason type
|
|
200
|
+
local added=0
|
|
201
|
+
for path in "${members[@]}"; do
|
|
202
|
+
[[ -z "$path" ]] && continue
|
|
203
|
+
# Skip globs we can't expand cheaply without bash nullglob walk
|
|
204
|
+
if [[ "$path" == *"*"* ]]; then
|
|
205
|
+
local base="${path%%/\*}"
|
|
206
|
+
[[ -d "$REPO/$base" ]] || continue
|
|
207
|
+
while IFS= read -r ct; do
|
|
208
|
+
name="$(basename "$(dirname "$ct")")"
|
|
209
|
+
[[ -z "$name" ]] && continue
|
|
210
|
+
if is_deferred_name "$name"; then
|
|
211
|
+
required=false; reason="allow-defer match"
|
|
212
|
+
else
|
|
213
|
+
required=true; reason=""
|
|
214
|
+
fi
|
|
215
|
+
type=Module
|
|
216
|
+
# Entrypoint packaging root if only main.rs and no lib.rs (heuristic)
|
|
217
|
+
if [[ -f "$(dirname "$ct")/src/main.rs" && ! -f "$(dirname "$ct")/src/lib.rs" ]]; then
|
|
218
|
+
type=Module
|
|
219
|
+
fi
|
|
220
|
+
add_concept "$name" systems "$type" "$(dirname "$ct" | sed "s|^$REPO/||")" 0 "$required" "$reason"
|
|
221
|
+
added=$((added+1))
|
|
222
|
+
done < <(find "$REPO/$base" -mindepth 1 -maxdepth 3 -type f -name 'Cargo.toml' 2>/dev/null | sort)
|
|
223
|
+
continue
|
|
224
|
+
fi
|
|
225
|
+
[[ -f "$REPO/$path/Cargo.toml" || -f "$REPO/$path" ]] || continue
|
|
226
|
+
local res="$path"
|
|
227
|
+
[[ -f "$REPO/$path" && "$path" == */Cargo.toml ]] && res="$(dirname "$path")"
|
|
228
|
+
name="$(basename "$res")"
|
|
229
|
+
if is_deferred_name "$name"; then
|
|
230
|
+
required=false; reason="allow-defer match"
|
|
231
|
+
else
|
|
232
|
+
required=true; reason=""
|
|
233
|
+
fi
|
|
234
|
+
add_concept "$name" systems Module "$res" 0 "$required" "$reason"
|
|
235
|
+
added=$((added+1))
|
|
236
|
+
done
|
|
237
|
+
[[ $added -gt 0 ]] || return 1
|
|
238
|
+
DISCOVERY_META+=("cargo")
|
|
239
|
+
return 0
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
plan_from_npm_workspaces() {
|
|
243
|
+
local pkg="$REPO/package.json"
|
|
244
|
+
[[ -f "$pkg" ]] || return 1
|
|
245
|
+
command -v jq >/dev/null 2>&1 || return 1
|
|
246
|
+
# workspaces may be array or { packages: [] }
|
|
247
|
+
local names
|
|
248
|
+
names="$(jq -r '
|
|
249
|
+
if .workspaces|type=="array" then .workspaces[]
|
|
250
|
+
elif .workspaces.packages|type=="array" then .workspaces.packages[]
|
|
251
|
+
else empty end
|
|
252
|
+
' "$pkg" 2>/dev/null || true)"
|
|
253
|
+
[[ -n "$names" ]] || return 1
|
|
254
|
+
local path name required reason added=0
|
|
255
|
+
while IFS= read -r path; do
|
|
256
|
+
[[ -z "$path" || "$path" == *"*"* ]] && continue
|
|
257
|
+
name="$(basename "$path")"
|
|
258
|
+
if is_deferred_name "$name"; then
|
|
259
|
+
required=false; reason="allow-defer match"
|
|
260
|
+
else
|
|
261
|
+
required=true; reason=""
|
|
262
|
+
fi
|
|
263
|
+
add_concept "$name" systems Module "$path" 0 "$required" "$reason"
|
|
264
|
+
added=$((added+1))
|
|
265
|
+
done <<< "$names"
|
|
266
|
+
[[ $added -gt 0 ]] || return 1
|
|
267
|
+
DISCOVERY_META+=("npm")
|
|
268
|
+
return 0
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
plan_from_go_modules() {
|
|
272
|
+
# go.work use directives, else single go.mod at root, else first-level */go.mod
|
|
273
|
+
local added=0 name required reason path
|
|
274
|
+
if [[ -f "$REPO/go.work" ]]; then
|
|
275
|
+
while IFS= read -r path; do
|
|
276
|
+
path="$(printf '%s' "$path" | sed -E 's/^[[:space:]]*//')"
|
|
277
|
+
[[ -z "$path" || "$path" == //* ]] && continue
|
|
278
|
+
[[ -f "$REPO/$path/go.mod" ]] || continue
|
|
279
|
+
name="$(basename "$path")"
|
|
280
|
+
if is_deferred_name "$name"; then
|
|
281
|
+
required=false; reason="allow-defer match"
|
|
282
|
+
else
|
|
283
|
+
required=true; reason=""
|
|
284
|
+
fi
|
|
285
|
+
add_concept "$name" systems Module "$path" 0 "$required" "$reason"
|
|
286
|
+
added=$((added+1))
|
|
287
|
+
done < <(awk '/^use[[:space:]]*\(/,/^\)/ {if ($1!="use" && $1!="(" && $1!=")") print $1}
|
|
288
|
+
/^use[[:space:]]+\./ {print $2}' "$REPO/go.work" | tr -d '"')
|
|
289
|
+
fi
|
|
290
|
+
if [[ $added -eq 0 && -f "$REPO/go.mod" ]]; then
|
|
291
|
+
name="$(basename "$REPO")"
|
|
292
|
+
add_concept "$name" systems Module "." 0 true ""
|
|
293
|
+
added=1
|
|
294
|
+
fi
|
|
295
|
+
if [[ $added -eq 0 ]]; then
|
|
296
|
+
while IFS= read -r path; do
|
|
297
|
+
name="$(basename "$(dirname "$path")")"
|
|
298
|
+
add_concept "$name" systems Module "$(dirname "$path" | sed "s|^$REPO/||")" 0 true ""
|
|
299
|
+
added=$((added+1))
|
|
300
|
+
done < <(find "$REPO" -mindepth 2 -maxdepth 3 -type f -name 'go.mod' 2>/dev/null | sort | head -50)
|
|
301
|
+
fi
|
|
302
|
+
[[ $added -gt 0 ]] || return 1
|
|
303
|
+
DISCOVERY_META+=("go")
|
|
304
|
+
return 0
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# --- 2b. Graph path ---
|
|
145
308
|
plan_from_graph() {
|
|
146
309
|
local arch; arch="$(scripts_graph_arch)" || return 1
|
|
147
310
|
[[ -n "$arch" ]] || return 1
|
|
148
311
|
echo "$arch" | jq -e '.packages != null' >/dev/null 2>&1 || return 1
|
|
149
312
|
|
|
150
|
-
|
|
313
|
+
local have_lang_inventory=0
|
|
314
|
+
[[ ${#DISCOVERY_META[@]} -gt 0 ]] && have_lang_inventory=1
|
|
315
|
+
|
|
151
316
|
local name fan_in type required reason
|
|
152
317
|
# Packages → systems/<pkg>.md
|
|
153
318
|
while IFS=$'\t' read -r name fan_in; do
|
|
154
319
|
[[ -z "$name" ]] && continue
|
|
320
|
+
# Drop tokenizer noise when we already have a real inventory (cargo/npm/go).
|
|
321
|
+
if [[ $have_lang_inventory -eq 1 ]] && is_noise_package_name "$name"; then
|
|
322
|
+
continue
|
|
323
|
+
fi
|
|
324
|
+
# Even without inventory, skip pure noise names that do not map to a dir.
|
|
325
|
+
if is_noise_package_name "$name"; then
|
|
326
|
+
local mapped=0
|
|
327
|
+
for d in "$REPO/$name" "$REPO/crates/$name" "$REPO/src/$name" "$REPO/third_party/$name"; do
|
|
328
|
+
[[ -d "$d" ]] && mapped=1 && break
|
|
329
|
+
done
|
|
330
|
+
[[ $mapped -eq 0 ]] && continue
|
|
331
|
+
fi
|
|
155
332
|
if is_deferred_name "$name"; then
|
|
156
333
|
required=false; reason="allow-defer match"; type=Module
|
|
157
334
|
elif (( fan_in >= MIN_FAN_IN )); then
|
|
158
335
|
required=true; reason=""; type=Subsystem
|
|
159
336
|
elif [[ $DEFER_BELOW_FLOOR -eq 1 ]]; then
|
|
160
|
-
# Opt-in legacy behavior: low-fan-in packages are exempted.
|
|
161
337
|
required=false; reason="fan_in $fan_in < floor $MIN_FAN_IN"; type=Module
|
|
162
338
|
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
339
|
required=true; reason=""; type=Module
|
|
166
340
|
fi
|
|
341
|
+
# When language inventory exists, graph packages that are coarse parents
|
|
342
|
+
# (e.g. codegen/) become Subsystems only if a matching directory exists.
|
|
343
|
+
if [[ $have_lang_inventory -eq 1 ]]; then
|
|
344
|
+
if [[ -d "$REPO/$name" || -d "$REPO/crates/$name" || -d "$REPO/third_party/$name" || -d "$REPO/prod/$name" ]]; then
|
|
345
|
+
type=Subsystem
|
|
346
|
+
# Prefer real path as resource when known
|
|
347
|
+
for d in "$name" "crates/$name" "third_party/$name" "prod/$name"; do
|
|
348
|
+
if [[ -d "$REPO/$d" ]]; then
|
|
349
|
+
add_concept "$name" systems "$type" "$d" "$fan_in" "$required" "$reason"
|
|
350
|
+
continue 2
|
|
351
|
+
fi
|
|
352
|
+
done
|
|
353
|
+
else
|
|
354
|
+
# Unmapped graph label with inventory present → skip (not a crate).
|
|
355
|
+
continue
|
|
356
|
+
fi
|
|
357
|
+
fi
|
|
167
358
|
add_concept "$name" systems "$type" "$name" "$fan_in" "$required" "$reason"
|
|
168
359
|
done < <(echo "$arch" | jq -r '.packages[]? | [.name, (.fan_in // 0)] | @tsv')
|
|
169
360
|
|
|
@@ -178,6 +369,8 @@ plan_from_graph() {
|
|
|
178
369
|
done < <(echo "$arch" | jq -r '
|
|
179
370
|
(.entry_points // [])[]? | if type=="object" then (.name // .path // empty) else . end' \
|
|
180
371
|
| sort -u)
|
|
372
|
+
DISCOVERY_META+=("graph")
|
|
373
|
+
return 0
|
|
181
374
|
}
|
|
182
375
|
|
|
183
376
|
# graph-arch.sh wrapper that tolerates the "unavailable" sentinel.
|
|
@@ -218,13 +411,30 @@ plan_from_heuristic() {
|
|
|
218
411
|
}
|
|
219
412
|
|
|
220
413
|
# --- Drive discovery in priority order ---
|
|
414
|
+
# 1. --manifest (authoritative, exclusive)
|
|
415
|
+
# 2. Language inventories (Cargo / npm / Go) — can combine with graph parents
|
|
416
|
+
# 3. Graph packages (filtered when inventory present)
|
|
417
|
+
# 4. Heuristic top-level dirs
|
|
221
418
|
if [[ -n "$MANIFEST" ]]; then
|
|
222
419
|
[[ -f "$MANIFEST" ]] || { echo "ERROR: --manifest not found: $MANIFEST" >&2; exit 1; }
|
|
223
420
|
plan_from_manifest
|
|
224
|
-
|
|
225
|
-
:
|
|
421
|
+
DISCOVERY_META+=("manifest")
|
|
226
422
|
else
|
|
227
|
-
|
|
423
|
+
# Language inventories first so cargo crate names win concept_ids.
|
|
424
|
+
plan_from_cargo_workspace || true
|
|
425
|
+
plan_from_npm_workspaces || true
|
|
426
|
+
plan_from_go_modules || true
|
|
427
|
+
if plan_from_graph; then
|
|
428
|
+
:
|
|
429
|
+
elif [[ ${#E_ID[@]} -eq 0 ]]; then
|
|
430
|
+
plan_from_heuristic
|
|
431
|
+
DISCOVERY_META+=("heuristic")
|
|
432
|
+
fi
|
|
433
|
+
# If graph failed but we have cargo/npm/go, still success.
|
|
434
|
+
if [[ ${#E_ID[@]} -eq 0 ]]; then
|
|
435
|
+
plan_from_heuristic
|
|
436
|
+
DISCOVERY_META+=("heuristic")
|
|
437
|
+
fi
|
|
228
438
|
fi
|
|
229
439
|
|
|
230
440
|
if [[ ${#E_ID[@]} -eq 0 ]]; then
|
|
@@ -232,6 +442,19 @@ if [[ ${#E_ID[@]} -eq 0 ]]; then
|
|
|
232
442
|
exit 2
|
|
233
443
|
fi
|
|
234
444
|
|
|
445
|
+
# Compose SOURCE label from discovery meta
|
|
446
|
+
if [[ ${#DISCOVERY_META[@]} -gt 0 ]]; then
|
|
447
|
+
# de-dupe meta labels
|
|
448
|
+
SOURCE="$(printf '%s\n' "${DISCOVERY_META[@]}" | awk '!s[$0]++' | paste -sd+ -)"
|
|
449
|
+
[[ -z "$SOURCE" ]] && SOURCE="heuristic"
|
|
450
|
+
# degraded only when pure heuristic with no language/graph
|
|
451
|
+
if [[ "$SOURCE" == "heuristic" ]]; then
|
|
452
|
+
DEGRADED="true"
|
|
453
|
+
else
|
|
454
|
+
DEGRADED="false"
|
|
455
|
+
fi
|
|
456
|
+
fi
|
|
457
|
+
|
|
235
458
|
# Required-first, then deferred; stable within group (topological-ish: high fan-in
|
|
236
459
|
# subsystems first so forward cross-links resolve during generation).
|
|
237
460
|
emit_plan() {
|
|
@@ -257,6 +480,14 @@ emit_plan() {
|
|
|
257
480
|
printf ' "source": "%s",\n' "$SOURCE"
|
|
258
481
|
printf ' "degraded": %s,\n' "$DEGRADED"
|
|
259
482
|
printf ' "min_fan_in": %d,\n' "$MIN_FAN_IN"
|
|
483
|
+
printf ' "discovery": ['
|
|
484
|
+
local di first_d=1
|
|
485
|
+
for di in "${DISCOVERY_META[@]:-}"; do
|
|
486
|
+
[[ -z "$di" ]] && continue
|
|
487
|
+
[[ $first_d -eq 1 ]] && first_d=0 || printf ','
|
|
488
|
+
printf '"%s"' "$(json_escape "$di")"
|
|
489
|
+
done
|
|
490
|
+
printf '],\n'
|
|
260
491
|
# generated_order
|
|
261
492
|
printf ' "generated_order": ['
|
|
262
493
|
local first=1
|
|
@@ -62,12 +62,12 @@ EOF
|
|
|
62
62
|
|
|
63
63
|
while [[ $# -gt 0 ]]; do
|
|
64
64
|
case "$1" in
|
|
65
|
-
--arch-out) ARCH_OUT="$2"; shift 2;;
|
|
65
|
+
--arch-out) ARCH_OUT="${2:?--arch-out requires a value}"; shift 2;;
|
|
66
66
|
--concept-map-into) CMAP_INTO+=("$2"); shift 2;;
|
|
67
67
|
--section-indexes) SECTION_INDEXES=1; shift;;
|
|
68
|
-
--web) WEB_OUT="$2"; shift 2;;
|
|
69
|
-
--coverage-report) COVERAGE_REPORT="$2"; shift 2;;
|
|
70
|
-
--validated-at) VALIDATED_AT="$2"; shift 2;;
|
|
68
|
+
--web) WEB_OUT="${2:?--web requires a value}"; shift 2;;
|
|
69
|
+
--coverage-report) COVERAGE_REPORT="${2:?--coverage-report requires a value}"; shift 2;;
|
|
70
|
+
--validated-at) VALIDATED_AT="${2:?--validated-at requires a value}"; shift 2;;
|
|
71
71
|
--help|-h) usage; exit 0;;
|
|
72
72
|
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
|
|
73
73
|
*)
|
|
@@ -101,6 +101,38 @@ ordered_pages() {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
# Strip YAML frontmatter from a page (leading --- ... --- block on line 1).
|
|
104
|
+
|
|
105
|
+
# Rewrite markdown links so concatenated architecture.md (at draft/) resolves them.
|
|
106
|
+
# stdin body; $1 = bundle-relative path e.g. systems/foo.md
|
|
107
|
+
rewrite_body_links() {
|
|
108
|
+
local rel="${1:-}"
|
|
109
|
+
local sec="${rel%%/*}"
|
|
110
|
+
python3 -c '
|
|
111
|
+
import sys, re
|
|
112
|
+
sec = sys.argv[1]
|
|
113
|
+
text = sys.stdin.read()
|
|
114
|
+
sections = ("systems", "features", "overview", "entrypoints", "reference")
|
|
115
|
+
def repl(m):
|
|
116
|
+
label, href = m.group(1), m.group(2).strip()
|
|
117
|
+
if href.startswith(("http://", "https://", "mailto:", "#", "wiki/")):
|
|
118
|
+
return m.group(0)
|
|
119
|
+
if "::" in href and not href.startswith("../"):
|
|
120
|
+
return "`" + label + "`"
|
|
121
|
+
path, _, frag = href.partition("#")
|
|
122
|
+
frag_s = "#" + frag if frag else ""
|
|
123
|
+
if path.startswith("../"):
|
|
124
|
+
rest = path[3:]
|
|
125
|
+
if rest.split("/")[0] in sections:
|
|
126
|
+
return "[%s](wiki/%s%s)" % (label, rest, frag_s)
|
|
127
|
+
if any(path.startswith(s + "/") for s in sections):
|
|
128
|
+
return "[%s](wiki/%s%s)" % (label, path, frag_s)
|
|
129
|
+
if path.endswith(".md") and "/" not in path and sec in sections:
|
|
130
|
+
return "[%s](wiki/%s/%s%s)" % (label, sec, path, frag_s)
|
|
131
|
+
return m.group(0)
|
|
132
|
+
sys.stdout.write(re.sub(r"\[([^\]]*)\]\(([^)]+)\)", repl, text))
|
|
133
|
+
' "$sec"
|
|
134
|
+
}
|
|
135
|
+
|
|
104
136
|
strip_frontmatter() {
|
|
105
137
|
awk '
|
|
106
138
|
NR==1 && /^---$/ { fm=1; next }
|
|
@@ -109,6 +141,17 @@ strip_frontmatter() {
|
|
|
109
141
|
' "$1"
|
|
110
142
|
}
|
|
111
143
|
|
|
144
|
+
# Human-facing title for a page: frontmatter `title`, else the first `# H1` in the
|
|
145
|
+
# body, else the bundle-relative path. Index files are frontmatter-less under OKF
|
|
146
|
+
# §6, so the H1 fallback keeps the rendered TOC / nav readable.
|
|
147
|
+
page_title() {
|
|
148
|
+
local t; t="$(get_yaml_field "$1" title)"
|
|
149
|
+
if [[ -z "$t" ]]; then
|
|
150
|
+
t="$(strip_frontmatter "$1" | grep -m1 -E '^#[[:space:]]+' | sed -E 's/^#[[:space:]]+//')"
|
|
151
|
+
fi
|
|
152
|
+
printf '%s' "$t"
|
|
153
|
+
}
|
|
154
|
+
|
|
112
155
|
# Coverage-honesty banner, sourced from okf-coverage-check.sh's JSON report.
|
|
113
156
|
# Silent when no report is supplied (keeps the view backward-compatible).
|
|
114
157
|
emit_coverage_banner() {
|
|
@@ -159,10 +202,9 @@ render_architecture() {
|
|
|
159
202
|
echo "- **${sec}/**"
|
|
160
203
|
last_sec="$sec"
|
|
161
204
|
fi
|
|
162
|
-
title="$(
|
|
205
|
+
title="$(page_title "$BUNDLE/$rel")"
|
|
163
206
|
[[ -n "$title" ]] || title="$rel"
|
|
164
|
-
local anchor; anchor="$(
|
|
165
|
-
anchor="${anchor#-}"; anchor="${anchor%-}"
|
|
207
|
+
local anchor; anchor="$(gfm_slug "$title")"
|
|
166
208
|
echo " - [${title}](#${anchor})"
|
|
167
209
|
done < <(ordered_pages)
|
|
168
210
|
echo ""
|
|
@@ -172,10 +214,16 @@ render_architecture() {
|
|
|
172
214
|
echo ""
|
|
173
215
|
echo "---"
|
|
174
216
|
echo ""
|
|
175
|
-
strip_frontmatter "$BUNDLE/$rel"
|
|
217
|
+
strip_frontmatter "$BUNDLE/$rel" | rewrite_body_links "$rel"
|
|
176
218
|
done < <(ordered_pages)
|
|
177
219
|
} >"$tmp"
|
|
178
220
|
mv "$tmp" "$out"
|
|
221
|
+
if [[ -x "$SCRIPT_DIR/okf-fix-links.sh" ]]; then
|
|
222
|
+
draft_dir="$(cd "$(dirname "$out")" && pwd)"
|
|
223
|
+
if [[ -d "$draft_dir/wiki" || "$(basename "$BUNDLE")" == "wiki" ]]; then
|
|
224
|
+
"$SCRIPT_DIR/okf-fix-links.sh" --file "$out" --wiki "$BUNDLE" --fix >/dev/null 2>&1 || true
|
|
225
|
+
fi
|
|
226
|
+
fi
|
|
179
227
|
echo "rendered architecture view → $out ($(ordered_pages | grep -c . ) pages)"
|
|
180
228
|
}
|
|
181
229
|
|
|
@@ -183,10 +231,14 @@ render_architecture() {
|
|
|
183
231
|
build_concept_map() {
|
|
184
232
|
echo "| Concept | Type | Open it when… |"
|
|
185
233
|
echo "|---------|------|---------------|"
|
|
186
|
-
local rel type title desc
|
|
234
|
+
local rel base type title desc
|
|
187
235
|
while IFS= read -r -d '' page; do
|
|
188
236
|
rel="${page#"$BUNDLE/"}"
|
|
189
|
-
|
|
237
|
+
base="$(basename "$rel")"
|
|
238
|
+
# Meta pages are not routable concepts (reserved index/log + the typed,
|
|
239
|
+
# tool-generated coverage page).
|
|
240
|
+
[[ "$base" == "index.md" || "$base" == "log.md" || "$base" == "coverage.md" ]] && continue
|
|
241
|
+
grep -q '<!-- okf:coverage-generated -->' "$page" 2>/dev/null && continue
|
|
190
242
|
type="$(get_yaml_field "$page" type)"
|
|
191
243
|
[[ -n "$type" ]] || continue
|
|
192
244
|
title="$(get_yaml_field "$page" title)"
|
|
@@ -330,7 +382,7 @@ HTML_HEAD
|
|
|
330
382
|
while IFS= read -r -d '' page; do
|
|
331
383
|
local rel title type
|
|
332
384
|
rel="${page#"$BUNDLE/"}"
|
|
333
|
-
title="$(
|
|
385
|
+
title="$(page_title "$page")"; [[ -n "$title" ]] || title="$rel"
|
|
334
386
|
type="$(get_yaml_field "$page" type)"
|
|
335
387
|
printf '%s: {"title": %s, "type": %s, "md": %s},\n' \
|
|
336
388
|
"$(jq -Rn --arg v "$rel" '$v')" \
|
|
@@ -372,7 +424,7 @@ function render(md, base){
|
|
|
372
424
|
var blocks=[], src=md.replace(/```(\w*)\n([\s\S]*?)```/g,function(m,lang,body){
|
|
373
425
|
var cls = lang==='mermaid' ? ' class="mermaid-src"' : '';
|
|
374
426
|
blocks.push('<pre'+cls+'><code>'+esc(body.replace(/\n$/,''))+'</code></pre>');
|
|
375
|
-
return '
|
|
427
|
+
return 'BLOCK'+(blocks.length-1)+'';
|
|
376
428
|
});
|
|
377
429
|
var lines=src.split('\n'), out='', i=0, list='', tbl=[];
|
|
378
430
|
function closeList(){ if(list){ out+='</'+list+'>'; list=''; } }
|
|
@@ -395,7 +447,7 @@ function render(md, base){
|
|
|
395
447
|
if(/^\s*>\s?/.test(ln)){ closeList(); out+='<blockquote>'+inline(esc(ln.replace(/^\s*>\s?/,'')),base)+'</blockquote>'; continue; }
|
|
396
448
|
var li=ln.match(/^\s*([-*]|\d+\.)\s+(.*)$/);
|
|
397
449
|
if(li){ var want=/^\d/.test(li[1])?'ol':'ul'; if(list!==want){ closeList(); list=want; out+='<'+want+'>'; } out+='<li>'+inline(esc(li[2]),base)+'</li>'; continue; }
|
|
398
|
-
var b=ln.match(
|
|
450
|
+
var b=ln.match(/^BLOCK(\d+)$/);
|
|
399
451
|
if(b){ closeList(); out+=blocks[+b[1]]; continue; }
|
|
400
452
|
if(/^\s*$/.test(ln)){ closeList(); continue; }
|
|
401
453
|
closeList(); out+='<p>'+inline(esc(ln),base)+'</p>';
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
# code, so a bundle that is structurally broken, full of stubs, or missing
|
|
7
7
|
# required components is never promoted.
|
|
8
8
|
#
|
|
9
|
-
# Layer 1 okf-validate.sh structure (frontmatter, types, links, index)
|
|
10
9
|
# Layer 2 okf-validate-quality.sh per-type anti-stub / depth / mermaid lint
|
|
11
10
|
# Layer 3 okf-coverage-check.sh every required plan entry has a real page
|
|
11
|
+
# (may rewrite systems/coverage.md)
|
|
12
|
+
# Layer 1 okf-validate.sh structure LAST so coverage rewrite is checked
|
|
12
13
|
#
|
|
13
14
|
# Usage:
|
|
14
15
|
# okf-validate-all.sh <BUNDLE_DIR> [--plan FILE] [--path-index FILE]
|
|
@@ -53,10 +54,10 @@ EOF
|
|
|
53
54
|
|
|
54
55
|
while [[ $# -gt 0 ]]; do
|
|
55
56
|
case "$1" in
|
|
56
|
-
--plan) PLAN="$2"; shift 2;;
|
|
57
|
-
--path-index) PATH_INDEX="$2"; shift 2;;
|
|
57
|
+
--plan) PLAN="${2:?--plan requires a value}"; shift 2;;
|
|
58
|
+
--path-index) PATH_INDEX="${2:?--path-index requires a value}"; shift 2;;
|
|
58
59
|
--strict) STRICT=1; shift;;
|
|
59
|
-
--report) REPORT="$2"; shift 2;;
|
|
60
|
+
--report) REPORT="${2:?--report requires a value}"; shift 2;;
|
|
60
61
|
--json) JSON=1; shift;;
|
|
61
62
|
--help|-h) usage; exit 0;;
|
|
62
63
|
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
|
|
@@ -79,16 +80,23 @@ run_layer() {
|
|
|
79
80
|
return $rc
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
#
|
|
83
|
+
# Order matters:
|
|
84
|
+
# 1) quality — page content (does not rewrite files)
|
|
85
|
+
# 2) coverage — may REGENERATE systems/coverage.md (tool-owned)
|
|
86
|
+
# 3) structure — link/frontmatter check LAST so coverage rewrite cannot leave
|
|
87
|
+
# dangling links that structure already approved.
|
|
88
|
+
#
|
|
89
|
+
# Historical bug: structure-before-coverage let init promote a bundle whose
|
|
90
|
+
# coverage.md still had broken relative links (entrypoints/ from systems/).
|
|
91
|
+
|
|
83
92
|
v1_args=("$BUNDLE")
|
|
84
93
|
[[ -n "$PATH_INDEX" ]] && v1_args+=(--path-index "$PATH_INDEX")
|
|
85
|
-
if run_layer structure "$SCRIPT_DIR/okf-validate.sh" "${v1_args[@]}"; then L1=pass; else L1=fail; OVERALL=1; fi
|
|
86
|
-
|
|
87
|
-
# Layer 2: quality.
|
|
88
94
|
q_args=("$BUNDLE"); [[ $STRICT -eq 1 ]] && q_args+=(--strict)
|
|
95
|
+
|
|
96
|
+
# Layer 2: quality (run first for fast fail on stubs).
|
|
89
97
|
if run_layer quality "$SCRIPT_DIR/okf-validate-quality.sh" "${q_args[@]}"; then L2=pass; else L2=fail; OVERALL=1; fi
|
|
90
98
|
|
|
91
|
-
# Layer 3: coverage (
|
|
99
|
+
# Layer 3: coverage (writes coverage.md when not --no-coverage-page).
|
|
92
100
|
if [[ -n "$PLAN" ]]; then
|
|
93
101
|
if run_layer coverage "$SCRIPT_DIR/okf-coverage-check.sh" --plan "$PLAN" --bundle "$BUNDLE"; then
|
|
94
102
|
L3=pass
|
|
@@ -97,6 +105,9 @@ if [[ -n "$PLAN" ]]; then
|
|
|
97
105
|
fi
|
|
98
106
|
fi
|
|
99
107
|
|
|
108
|
+
# Layer 1: structure LAST (after coverage page is final).
|
|
109
|
+
if run_layer structure "$SCRIPT_DIR/okf-validate.sh" "${v1_args[@]}"; then L1=pass; else L1=fail; OVERALL=1; fi
|
|
110
|
+
|
|
100
111
|
REPORT_JSON="$(printf '{"valid":%s,"bundle":"%s","layers":{"structure":"%s","quality":"%s","coverage":"%s"}}\n' \
|
|
101
112
|
"$([[ $OVERALL -eq 0 ]] && echo true || echo false)" "$(json_escape "$BUNDLE")" "$L1" "$L2" "$L3")"
|
|
102
113
|
|
|
@@ -76,15 +76,9 @@ body_of() {
|
|
|
76
76
|
# Count body lines after the frontmatter close (Q-LEN = "lines after frontmatter").
|
|
77
77
|
body_lines() { body_of "$1" | wc -l | tr -d ' '; }
|
|
78
78
|
|
|
79
|
-
# x-grounded-paths array length
|
|
79
|
+
# x-grounded-paths array length — inline `[a, b]` or YAML block list (via _lib.sh).
|
|
80
80
|
grounded_count() {
|
|
81
|
-
|
|
82
|
-
arr="$(grep -m1 -E '^x-grounded-paths:' "$1" 2>/dev/null || true)"
|
|
83
|
-
[[ -z "$arr" ]] && { echo 0; return; }
|
|
84
|
-
arr="${arr#*[}"; arr="${arr%]*}"
|
|
85
|
-
arr="$(printf '%s' "$arr" | tr -d ' ')"
|
|
86
|
-
[[ -z "$arr" ]] && { echo 0; return; }
|
|
87
|
-
awk -F',' '{print NF}' <<< "$arr"
|
|
81
|
+
grounded_paths_count "$1"
|
|
88
82
|
}
|
|
89
83
|
|
|
90
84
|
has_section() { grep -qE "^##[[:space:]]+$1([[:space:]]|$)" "$2"; }
|
|
@@ -66,7 +66,7 @@ EOF
|
|
|
66
66
|
|
|
67
67
|
while [[ $# -gt 0 ]]; do
|
|
68
68
|
case "$1" in
|
|
69
|
-
--path-index) PATH_INDEX="$2"; shift 2;;
|
|
69
|
+
--path-index) PATH_INDEX="${2:?--path-index requires a value}"; shift 2;;
|
|
70
70
|
--reverse) REVERSE=1; shift;;
|
|
71
71
|
--structure-only) STRUCTURE_ONLY=1; shift;;
|
|
72
72
|
--json) JSON=1; shift;;
|
|
@@ -135,18 +135,25 @@ while IFS= read -r -d '' page; do
|
|
|
135
135
|
rel="${page#"$BUNDLE/"}"
|
|
136
136
|
base="$(basename "$rel")"
|
|
137
137
|
|
|
138
|
+
# Meta pages are NOT concepts: reserved index.md/log.md (OKF §6/§7) carry no
|
|
139
|
+
# concept frontmatter, and the tool-generated coverage page uses a descriptive
|
|
140
|
+
# (non-frozen) type. Exempt them from the frozen-vocab + required-field + body
|
|
141
|
+
# checks, and don't count them as concepts. The template-token (§3b) and
|
|
142
|
+
# cross-link (§4) scans below still cover them.
|
|
143
|
+
if is_meta_page "$base" "$page"; then
|
|
144
|
+
continue
|
|
145
|
+
fi
|
|
146
|
+
|
|
138
147
|
type_val="$(get_yaml_field "$page" "type")"
|
|
139
148
|
|
|
140
149
|
# Empty / placeholder pages: a non-meta page with no type (or no body) slips
|
|
141
150
|
# past every downstream check (quality + coverage both key on `type`). Catch
|
|
142
151
|
# it here so a blank or stub file can never ship.
|
|
143
152
|
if [[ -z "$type_val" ]]; then
|
|
144
|
-
if
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
add_error "$rel: untyped page (missing frontmatter 'type:') — not a valid concept page"
|
|
149
|
-
fi
|
|
153
|
+
if [[ "$(nonblank_body_lines "$page")" -eq 0 ]]; then
|
|
154
|
+
add_error "$rel: empty page (no frontmatter type, no body) — every wiki page must be a real concept"
|
|
155
|
+
else
|
|
156
|
+
add_error "$rel: untyped page (missing frontmatter 'type:') — not a valid concept page"
|
|
150
157
|
fi
|
|
151
158
|
continue
|
|
152
159
|
fi
|
|
@@ -230,12 +237,14 @@ if [[ -n "$PATH_INDEX" ]]; then
|
|
|
230
237
|
# themselves end in .md (e.g. grounding to docs/INVARIANTS.md) — those are
|
|
231
238
|
# not bundle pages, so we extract strings *inside* the [ ... ] value arrays
|
|
232
239
|
# and ignore keys entirely. Each page must exist in the bundle.
|
|
240
|
+
# Flatten first: the index may be pretty-printed, so value arrays can
|
|
241
|
+
# span lines and a line-oriented match would silently skip them.
|
|
233
242
|
while IFS= read -r ref; do
|
|
234
243
|
[[ -z "$ref" ]] && continue
|
|
235
244
|
if [[ ! -f "$BUNDLE/$ref" ]]; then
|
|
236
245
|
add_error "path-index references missing concept page: $ref"
|
|
237
246
|
fi
|
|
238
|
-
done < <(
|
|
247
|
+
done < <(tr '\n' ' ' < "$PATH_INDEX" 2>/dev/null | grep -oE '\[[^]]*\]' \
|
|
239
248
|
| grep -oE '"[^"]+\.md"' | tr -d '"' | sort -u)
|
|
240
249
|
fi
|
|
241
250
|
fi
|
|
@@ -244,9 +253,10 @@ fi
|
|
|
244
253
|
# Forward proves the index doesn't name ghosts; reverse proves no page is an
|
|
245
254
|
# orphan with no source mapping (a symptom of hand-written / off-plan pages).
|
|
246
255
|
if [[ $REVERSE -eq 1 && $STRUCTURE_ONLY -eq 0 && -n "$PATH_INDEX" && -f "$PATH_INDEX" ]]; then
|
|
247
|
-
# All pages the index maps to (its array values).
|
|
256
|
+
# All pages the index maps to (its array values). Flattened for the same
|
|
257
|
+
# pretty-printed-array reason as the forward check above.
|
|
248
258
|
INDEXED_FILE="$(mktemp)"
|
|
249
|
-
|
|
259
|
+
tr '\n' ' ' < "$PATH_INDEX" 2>/dev/null | grep -oE '\[[^]]*\]' \
|
|
250
260
|
| grep -oE '"[^"]+\.md"' | tr -d '"' | sort -u > "$INDEXED_FILE" || true
|
|
251
261
|
while IFS= read -r -d '' page; do
|
|
252
262
|
rel="${page#"$BUNDLE/"}"
|
|
@@ -49,10 +49,10 @@ EOF
|
|
|
49
49
|
|
|
50
50
|
while [[ $# -gt 0 ]]; do
|
|
51
51
|
case "$1" in
|
|
52
|
-
--since) SINCE="$2"; shift 2;;
|
|
53
|
-
--limit) LIMIT="$2"; shift 2;;
|
|
54
|
-
--scope-pattern) SCOPE_PATTERN="$2"; shift 2;;
|
|
55
|
-
--branch) BRANCH="$2"; shift 2;;
|
|
52
|
+
--since) SINCE="${2:?--since requires a value}"; shift 2;;
|
|
53
|
+
--limit) LIMIT="${2:?--limit requires a value}"; shift 2;;
|
|
54
|
+
--scope-pattern) SCOPE_PATTERN="${2:?--scope-pattern requires a value}"; shift 2;;
|
|
55
|
+
--branch) BRANCH="${2:?--branch requires a value}"; shift 2;;
|
|
56
56
|
--help|-h) usage; exit 0;;
|
|
57
57
|
*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
|
|
58
58
|
esac
|