@drafthq/draft 3.5.3 → 3.7.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/README.md +28 -49
- package/bin/README.md +47 -1
- package/cli/src/hosts/cursor.js +3 -0
- package/cli/src/installer.js +8 -4
- package/cli/src/lib/cursor-registry.js +10 -0
- package/cli/src/lib/fsx.js +3 -0
- package/cli/src/lib/marker.js +8 -3
- package/core/agents/architect.md +21 -9
- package/core/agents/debugger.md +5 -0
- package/core/agents/ops.md +7 -2
- package/core/agents/planner.md +7 -0
- package/core/agents/rca.md +14 -3
- package/core/agents/reviewer.md +1 -0
- package/core/agents/writer.md +6 -0
- package/core/knowledge-base.md +22 -3
- package/core/methodology.md +30 -5
- package/core/shared/condensation.md +28 -18
- package/core/shared/context-verify.md +3 -1
- package/core/shared/cross-skill-dispatch.md +7 -3
- package/core/shared/draft-context-loading.md +3 -0
- package/core/shared/git-report-metadata.md +2 -1
- package/core/shared/graph-query.md +24 -26
- package/core/shared/graph-usage-report.md +1 -1
- package/core/shared/jira-sync.md +10 -6
- package/core/shared/okf-retrieval.md +1 -1
- package/core/shared/parallel-analysis.md +14 -7
- package/core/shared/pattern-learning.md +2 -0
- package/core/shared/template-contract.md +1 -1
- package/core/shared/tool-resolver.md +20 -15
- package/core/shared/vcs-commands.md +5 -2
- package/core/templates/ai-context.md +3 -2
- package/core/templates/ai-profile.md +7 -0
- package/core/templates/architecture.md +6 -2
- package/core/templates/discovery.md +1 -1
- package/core/templates/guardrails.md +9 -0
- package/core/templates/hld.md +5 -4
- package/core/templates/intake-questions.md +45 -0
- package/core/templates/jira.md +11 -2
- package/core/templates/lld.md +8 -5
- package/core/templates/okf/concept.md +0 -1
- package/core/templates/plan.md +1 -1
- package/core/templates/product.md +11 -2
- package/core/templates/rca.md +6 -0
- package/core/templates/service-index.md +1 -0
- package/core/templates/spec.md +15 -0
- package/core/templates/tech-stack.md +12 -0
- package/core/templates/workflow.md +20 -0
- package/integrations/agents/AGENTS.md +1449 -817
- package/integrations/copilot/.github/copilot-instructions.md +1449 -817
- package/package.json +1 -1
- package/scripts/fetch-memory-engine.sh +25 -7
- package/scripts/lib.sh +90 -6
- package/scripts/tools/_graph_queries.sh +22 -8
- package/scripts/tools/_lib.sh +81 -11
- package/scripts/tools/adr-index.sh +1 -1
- package/scripts/tools/check-repo-size.sh +125 -0
- package/scripts/tools/check-skill-line-caps.sh +3 -4
- package/scripts/tools/check-template-noop.sh +0 -1
- package/scripts/tools/check-track-hygiene.sh +41 -21
- package/scripts/tools/classify-files.sh +3 -3
- package/scripts/tools/cycle-detect.sh +18 -17
- package/scripts/tools/detect-test-framework.sh +1 -1
- package/scripts/tools/emit-skill-metrics.sh +7 -2
- package/scripts/tools/fix-whitespace.sh +3 -0
- package/scripts/tools/freshness-check.sh +2 -2
- package/scripts/tools/git-metadata.sh +12 -6
- package/scripts/tools/graph-arch.sh +5 -11
- package/scripts/tools/graph-callers.sh +10 -15
- 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 +21 -19
- package/scripts/tools/graph-init.sh +13 -9
- package/scripts/tools/graph-preflight.sh +4 -1
- package/scripts/tools/graph-query.sh +74 -18
- package/scripts/tools/graph-risk.sh +3 -10
- package/scripts/tools/graph-search.sh +4 -11
- package/scripts/tools/graph-snapshot.sh +38 -9
- 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 +6 -12
- package/scripts/tools/install-smoke-test.sh +223 -0
- package/scripts/tools/mermaid-from-graph.sh +5 -11
- package/scripts/tools/migrate-track-frontmatter.sh +11 -10
- package/scripts/tools/okf-coverage-check.sh +8 -7
- package/scripts/tools/okf-emit-catalog.sh +256 -0
- package/scripts/tools/okf-fix-links.sh +251 -0
- package/scripts/tools/okf-plan-concepts.sh +242 -14
- package/scripts/tools/okf-render-views.sh +63 -21
- package/scripts/tools/okf-validate-all.sh +20 -9
- package/scripts/tools/okf-validate-quality.sh +15 -10
- package/scripts/tools/okf-validate.sh +20 -6
- package/scripts/tools/parse-git-log.sh +23 -5
- package/scripts/tools/parse-reports.sh +1 -1
- package/scripts/tools/render-track.sh +1 -1
- package/scripts/tools/resolve-tools.sh +24 -8
- 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 +3 -4
- package/scripts/tools/verify-doc-anchors.sh +14 -34
- package/scripts/tools/verify-graph-binary.sh +15 -5
- package/skills/GRAPH.md +21 -10
- package/skills/adr/SKILL.md +25 -7
- package/skills/assist-review/SKILL.md +2 -1
- package/skills/bughunt/SKILL.md +55 -12
- package/skills/bughunt/references/regression-tests.md +14 -4
- package/skills/change/SKILL.md +19 -9
- package/skills/coverage/SKILL.md +28 -12
- package/skills/debug/SKILL.md +13 -3
- package/skills/decompose/SKILL.md +42 -12
- package/skills/deep-review/SKILL.md +19 -6
- package/skills/deploy-checklist/SKILL.md +10 -3
- package/skills/documentation/SKILL.md +12 -1
- package/skills/draft/SKILL.md +40 -29
- package/skills/draft/context-files.md +8 -1
- package/skills/draft/quality-guide.md +10 -2
- package/skills/graph/SKILL.md +2 -2
- package/skills/impact/SKILL.md +4 -2
- package/skills/implement/SKILL.md +62 -20
- package/skills/incident-response/SKILL.md +5 -2
- package/skills/init/SKILL.md +112 -30
- package/skills/init/references/architecture-spec.md +32 -8
- package/skills/init/references/okf-emitter.md +42 -78
- package/skills/jira/SKILL.md +33 -13
- package/skills/jira/references/review.md +61 -19
- package/skills/learn/SKILL.md +30 -7
- package/skills/new-track/SKILL.md +73 -18
- package/skills/quick-review/SKILL.md +8 -3
- package/skills/revert/SKILL.md +11 -5
- package/skills/review/SKILL.md +126 -27
- package/skills/review/references/zero-setup-mode.md +117 -0
- package/skills/standup/SKILL.md +10 -2
- package/skills/status/SKILL.md +7 -7
- package/skills/tech-debt/SKILL.md +9 -3
- package/skills/testing-strategy/SKILL.md +10 -2
- package/skills/tour/SKILL.md +2 -1
- package/skills/upload/SKILL.md +4 -3
- 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[[:space:]]*=' "$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,27 @@ 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
|
+
plan_from_graph || true
|
|
428
|
+
# Last resort only: any of cargo/npm/go/graph succeeding leaves E_ID non-empty
|
|
429
|
+
# and skips this. Re-running the heuristic a second time can never add what
|
|
430
|
+
# the first pass didn't find, so there is one call site, not two.
|
|
431
|
+
if [[ ${#E_ID[@]} -eq 0 ]]; then
|
|
432
|
+
plan_from_heuristic
|
|
433
|
+
DISCOVERY_META+=("heuristic")
|
|
434
|
+
fi
|
|
228
435
|
fi
|
|
229
436
|
|
|
230
437
|
if [[ ${#E_ID[@]} -eq 0 ]]; then
|
|
@@ -232,6 +439,19 @@ if [[ ${#E_ID[@]} -eq 0 ]]; then
|
|
|
232
439
|
exit 2
|
|
233
440
|
fi
|
|
234
441
|
|
|
442
|
+
# Compose SOURCE label from discovery meta
|
|
443
|
+
if [[ ${#DISCOVERY_META[@]} -gt 0 ]]; then
|
|
444
|
+
# de-dupe meta labels
|
|
445
|
+
SOURCE="$(printf '%s\n' "${DISCOVERY_META[@]}" | awk '!s[$0]++' | paste -sd+ -)"
|
|
446
|
+
[[ -z "$SOURCE" ]] && SOURCE="heuristic"
|
|
447
|
+
# degraded only when pure heuristic with no language/graph
|
|
448
|
+
if [[ "$SOURCE" == "heuristic" ]]; then
|
|
449
|
+
DEGRADED="true"
|
|
450
|
+
else
|
|
451
|
+
DEGRADED="false"
|
|
452
|
+
fi
|
|
453
|
+
fi
|
|
454
|
+
|
|
235
455
|
# Required-first, then deferred; stable within group (topological-ish: high fan-in
|
|
236
456
|
# subsystems first so forward cross-links resolve during generation).
|
|
237
457
|
emit_plan() {
|
|
@@ -257,6 +477,14 @@ emit_plan() {
|
|
|
257
477
|
printf ' "source": "%s",\n' "$SOURCE"
|
|
258
478
|
printf ' "degraded": %s,\n' "$DEGRADED"
|
|
259
479
|
printf ' "min_fan_in": %d,\n' "$MIN_FAN_IN"
|
|
480
|
+
printf ' "discovery": ['
|
|
481
|
+
local di first_d=1
|
|
482
|
+
for di in "${DISCOVERY_META[@]:-}"; do
|
|
483
|
+
[[ -z "$di" ]] && continue
|
|
484
|
+
[[ $first_d -eq 1 ]] && first_d=0 || printf ','
|
|
485
|
+
printf '"%s"' "$(json_escape "$di")"
|
|
486
|
+
done
|
|
487
|
+
printf '],\n'
|
|
260
488
|
# generated_order
|
|
261
489
|
printf ' "generated_order": ['
|
|
262
490
|
local first=1
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# okf-render-views.sh — render the demoted views from an OKF taxonomy bundle.
|
|
3
3
|
#
|
|
4
|
-
# The wiki/ bundle is the source of truth. This produces the
|
|
4
|
+
# The wiki/ bundle is the source of truth. This produces the derived,
|
|
5
5
|
# human-facing views deterministically (so they never drift from the bundle and
|
|
6
6
|
# carry zero extra maintenance):
|
|
7
7
|
# 1. architecture.md — a single linear concatenation of every concept page,
|
|
@@ -24,6 +24,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
24
24
|
# shellcheck source=scripts/tools/_lib.sh
|
|
25
25
|
source "$SCRIPT_DIR/_lib.sh"
|
|
26
26
|
|
|
27
|
+
command -v python3 >/dev/null 2>&1 || {
|
|
28
|
+
echo "ERROR: python3 is required by ${0##*/} (link rewriting in rendered views)" >&2
|
|
29
|
+
exit 2
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
BUNDLE=""
|
|
28
33
|
ARCH_OUT=""
|
|
29
34
|
WEB_OUT=""
|
|
@@ -56,18 +61,18 @@ Flags:
|
|
|
56
61
|
this tool has no clock dependency).
|
|
57
62
|
--help Show this help.
|
|
58
63
|
|
|
59
|
-
Requires jq (
|
|
64
|
+
Requires jq (for --web) and python3 (link rewriting). Exit 0 ok, 1 error, 2 bundle not found.
|
|
60
65
|
EOF
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
while [[ $# -gt 0 ]]; do
|
|
64
69
|
case "$1" in
|
|
65
|
-
--arch-out) ARCH_OUT="$2"; shift 2;;
|
|
70
|
+
--arch-out) ARCH_OUT="${2:?--arch-out requires a value}"; shift 2;;
|
|
66
71
|
--concept-map-into) CMAP_INTO+=("$2"); shift 2;;
|
|
67
72
|
--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;;
|
|
73
|
+
--web) WEB_OUT="${2:?--web requires a value}"; shift 2;;
|
|
74
|
+
--coverage-report) COVERAGE_REPORT="${2:?--coverage-report requires a value}"; shift 2;;
|
|
75
|
+
--validated-at) VALIDATED_AT="${2:?--validated-at requires a value}"; shift 2;;
|
|
71
76
|
--help|-h) usage; exit 0;;
|
|
72
77
|
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
|
|
73
78
|
*)
|
|
@@ -100,6 +105,37 @@ ordered_pages() {
|
|
|
100
105
|
done
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
# Rewrite markdown links so concatenated architecture.md (at draft/) resolves them.
|
|
109
|
+
# stdin body; $1 = bundle-relative path e.g. systems/foo.md
|
|
110
|
+
rewrite_body_links() {
|
|
111
|
+
local rel="${1:-}"
|
|
112
|
+
local sec="${rel%%/*}"
|
|
113
|
+
python3 -c '
|
|
114
|
+
import sys, re
|
|
115
|
+
sec = sys.argv[1]
|
|
116
|
+
text = sys.stdin.read()
|
|
117
|
+
sections = ("systems", "features", "overview", "entrypoints", "reference")
|
|
118
|
+
def repl(m):
|
|
119
|
+
label, href = m.group(1), m.group(2).strip()
|
|
120
|
+
if href.startswith(("http://", "https://", "mailto:", "#", "wiki/")):
|
|
121
|
+
return m.group(0)
|
|
122
|
+
if "::" in href and not href.startswith("../"):
|
|
123
|
+
return "`" + label + "`"
|
|
124
|
+
path, _, frag = href.partition("#")
|
|
125
|
+
frag_s = "#" + frag if frag else ""
|
|
126
|
+
if path.startswith("../"):
|
|
127
|
+
rest = path[3:]
|
|
128
|
+
if rest.split("/")[0] in sections:
|
|
129
|
+
return "[%s](wiki/%s%s)" % (label, rest, frag_s)
|
|
130
|
+
if any(path.startswith(s + "/") for s in sections):
|
|
131
|
+
return "[%s](wiki/%s%s)" % (label, path, frag_s)
|
|
132
|
+
if path.endswith(".md") and "/" not in path and sec in sections:
|
|
133
|
+
return "[%s](wiki/%s/%s%s)" % (label, sec, path, frag_s)
|
|
134
|
+
return m.group(0)
|
|
135
|
+
sys.stdout.write(re.sub(r"\[([^\]]*)\]\(([^)]+)\)", repl, text))
|
|
136
|
+
' "$sec"
|
|
137
|
+
}
|
|
138
|
+
|
|
103
139
|
# Strip YAML frontmatter from a page (leading --- ... --- block on line 1).
|
|
104
140
|
strip_frontmatter() {
|
|
105
141
|
awk '
|
|
@@ -172,8 +208,7 @@ render_architecture() {
|
|
|
172
208
|
fi
|
|
173
209
|
title="$(page_title "$BUNDLE/$rel")"
|
|
174
210
|
[[ -n "$title" ]] || title="$rel"
|
|
175
|
-
local anchor; anchor="$(
|
|
176
|
-
anchor="${anchor#-}"; anchor="${anchor%-}"
|
|
211
|
+
local anchor; anchor="$(gfm_slug "$title")"
|
|
177
212
|
echo " - [${title}](#${anchor})"
|
|
178
213
|
done < <(ordered_pages)
|
|
179
214
|
echo ""
|
|
@@ -183,10 +218,17 @@ render_architecture() {
|
|
|
183
218
|
echo ""
|
|
184
219
|
echo "---"
|
|
185
220
|
echo ""
|
|
186
|
-
strip_frontmatter "$BUNDLE/$rel"
|
|
221
|
+
strip_frontmatter "$BUNDLE/$rel" | rewrite_body_links "$rel"
|
|
187
222
|
done < <(ordered_pages)
|
|
188
223
|
} >"$tmp"
|
|
224
|
+
apply_dest_mode "$tmp" "$out" # mktemp is 0600; mv would strip the dest's mode
|
|
189
225
|
mv "$tmp" "$out"
|
|
226
|
+
if [[ -x "$SCRIPT_DIR/okf-fix-links.sh" ]]; then
|
|
227
|
+
draft_dir="$(cd "$(dirname "$out")" && pwd)"
|
|
228
|
+
if [[ -d "$draft_dir/wiki" || "$(basename "$BUNDLE")" == "wiki" ]]; then
|
|
229
|
+
"$SCRIPT_DIR/okf-fix-links.sh" --file "$out" --wiki "$BUNDLE" --fix >/dev/null 2>&1 || true
|
|
230
|
+
fi
|
|
231
|
+
fi
|
|
190
232
|
echo "rendered architecture view → $out ($(ordered_pages | grep -c . ) pages)"
|
|
191
233
|
}
|
|
192
234
|
|
|
@@ -206,12 +248,7 @@ build_concept_map() {
|
|
|
206
248
|
[[ -n "$type" ]] || continue
|
|
207
249
|
title="$(get_yaml_field "$page" title)"
|
|
208
250
|
[[ -n "$title" ]] || title="$rel"
|
|
209
|
-
|
|
210
|
-
desc="$(awk '
|
|
211
|
-
NR==1&&/^---$/{fm=1;next} fm&&/^---$/{exit}
|
|
212
|
-
fm && /^description:/ { collect=1; sub(/^description:[[:space:]]*>?[[:space:]]*/,""); if($0!=""){print; exit} next }
|
|
213
|
-
fm && collect { sub(/^[[:space:]]+/,""); if($0!=""){print; exit} }
|
|
214
|
-
' "$page")"
|
|
251
|
+
desc="$(page_desc "$page")"
|
|
215
252
|
echo "| [${title}](${rel}) | ${type} | ${desc} |"
|
|
216
253
|
done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
|
|
217
254
|
}
|
|
@@ -276,14 +313,18 @@ inject_concept_map() {
|
|
|
276
313
|
/<!-- CONCEPT-MAP:END -->/ { skip=0 }
|
|
277
314
|
!skip { print }
|
|
278
315
|
' "$target" >"$tmp"
|
|
316
|
+
apply_dest_mode "$tmp" "$target" # mktemp is 0600; mv would strip the dest's mode
|
|
279
317
|
mv "$tmp" "$target"
|
|
280
318
|
echo "injected Concept Map → $target"
|
|
281
319
|
}
|
|
282
320
|
|
|
283
321
|
# --- 3. Render a self-contained offline HTML viewer (single file) ---
|
|
284
322
|
# All pages are inlined as JSON; a small built-in markdown renderer draws them in
|
|
285
|
-
# the browser. No server, no internet, no CDN. jq encodes page content safely
|
|
286
|
-
#
|
|
323
|
+
# the browser. No server, no internet, no CDN. jq encodes page content safely,
|
|
324
|
+
# and the whole emitted data block is passed through a `</` → `<\/` filter: JSON
|
|
325
|
+
# escaping does NOT stop a literal "</script>" from closing the <script> element,
|
|
326
|
+
# and page titles/types/paths come from repo-derived frontmatter, so every field
|
|
327
|
+
# has to be neutralized, not just the markdown body.
|
|
287
328
|
render_web() {
|
|
288
329
|
local out="$1"
|
|
289
330
|
command -v jq >/dev/null 2>&1 || { echo "ERROR: --web requires jq" >&2; return 1; }
|
|
@@ -351,14 +392,14 @@ HTML_HEAD
|
|
|
351
392
|
"$(jq -Rn --arg v "$rel" '$v')" \
|
|
352
393
|
"$(jq -Rn --arg v "$title" '$v')" \
|
|
353
394
|
"$(jq -Rn --arg v "$type" '$v')" \
|
|
354
|
-
"$(strip_frontmatter "$page" | jq -Rs .
|
|
395
|
+
"$(strip_frontmatter "$page" | jq -Rs .)"
|
|
355
396
|
done < <(find "$BUNDLE" -type f -name '*.md' -print0 | sort -z)
|
|
356
397
|
echo "};"
|
|
357
398
|
# ORDER: bundle root index.md first, then everything else sorted.
|
|
358
399
|
echo "const ORDER = Object.keys(PAGES).sort(function(a,b){"
|
|
359
400
|
echo " if(a==='index.md') return -1; if(b==='index.md') return 1;"
|
|
360
401
|
echo " return a<b?-1:a>b?1:0; });"
|
|
361
|
-
} >>"$tmp"
|
|
402
|
+
} | sed 's#</#<\\/#g' >>"$tmp"
|
|
362
403
|
|
|
363
404
|
cat >>"$tmp" <<'HTML_TAIL'
|
|
364
405
|
function esc(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
@@ -387,7 +428,7 @@ function render(md, base){
|
|
|
387
428
|
var blocks=[], src=md.replace(/```(\w*)\n([\s\S]*?)```/g,function(m,lang,body){
|
|
388
429
|
var cls = lang==='mermaid' ? ' class="mermaid-src"' : '';
|
|
389
430
|
blocks.push('<pre'+cls+'><code>'+esc(body.replace(/\n$/,''))+'</code></pre>');
|
|
390
|
-
return '
|
|
431
|
+
return 'BLOCK'+(blocks.length-1)+'';
|
|
391
432
|
});
|
|
392
433
|
var lines=src.split('\n'), out='', i=0, list='', tbl=[];
|
|
393
434
|
function closeList(){ if(list){ out+='</'+list+'>'; list=''; } }
|
|
@@ -410,7 +451,7 @@ function render(md, base){
|
|
|
410
451
|
if(/^\s*>\s?/.test(ln)){ closeList(); out+='<blockquote>'+inline(esc(ln.replace(/^\s*>\s?/,'')),base)+'</blockquote>'; continue; }
|
|
411
452
|
var li=ln.match(/^\s*([-*]|\d+\.)\s+(.*)$/);
|
|
412
453
|
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(
|
|
454
|
+
var b=ln.match(/^BLOCK(\d+)$/);
|
|
414
455
|
if(b){ closeList(); out+=blocks[+b[1]]; continue; }
|
|
415
456
|
if(/^\s*$/.test(ln)){ closeList(); continue; }
|
|
416
457
|
closeList(); out+='<p>'+inline(esc(ln),base)+'</p>';
|
|
@@ -450,6 +491,7 @@ show(decodeURIComponent(location.hash.slice(1)) || ORDER[0]);
|
|
|
450
491
|
HTML_TAIL
|
|
451
492
|
|
|
452
493
|
mkdir -p "$(dirname "$out")"
|
|
494
|
+
apply_dest_mode "$tmp" "$out" # mktemp is 0600; mv would strip the dest's mode
|
|
453
495
|
mv "$tmp" "$out"
|
|
454
496
|
echo "rendered offline HTML viewer → $out ($(find "$BUNDLE" -type f -name '*.md' | grep -c .) pages)"
|
|
455
497
|
}
|
|
@@ -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
|
|
|
@@ -73,18 +73,23 @@ body_of() {
|
|
|
73
73
|
awk 'NR==1&&/^---$/{fm=1;next} fm&&/^---$/{fm=0;next} !fm{print}' "$1"
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
# Body text the Q-TEMPLATE scan is allowed to see: fenced code blocks and
|
|
77
|
+
# shell-style ${VAR} expansions removed. Both legitimately carry {ALL_CAPS}
|
|
78
|
+
# spans that are not unreplaced placeholders (a page quoting `${HOME}` or a CI
|
|
79
|
+
# snippet would otherwise fail).
|
|
80
|
+
template_scan_body() {
|
|
81
|
+
body_of "$1" | awk '
|
|
82
|
+
/^[[:space:]]*```/ { fence = !fence; next }
|
|
83
|
+
!fence { gsub(/\$\{[A-Za-z_][A-Za-z0-9_]*\}/, ""); print }
|
|
84
|
+
'
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
# Count body lines after the frontmatter close (Q-LEN = "lines after frontmatter").
|
|
77
88
|
body_lines() { body_of "$1" | wc -l | tr -d ' '; }
|
|
78
89
|
|
|
79
|
-
# x-grounded-paths array length
|
|
90
|
+
# x-grounded-paths array length — inline `[a, b]` or YAML block list (via _lib.sh).
|
|
80
91
|
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"
|
|
92
|
+
grounded_paths_count "$1"
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
has_section() { grep -qE "^##[[:space:]]+$1([[:space:]]|$)" "$2"; }
|
|
@@ -120,7 +125,7 @@ mermaid_lint() {
|
|
|
120
125
|
echo "unicode arrow in mermaid (use --> not →)"; return 1
|
|
121
126
|
fi
|
|
122
127
|
# '&' node chaining (common breaker).
|
|
123
|
-
if printf '%s' "$blocks" | grep -qE '[A-Za-z0-9_]
|
|
128
|
+
if printf '%s' "$blocks" | grep -qE '[A-Za-z0-9_][[:space:]]*&[[:space:]]*[A-Za-z0-9_]'; then
|
|
124
129
|
echo "'&' node chaining in mermaid"; return 1
|
|
125
130
|
fi
|
|
126
131
|
# Reserved bareword node ids.
|
|
@@ -241,7 +246,7 @@ while IFS= read -r -d '' page; do
|
|
|
241
246
|
fi
|
|
242
247
|
|
|
243
248
|
# Q-TEMPLATE: unreplaced {TOKEN} placeholders.
|
|
244
|
-
if
|
|
249
|
+
if template_scan_body "$page" | grep -qE "$TOKEN_RE"; then
|
|
245
250
|
fail "$rel" "Q-TEMPLATE" "unreplaced template token {PLACEHOLDER}"
|
|
246
251
|
fi
|
|
247
252
|
|