@drafthq/draft 3.5.3 → 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/plan.md +1 -1
- package/integrations/agents/AGENTS.md +122 -470
- package/integrations/copilot/.github/copilot-instructions.md +122 -470
- 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 +4 -4
- 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 +46 -9
- package/scripts/tools/okf-validate-all.sh +20 -9
- package/scripts/tools/okf-validate-quality.sh +2 -8
- package/scripts/tools/okf-validate.sh +7 -4
- 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 +39 -75
- 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 }
|
|
@@ -172,8 +204,7 @@ render_architecture() {
|
|
|
172
204
|
fi
|
|
173
205
|
title="$(page_title "$BUNDLE/$rel")"
|
|
174
206
|
[[ -n "$title" ]] || title="$rel"
|
|
175
|
-
local anchor; anchor="$(
|
|
176
|
-
anchor="${anchor#-}"; anchor="${anchor%-}"
|
|
207
|
+
local anchor; anchor="$(gfm_slug "$title")"
|
|
177
208
|
echo " - [${title}](#${anchor})"
|
|
178
209
|
done < <(ordered_pages)
|
|
179
210
|
echo ""
|
|
@@ -183,10 +214,16 @@ render_architecture() {
|
|
|
183
214
|
echo ""
|
|
184
215
|
echo "---"
|
|
185
216
|
echo ""
|
|
186
|
-
strip_frontmatter "$BUNDLE/$rel"
|
|
217
|
+
strip_frontmatter "$BUNDLE/$rel" | rewrite_body_links "$rel"
|
|
187
218
|
done < <(ordered_pages)
|
|
188
219
|
} >"$tmp"
|
|
189
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
|
|
190
227
|
echo "rendered architecture view → $out ($(ordered_pages | grep -c . ) pages)"
|
|
191
228
|
}
|
|
192
229
|
|
|
@@ -387,7 +424,7 @@ function render(md, base){
|
|
|
387
424
|
var blocks=[], src=md.replace(/```(\w*)\n([\s\S]*?)```/g,function(m,lang,body){
|
|
388
425
|
var cls = lang==='mermaid' ? ' class="mermaid-src"' : '';
|
|
389
426
|
blocks.push('<pre'+cls+'><code>'+esc(body.replace(/\n$/,''))+'</code></pre>');
|
|
390
|
-
return '
|
|
427
|
+
return 'BLOCK'+(blocks.length-1)+'';
|
|
391
428
|
});
|
|
392
429
|
var lines=src.split('\n'), out='', i=0, list='', tbl=[];
|
|
393
430
|
function closeList(){ if(list){ out+='</'+list+'>'; list=''; } }
|
|
@@ -410,7 +447,7 @@ function render(md, base){
|
|
|
410
447
|
if(/^\s*>\s?/.test(ln)){ closeList(); out+='<blockquote>'+inline(esc(ln.replace(/^\s*>\s?/,'')),base)+'</blockquote>'; continue; }
|
|
411
448
|
var li=ln.match(/^\s*([-*]|\d+\.)\s+(.*)$/);
|
|
412
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; }
|
|
413
|
-
var b=ln.match(
|
|
450
|
+
var b=ln.match(/^BLOCK(\d+)$/);
|
|
414
451
|
if(b){ closeList(); out+=blocks[+b[1]]; continue; }
|
|
415
452
|
if(/^\s*$/.test(ln)){ closeList(); continue; }
|
|
416
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;;
|
|
@@ -237,12 +237,14 @@ if [[ -n "$PATH_INDEX" ]]; then
|
|
|
237
237
|
# themselves end in .md (e.g. grounding to docs/INVARIANTS.md) — those are
|
|
238
238
|
# not bundle pages, so we extract strings *inside* the [ ... ] value arrays
|
|
239
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.
|
|
240
242
|
while IFS= read -r ref; do
|
|
241
243
|
[[ -z "$ref" ]] && continue
|
|
242
244
|
if [[ ! -f "$BUNDLE/$ref" ]]; then
|
|
243
245
|
add_error "path-index references missing concept page: $ref"
|
|
244
246
|
fi
|
|
245
|
-
done < <(
|
|
247
|
+
done < <(tr '\n' ' ' < "$PATH_INDEX" 2>/dev/null | grep -oE '\[[^]]*\]' \
|
|
246
248
|
| grep -oE '"[^"]+\.md"' | tr -d '"' | sort -u)
|
|
247
249
|
fi
|
|
248
250
|
fi
|
|
@@ -251,9 +253,10 @@ fi
|
|
|
251
253
|
# Forward proves the index doesn't name ghosts; reverse proves no page is an
|
|
252
254
|
# orphan with no source mapping (a symptom of hand-written / off-plan pages).
|
|
253
255
|
if [[ $REVERSE -eq 1 && $STRUCTURE_ONLY -eq 0 && -n "$PATH_INDEX" && -f "$PATH_INDEX" ]]; then
|
|
254
|
-
# 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.
|
|
255
258
|
INDEXED_FILE="$(mktemp)"
|
|
256
|
-
|
|
259
|
+
tr '\n' ' ' < "$PATH_INDEX" 2>/dev/null | grep -oE '\[[^]]*\]' \
|
|
257
260
|
| grep -oE '"[^"]+\.md"' | tr -d '"' | sort -u > "$INDEXED_FILE" || true
|
|
258
261
|
while IFS= read -r -d '' page; do
|
|
259
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
|
|
@@ -48,7 +48,7 @@ TO_STDOUT=0
|
|
|
48
48
|
while (($#)); do
|
|
49
49
|
case "$1" in
|
|
50
50
|
-h|--help) USAGE_HELP_MODE=1 usage ;;
|
|
51
|
-
--out) OUT_PATH="$2"; shift 2 ;;
|
|
51
|
+
--out) OUT_PATH="${2:?--out requires a value}"; shift 2 ;;
|
|
52
52
|
--stdout) TO_STDOUT=1; shift ;;
|
|
53
53
|
-*) printf 'Unknown flag: %s\n' "$1" >&2; usage ;;
|
|
54
54
|
*) TRACK_DIR="$1"; shift ;;
|
|
@@ -51,7 +51,7 @@ EOF
|
|
|
51
51
|
|
|
52
52
|
while [[ $# -gt 0 ]]; do
|
|
53
53
|
case "$1" in
|
|
54
|
-
--path) COVERAGE_PATH="$2"; shift 2;;
|
|
54
|
+
--path) COVERAGE_PATH="${2:?--path requires a value}"; shift 2;;
|
|
55
55
|
--schema-check) SCHEMA_CHECK="true"; shift;;
|
|
56
56
|
--help|-h) usage; exit 0;;
|
|
57
57
|
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
#
|
|
7
7
|
# Usage:
|
|
8
8
|
# scripts/tools/scan-markers.sh [--root DIR] [--markers LIST]
|
|
9
|
-
# [--min-age-days N]
|
|
9
|
+
# [--min-age-days N]
|
|
10
10
|
#
|
|
11
11
|
# Exit codes: 0 OK (even with zero hits), 1 invocation error, 2 not a git repo
|
|
12
12
|
# (emits [] on stdout so consumers can still parse).
|
|
@@ -38,9 +38,9 @@ EOF
|
|
|
38
38
|
|
|
39
39
|
while [[ $# -gt 0 ]]; do
|
|
40
40
|
case "$1" in
|
|
41
|
-
--root) ROOT="$2"; shift 2;;
|
|
42
|
-
--markers) MARKERS="$2"; shift 2;;
|
|
43
|
-
--min-age-days) MIN_AGE="$2"; shift 2;;
|
|
41
|
+
--root) ROOT="${2:?--root requires a value}"; shift 2;;
|
|
42
|
+
--markers) MARKERS="${2:?--markers requires a value}"; shift 2;;
|
|
43
|
+
--min-age-days) MIN_AGE="${2:?--min-age-days requires a value}"; shift 2;;
|
|
44
44
|
--help|-h) usage; exit 0;;
|
|
45
45
|
*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
|
|
46
46
|
esac
|
|
@@ -43,8 +43,8 @@ REQUIRED="name,description"
|
|
|
43
43
|
|
|
44
44
|
while [[ $# -gt 0 ]]; do
|
|
45
45
|
case "$1" in
|
|
46
|
-
--require) REQUIRED="$2"; shift 2;;
|
|
47
|
-
--mode) MODE="$2"; shift 2;;
|
|
46
|
+
--require) REQUIRED="${2:?--require requires a value}"; shift 2;;
|
|
47
|
+
--mode) MODE="${2:?--mode requires a value}"; shift 2;;
|
|
48
48
|
--help|-h) usage; exit 0;;
|
|
49
49
|
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
|
|
50
50
|
*)
|
|
@@ -48,7 +48,7 @@ while (($#)); do
|
|
|
48
48
|
case "$1" in
|
|
49
49
|
-h|--help) USAGE_HELP_MODE=1 usage ;;
|
|
50
50
|
--json) EMIT_JSON=1; shift ;;
|
|
51
|
-
--tolerance) TOLERANCE="$2"; shift 2 ;;
|
|
51
|
+
--tolerance) TOLERANCE="${2:?--tolerance requires a value}"; shift 2 ;;
|
|
52
52
|
-*) printf 'Unknown flag: %s\n' "$1" >&2; usage ;;
|
|
53
53
|
*) TRACK_PATHS+=("$1"); shift ;;
|
|
54
54
|
esac
|
|
@@ -83,7 +83,7 @@ extract_citations() {
|
|
|
83
83
|
# bracket expression prematurely terminates a regex literal in
|
|
84
84
|
# POSIX/BSD awk ("nonterminated character class"), though gawk is
|
|
85
85
|
# lenient. The string form is portable across awk implementations.
|
|
86
|
-
while (match(s, "[A-Za-z0-9_][A-Za-z0-9_./-]*\\.[A-Za-z0-9]+:[0-9]+")) {
|
|
86
|
+
while (match(s, "[A-Za-z0-9_][A-Za-z0-9_./-]*\\.[A-Za-z0-9]+:[0-9]+(-[0-9]+)?")) {
|
|
87
87
|
cite = substr(s, RSTART, RLENGTH)
|
|
88
88
|
printf("%s\t%d\t%s\n", FILENAME, NR, cite)
|
|
89
89
|
s = substr(s, RSTART + RLENGTH)
|