@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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# okf-emit-catalog.sh — deterministically emit minimum-viable concept pages for
|
|
3
|
+
# every REQUIRED entry in a concept-plan.json that does not yet have a page.
|
|
4
|
+
#
|
|
5
|
+
# Purpose: XL monorepos cannot rely on the LLM to narrate every Module page in
|
|
6
|
+
# one shot. This tool writes quality-gate-passing catalog pages from plan
|
|
7
|
+
# metadata + optional Cargo/README crumbs so init can complete without gaps.
|
|
8
|
+
# Agents may later enrich top-N hotspots; the catalog is the completeness floor.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# okf-emit-catalog.sh --plan FILE --bundle DIR [--repo DIR] [--force]
|
|
12
|
+
#
|
|
13
|
+
# Exit: 0 ok, 1 error, 2 plan/bundle missing.
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
17
|
+
# shellcheck source=scripts/tools/_lib.sh
|
|
18
|
+
source "$SCRIPT_DIR/_lib.sh"
|
|
19
|
+
|
|
20
|
+
PLAN=""
|
|
21
|
+
BUNDLE=""
|
|
22
|
+
REPO="."
|
|
23
|
+
FORCE=0
|
|
24
|
+
|
|
25
|
+
usage() {
|
|
26
|
+
cat <<'EOF'
|
|
27
|
+
okf-emit-catalog.sh — write minimum-viable concept pages for missing plan entries.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
okf-emit-catalog.sh --plan FILE --bundle DIR [--repo DIR] [--force]
|
|
31
|
+
|
|
32
|
+
Flags:
|
|
33
|
+
--plan FILE concept-plan.json (required)
|
|
34
|
+
--bundle DIR wiki/ bundle directory (required)
|
|
35
|
+
--repo DIR repository root for grounding (default: .)
|
|
36
|
+
--force Overwrite existing pages (default: skip if present)
|
|
37
|
+
--help Show help
|
|
38
|
+
|
|
39
|
+
Exit: 0 ok, 1 error, 2 missing plan/bundle.
|
|
40
|
+
EOF
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
while [[ $# -gt 0 ]]; do
|
|
44
|
+
case "$1" in
|
|
45
|
+
--plan) PLAN="${2:?--plan requires a value}"; shift 2;;
|
|
46
|
+
--bundle) BUNDLE="${2:?--bundle requires a value}"; shift 2;;
|
|
47
|
+
--repo) REPO="${2:?--repo requires a value}"; shift 2;;
|
|
48
|
+
--force) FORCE=1; shift;;
|
|
49
|
+
--help|-h) usage; exit 0;;
|
|
50
|
+
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
|
|
51
|
+
*) echo "Unexpected arg: $1" >&2; exit 1;;
|
|
52
|
+
esac
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
[[ -n "$PLAN" && -f "$PLAN" ]] || { echo "ERROR: --plan required" >&2; exit 2; }
|
|
56
|
+
[[ -n "$BUNDLE" && -d "$BUNDLE" ]] || { echo "ERROR: --bundle directory required" >&2; exit 2; }
|
|
57
|
+
[[ -d "$REPO" ]] || { echo "ERROR: --repo is not a directory" >&2; exit 1; }
|
|
58
|
+
command -v jq >/dev/null 2>&1 || { echo "ERROR: jq required" >&2; exit 1; }
|
|
59
|
+
|
|
60
|
+
BUNDLE="${BUNDLE%/}"
|
|
61
|
+
REPO="$(cd "$REPO" && pwd)"
|
|
62
|
+
TS="${OKF_CATALOG_TS:-$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "1970-01-01T00:00:00Z")}"
|
|
63
|
+
|
|
64
|
+
wrote=0
|
|
65
|
+
skipped=0
|
|
66
|
+
|
|
67
|
+
# Best-effort one-line description from resource path.
|
|
68
|
+
sniff_desc() {
|
|
69
|
+
local res="$1" lib
|
|
70
|
+
lib="$REPO/$res/src/lib.rs"
|
|
71
|
+
[[ -f "$lib" ]] || lib="$REPO/$res/lib.rs"
|
|
72
|
+
if [[ -f "$lib" ]]; then
|
|
73
|
+
awk '/^\/\/!/ { sub(/^\/\/![[:space:]]?/, ""); print; exit }' "$lib"
|
|
74
|
+
return
|
|
75
|
+
fi
|
|
76
|
+
if [[ -f "$REPO/$res/README.md" ]]; then
|
|
77
|
+
awk 'NF && !/^#/ { print; exit }' "$REPO/$res/README.md"
|
|
78
|
+
return
|
|
79
|
+
fi
|
|
80
|
+
printf 'Workspace component at %s.' "$res"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
write_page() {
|
|
84
|
+
local cid="$1" ctype="$2" resource="$3" fan_in="$4"
|
|
85
|
+
local out="$BUNDLE/$cid"
|
|
86
|
+
mkdir -p "$(dirname "$out")"
|
|
87
|
+
if [[ -f "$out" && $FORCE -eq 0 ]]; then
|
|
88
|
+
skipped=$((skipped+1))
|
|
89
|
+
return
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
local title stem section
|
|
93
|
+
stem="$(basename "$cid" .md)"
|
|
94
|
+
section="$(dirname "$cid")"
|
|
95
|
+
title="$stem"
|
|
96
|
+
local desc
|
|
97
|
+
desc="$(sniff_desc "$resource")"
|
|
98
|
+
[[ -n "$desc" ]] || desc="Open when changing the ${stem} component at ${resource}."
|
|
99
|
+
|
|
100
|
+
# Routing description (load-bearing for concept map).
|
|
101
|
+
local routing="Open when changing \`${stem}\` (${resource}). ${desc}"
|
|
102
|
+
routing="$(printf '%s' "$routing" | tr '\n' ' ' | sed -E 's/[[:space:]]+/ /g' | cut -c1-280)"
|
|
103
|
+
|
|
104
|
+
local g1 g2
|
|
105
|
+
g1="$resource"
|
|
106
|
+
[[ -e "$REPO/$resource" ]] || g1="README.md"
|
|
107
|
+
if [[ -f "$REPO/$resource/src/lib.rs" ]]; then
|
|
108
|
+
g1="$resource/src/lib.rs"
|
|
109
|
+
elif [[ -f "$REPO/$resource/Cargo.toml" ]]; then
|
|
110
|
+
g1="$resource/Cargo.toml"
|
|
111
|
+
elif [[ -f "$REPO/$resource/package.json" ]]; then
|
|
112
|
+
g1="$resource/package.json"
|
|
113
|
+
elif [[ -f "$REPO/$resource/go.mod" ]]; then
|
|
114
|
+
g1="$resource/go.mod"
|
|
115
|
+
fi
|
|
116
|
+
g2="README.md"
|
|
117
|
+
[[ -f "$REPO/README.md" ]] || g2="Cargo.toml"
|
|
118
|
+
[[ -f "$REPO/$g2" || -f "$REPO/Cargo.toml" ]] || g2="."
|
|
119
|
+
|
|
120
|
+
# Types that need mermaid + full sections
|
|
121
|
+
local needs_diagram=0
|
|
122
|
+
case "$ctype" in
|
|
123
|
+
Subsystem|Module|Feature|Entrypoint) needs_diagram=1;;
|
|
124
|
+
esac
|
|
125
|
+
|
|
126
|
+
{
|
|
127
|
+
echo "---"
|
|
128
|
+
echo "type: $ctype"
|
|
129
|
+
echo "title: \"$title\""
|
|
130
|
+
echo "description: >"
|
|
131
|
+
echo " $routing"
|
|
132
|
+
echo "resource: \"$resource\""
|
|
133
|
+
echo "tags: [catalog, auto-emitted]"
|
|
134
|
+
echo "timestamp: \"$TS\""
|
|
135
|
+
echo "x-grounded-paths: [\"$g1\", \"$g2\"]"
|
|
136
|
+
echo "x-hotspot-score: 0.0"
|
|
137
|
+
echo "x-callers: []"
|
|
138
|
+
echo "x-catalog: true"
|
|
139
|
+
echo "---"
|
|
140
|
+
echo ""
|
|
141
|
+
echo "# $title"
|
|
142
|
+
echo ""
|
|
143
|
+
echo "## What it is"
|
|
144
|
+
echo ""
|
|
145
|
+
echo "Catalog entry for **\`${stem}\`** at \`${resource}\` (auto-emitted from the concept plan)."
|
|
146
|
+
echo ""
|
|
147
|
+
echo "$desc"
|
|
148
|
+
echo ""
|
|
149
|
+
echo "Fan-in (plan): ${fan_in}. Enrich this page during deep analysis if it is a hotspot."
|
|
150
|
+
echo ""
|
|
151
|
+
if [[ $needs_diagram -eq 1 ]]; then
|
|
152
|
+
echo "## How it works"
|
|
153
|
+
echo ""
|
|
154
|
+
echo "Primary implementation lives under \`${resource}\`. Prefer \`cargo check -p ${stem}\` /"
|
|
155
|
+
echo "package-local tests when iterating. Full control-flow diagrams belong in a later deep-dive."
|
|
156
|
+
echo ""
|
|
157
|
+
echo '```mermaid'
|
|
158
|
+
echo "flowchart TB"
|
|
159
|
+
echo " Consumer[Downstream consumers] --> C[\"${stem}\"]"
|
|
160
|
+
echo " C --> Impl[\"${resource}\"]"
|
|
161
|
+
echo '```'
|
|
162
|
+
echo ""
|
|
163
|
+
echo "## Used by"
|
|
164
|
+
echo ""
|
|
165
|
+
echo "- Parent wiki indexes under \`${section}/\`"
|
|
166
|
+
echo "- Workspace members that depend on \`${stem}\` (see package manifest)"
|
|
167
|
+
echo ""
|
|
168
|
+
echo "## Blast radius"
|
|
169
|
+
echo ""
|
|
170
|
+
echo "Changes to public APIs in \`${resource}\` may break dependent packages in this monorepo."
|
|
171
|
+
echo "Run targeted tests for \`${stem}\` and re-check direct reverse dependents."
|
|
172
|
+
echo ""
|
|
173
|
+
echo "## See also"
|
|
174
|
+
echo ""
|
|
175
|
+
echo "- [section index](index.md)"
|
|
176
|
+
echo "- Repository README / architecture overview"
|
|
177
|
+
echo ""
|
|
178
|
+
echo "## Notes"
|
|
179
|
+
echo ""
|
|
180
|
+
echo "- Emitted by \`okf-emit-catalog.sh\` so completeness does not depend on LLM coverage."
|
|
181
|
+
echo "- Safe to overwrite with a richer concept page on refresh/deep-dive (\`--force\`)."
|
|
182
|
+
else
|
|
183
|
+
echo "## How it works"
|
|
184
|
+
echo ""
|
|
185
|
+
echo "See \`${resource}\` and related package docs."
|
|
186
|
+
echo ""
|
|
187
|
+
echo "## See also"
|
|
188
|
+
echo ""
|
|
189
|
+
echo "- [section index](index.md)"
|
|
190
|
+
fi
|
|
191
|
+
} > "$out"
|
|
192
|
+
wrote=$((wrote+1))
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
while IFS=$'\t' read -r cid ctype resource fan_in required; do
|
|
196
|
+
[[ -z "$cid" ]] && continue
|
|
197
|
+
[[ "$required" == "true" ]] || continue
|
|
198
|
+
write_page "$cid" "${ctype:-Module}" "${resource:-.}" "${fan_in:-0}"
|
|
199
|
+
done < <(jq -r '.expected[] | [.concept_id, (.type // "Module"), (.resource // "."), ((.fan_in // 0)|tostring), (.required|tostring)] | @tsv' "$PLAN")
|
|
200
|
+
|
|
201
|
+
# Ensure section index stubs exist so render/section-indexes can run.
|
|
202
|
+
for sec in overview systems features reference entrypoints; do
|
|
203
|
+
dir="$BUNDLE/$sec"
|
|
204
|
+
[[ -d "$dir" ]] || continue
|
|
205
|
+
if [[ ! -f "$dir/index.md" ]]; then
|
|
206
|
+
cat > "$dir/index.md" <<EOF
|
|
207
|
+
---
|
|
208
|
+
title: "$sec"
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
# $sec
|
|
212
|
+
|
|
213
|
+
## Concept Map
|
|
214
|
+
|
|
215
|
+
<!-- CONCEPT-MAP:START -->
|
|
216
|
+
<!-- CONCEPT-MAP:END -->
|
|
217
|
+
EOF
|
|
218
|
+
fi
|
|
219
|
+
done
|
|
220
|
+
|
|
221
|
+
# Bundle root index if missing — only link section dirs that exist (no dangling rows).
|
|
222
|
+
if [[ ! -f "$BUNDLE/index.md" ]]; then
|
|
223
|
+
{
|
|
224
|
+
echo "---"
|
|
225
|
+
echo "type: Subsystem"
|
|
226
|
+
echo "title: Project Wiki"
|
|
227
|
+
echo "description: >"
|
|
228
|
+
echo " Root index of the project wiki. Start here, then route via the Concept Map."
|
|
229
|
+
echo "resource: ."
|
|
230
|
+
echo "tags: [index]"
|
|
231
|
+
echo "timestamp: \"$TS\""
|
|
232
|
+
echo "okf_version: \"0.1\""
|
|
233
|
+
echo "okf_types_version: \"0.1\""
|
|
234
|
+
echo "---"
|
|
235
|
+
echo ""
|
|
236
|
+
echo "# Project Wiki"
|
|
237
|
+
echo ""
|
|
238
|
+
echo "## Sections"
|
|
239
|
+
echo ""
|
|
240
|
+
echo "| Section | Index |"
|
|
241
|
+
echo "|---------|-------|"
|
|
242
|
+
for sec in overview systems features reference entrypoints; do
|
|
243
|
+
if [[ -d "$BUNDLE/$sec" && -f "$BUNDLE/$sec/index.md" ]]; then
|
|
244
|
+
echo "| $sec | [$sec/index.md]($sec/index.md) |"
|
|
245
|
+
fi
|
|
246
|
+
done
|
|
247
|
+
echo ""
|
|
248
|
+
echo "## Concept Map"
|
|
249
|
+
echo ""
|
|
250
|
+
echo "<!-- CONCEPT-MAP:START -->"
|
|
251
|
+
echo "<!-- CONCEPT-MAP:END -->"
|
|
252
|
+
} > "$BUNDLE/index.md"
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
echo "okf-emit-catalog: wrote=$wrote skipped_existing=$skipped plan=$PLAN bundle=$BUNDLE"
|
|
256
|
+
exit 0
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# okf-fix-links.sh — rewrite and validate markdown links in draft/ views.
|
|
3
|
+
#
|
|
4
|
+
# Fixes the class of bugs seen when okf-render-views concatenates wiki pages into
|
|
5
|
+
# draft/architecture.md (sibling-relative links and ambiguous basenames break).
|
|
6
|
+
# Also validates the whole draft/ tree (or a single file).
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# okf-fix-links.sh --draft DIR [--fix] [--check]
|
|
10
|
+
# okf-fix-links.sh --file FILE --wiki DIR [--fix]
|
|
11
|
+
#
|
|
12
|
+
# Exit: 0 all links resolve (after optional fix), 1 broken links remain, 2 usage.
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
# shellcheck source=scripts/tools/_lib.sh
|
|
17
|
+
source "$SCRIPT_DIR/_lib.sh"
|
|
18
|
+
|
|
19
|
+
DRAFT=""
|
|
20
|
+
FILE=""
|
|
21
|
+
WIKI=""
|
|
22
|
+
DO_FIX=0
|
|
23
|
+
DO_CHECK=1
|
|
24
|
+
|
|
25
|
+
usage() {
|
|
26
|
+
cat <<'EOF'
|
|
27
|
+
okf-fix-links.sh — fix/validate markdown links in Draft OKF outputs.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
okf-fix-links.sh --draft DIR [--fix] [--check]
|
|
31
|
+
okf-fix-links.sh --file architecture.md --wiki DIR/wiki [--fix]
|
|
32
|
+
|
|
33
|
+
Flags:
|
|
34
|
+
--draft DIR draft/ directory (architecture.md + wiki/ + .ai-context.md)
|
|
35
|
+
--file FILE single markdown file to rewrite/check
|
|
36
|
+
--wiki DIR wiki bundle (required with --file for basename map)
|
|
37
|
+
--fix rewrite broken/ambiguous links in place
|
|
38
|
+
--check report broken links (default on)
|
|
39
|
+
--help
|
|
40
|
+
|
|
41
|
+
Exit: 0 clean, 1 broken links remain, 2 bad invocation.
|
|
42
|
+
EOF
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
while [[ $# -gt 0 ]]; do
|
|
46
|
+
case "$1" in
|
|
47
|
+
--draft) DRAFT="${2:?--draft requires a value}"; shift 2;;
|
|
48
|
+
--file) FILE="${2:?--file requires a value}"; shift 2;;
|
|
49
|
+
--wiki) WIKI="${2:?--wiki requires a value}"; shift 2;;
|
|
50
|
+
--fix) DO_FIX=1; shift;;
|
|
51
|
+
--check) DO_CHECK=1; shift;;
|
|
52
|
+
--help|-h) usage; exit 0;;
|
|
53
|
+
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 2;;
|
|
54
|
+
*) echo "Unexpected arg: $1" >&2; exit 2;;
|
|
55
|
+
esac
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
if [[ -n "$DRAFT" ]]; then
|
|
59
|
+
DRAFT="${DRAFT%/}"
|
|
60
|
+
[[ -d "$DRAFT" ]] || { echo "ERROR: --draft not a directory" >&2; exit 2; }
|
|
61
|
+
WIKI="${WIKI:-$DRAFT/wiki}"
|
|
62
|
+
FILE="${FILE:-$DRAFT/architecture.md}"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
[[ -n "$FILE" && -f "$FILE" ]] || { echo "ERROR: --file or --draft/architecture.md required" >&2; exit 2; }
|
|
66
|
+
[[ -n "$WIKI" && -d "$WIKI" ]] || { echo "ERROR: wiki dir required" >&2; exit 2; }
|
|
67
|
+
|
|
68
|
+
# Build basename → list of wiki-relative paths (relative to draft root as wiki/...)
|
|
69
|
+
declare -A BASENAME_MAP_MULTI=()
|
|
70
|
+
while IFS= read -r -d '' p; do
|
|
71
|
+
rel="wiki/${p#"$WIKI"/}"
|
|
72
|
+
base="$(basename "$p")"
|
|
73
|
+
if [[ -n "${BASENAME_MAP_MULTI[$base]:-}" ]]; then
|
|
74
|
+
BASENAME_MAP_MULTI[$base]="${BASENAME_MAP_MULTI[$base]}|$rel"
|
|
75
|
+
else
|
|
76
|
+
BASENAME_MAP_MULTI[$base]="$rel"
|
|
77
|
+
fi
|
|
78
|
+
done < <(find "$WIKI" -type f -name '*.md' -print0)
|
|
79
|
+
|
|
80
|
+
pick_target() {
|
|
81
|
+
local base="$1" label="$2"
|
|
82
|
+
local cands="${BASENAME_MAP_MULTI[$base]:-}"
|
|
83
|
+
[[ -n "$cands" ]] || { echo ""; return; }
|
|
84
|
+
if [[ "$cands" != *"|"* ]]; then
|
|
85
|
+
printf '%s' "$cands"
|
|
86
|
+
return
|
|
87
|
+
fi
|
|
88
|
+
local lab; lab="$(printf '%s' "$label" | tr '[:upper:]' '[:lower:]')"
|
|
89
|
+
IFS='|' read -ra arr <<< "$cands"
|
|
90
|
+
local pref
|
|
91
|
+
if [[ "$lab" == *product* || "$lab" == *feature* || "$lab" == *user*guide* || "$lab" == *install* ]]; then
|
|
92
|
+
for pref in "${arr[@]}"; do [[ "$pref" == *"/features/"* ]] && { printf '%s' "$pref"; return; }; done
|
|
93
|
+
fi
|
|
94
|
+
if [[ "$lab" == *build* || "$lab" == *runbook* || "$lab" == *ops* || "$lab" == *source* ]]; then
|
|
95
|
+
for pref in "${arr[@]}"; do [[ "$pref" == *"/overview/"* ]] && { printf '%s' "$pref"; return; }; done
|
|
96
|
+
fi
|
|
97
|
+
for order in /systems/ /features/ /overview/ /reference/ /entrypoints/; do
|
|
98
|
+
for pref in "${arr[@]}"; do
|
|
99
|
+
[[ "$pref" == *"$order"* ]] && { printf '%s' "$pref"; return; }
|
|
100
|
+
done
|
|
101
|
+
done
|
|
102
|
+
printf '%s' "${arr[0]}"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Collect heading slugs from a file for anchor checks
|
|
106
|
+
build_slugs() {
|
|
107
|
+
local f="$1"
|
|
108
|
+
declare -gA SLUGS=()
|
|
109
|
+
local counts=()
|
|
110
|
+
# shellcheck disable=SC2034
|
|
111
|
+
while IFS= read -r line; do
|
|
112
|
+
[[ "$line" =~ ^#{1,6}[[:space:]]+(.*)$ ]] || continue
|
|
113
|
+
local raw="${BASH_REMATCH[1]}"
|
|
114
|
+
local s; s="$(gfm_slug "$raw")"
|
|
115
|
+
local n="${SLUGS[_count_$s]:-0}"
|
|
116
|
+
SLUGS[_count_$s]=$((n+1))
|
|
117
|
+
if [[ $n -eq 0 ]]; then
|
|
118
|
+
SLUGS[$s]=1
|
|
119
|
+
else
|
|
120
|
+
SLUGS["${s}-$n"]=1
|
|
121
|
+
fi
|
|
122
|
+
done < "$f"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
resolve_ok() {
|
|
126
|
+
local src="$1" href="$2"
|
|
127
|
+
[[ "$href" =~ ^https?:// || "$href" =~ ^mailto: ]] && return 0
|
|
128
|
+
if [[ "$href" == \#* ]]; then
|
|
129
|
+
local a="${href#\#}"
|
|
130
|
+
[[ -n "${SLUGS[$a]:-}" ]] && return 0
|
|
131
|
+
return 1
|
|
132
|
+
fi
|
|
133
|
+
local path="${href%%\#*}"
|
|
134
|
+
[[ -z "$path" ]] && return 0
|
|
135
|
+
local dir; dir="$(cd "$(dirname "$src")" && pwd)"
|
|
136
|
+
local draft_root=""
|
|
137
|
+
if [[ -n "$DRAFT" ]]; then
|
|
138
|
+
draft_root="$(cd "$DRAFT" && pwd)"
|
|
139
|
+
else
|
|
140
|
+
draft_root="$(cd "$(dirname "$src")" && pwd)"
|
|
141
|
+
fi
|
|
142
|
+
[[ -e "$dir/$path" || -e "$draft_root/$path" || -e "$path" ]] && return 0
|
|
143
|
+
return 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fix_file() {
|
|
147
|
+
local src="$1"
|
|
148
|
+
local tmp; tmp="$(mktemp)"
|
|
149
|
+
python3 - "$src" "$WIKI" "$tmp" <<'PY'
|
|
150
|
+
import sys, re
|
|
151
|
+
from pathlib import Path
|
|
152
|
+
|
|
153
|
+
src = Path(sys.argv[1])
|
|
154
|
+
wiki = Path(sys.argv[2])
|
|
155
|
+
out = Path(sys.argv[3])
|
|
156
|
+
text = src.read_text(encoding="utf-8", errors="replace")
|
|
157
|
+
|
|
158
|
+
# basename -> [wiki-relative draft paths]
|
|
159
|
+
bmap = {}
|
|
160
|
+
for p in wiki.rglob("*.md"):
|
|
161
|
+
rel = "wiki/" + str(p.relative_to(wiki)).replace("\\", "/")
|
|
162
|
+
bmap.setdefault(p.name, []).append(rel)
|
|
163
|
+
|
|
164
|
+
def pick(base, label):
|
|
165
|
+
cands = bmap.get(base, [])
|
|
166
|
+
if not cands:
|
|
167
|
+
return None
|
|
168
|
+
if len(cands) == 1:
|
|
169
|
+
return cands[0]
|
|
170
|
+
lab = label.lower()
|
|
171
|
+
def prefer(substr):
|
|
172
|
+
for c in cands:
|
|
173
|
+
if substr in c:
|
|
174
|
+
return c
|
|
175
|
+
return None
|
|
176
|
+
if any(k in lab for k in ("product", "feature", "user guide", "install", "onboarding")):
|
|
177
|
+
p = prefer("/features/")
|
|
178
|
+
if p: return p
|
|
179
|
+
if any(k in lab for k in ("build", "runbook", "ops", "source", "cargo")):
|
|
180
|
+
p = prefer("/overview/")
|
|
181
|
+
if p: return p
|
|
182
|
+
for s in ("/systems/", "/features/", "/overview/", "/reference/", "/entrypoints/"):
|
|
183
|
+
p = prefer(s)
|
|
184
|
+
if p: return p
|
|
185
|
+
return cands[0]
|
|
186
|
+
|
|
187
|
+
def gfm_slug(heading: str) -> str:
|
|
188
|
+
s = heading.strip().lower()
|
|
189
|
+
s = re.sub(r"[^\w\s-]", "", s, flags=re.UNICODE)
|
|
190
|
+
s = re.sub(r"\s+", "-", s)
|
|
191
|
+
s = re.sub(r"-+", "-", s).strip("-")
|
|
192
|
+
return s
|
|
193
|
+
|
|
194
|
+
# heading map for anchor fix
|
|
195
|
+
slug_counts = {}
|
|
196
|
+
heading_by_text = {}
|
|
197
|
+
for line in text.splitlines():
|
|
198
|
+
m = re.match(r"^(#{1,6})\s+(.+)$", line)
|
|
199
|
+
if not m:
|
|
200
|
+
continue
|
|
201
|
+
raw = m.group(2).strip()
|
|
202
|
+
base = gfm_slug(raw)
|
|
203
|
+
n = slug_counts.get(base, 0)
|
|
204
|
+
slug_counts[base] = n + 1
|
|
205
|
+
slug = base if n == 0 else f"{base}-{n}"
|
|
206
|
+
heading_by_text[raw] = slug
|
|
207
|
+
|
|
208
|
+
def repl(m):
|
|
209
|
+
label, href = m.group(1), m.group(2).strip()
|
|
210
|
+
# strip rustdoc
|
|
211
|
+
if "::" in href and not href.startswith(("http", "wiki/", "#", "../", "./")):
|
|
212
|
+
return f"`{label}`"
|
|
213
|
+
if href.startswith(("http://", "https://", "mailto:")):
|
|
214
|
+
return m.group(0)
|
|
215
|
+
if href.startswith("#"):
|
|
216
|
+
if label in heading_by_text and href[1:] != heading_by_text[label]:
|
|
217
|
+
return f"[{label}](#{heading_by_text[label]})"
|
|
218
|
+
g = gfm_slug(label)
|
|
219
|
+
# unique match on gfm of heading texts
|
|
220
|
+
for raw, slug in heading_by_text.items():
|
|
221
|
+
if gfm_slug(raw) == g and href[1:] != slug:
|
|
222
|
+
return f"[{label}](#{slug})"
|
|
223
|
+
return m.group(0)
|
|
224
|
+
path, sep, frag = href.partition("#")
|
|
225
|
+
frag_s = ("#" + frag) if frag else ""
|
|
226
|
+
if path.startswith("wiki/"):
|
|
227
|
+
return m.group(0)
|
|
228
|
+
m2 = re.match(r"^\.\./((?:systems|features|overview|entrypoints|reference)/.+)$", path)
|
|
229
|
+
if m2:
|
|
230
|
+
return f"[{label}](wiki/{m2.group(1)}{frag_s})"
|
|
231
|
+
if re.match(r"^(systems|features|overview|entrypoints|reference)/", path):
|
|
232
|
+
return f"[{label}](wiki/{path}{frag_s})"
|
|
233
|
+
if path.endswith(".md") and "/" not in path:
|
|
234
|
+
picked = pick(path, label)
|
|
235
|
+
if picked:
|
|
236
|
+
return f"[{label}]({picked}{frag_s})"
|
|
237
|
+
return m.group(0)
|
|
238
|
+
|
|
239
|
+
new = re.sub(r"\[([^\]]*)\]\(([^)]+)\)", repl, text)
|
|
240
|
+
out.write_text(new if new.endswith("\n") else new + "\n", encoding="utf-8")
|
|
241
|
+
PY
|
|
242
|
+
if [[ $DO_FIX -eq 1 ]]; then
|
|
243
|
+
mv "$tmp" "$src"
|
|
244
|
+
else
|
|
245
|
+
rm -f "$tmp"
|
|
246
|
+
fi
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if [[ $DO_FIX -eq 1 ]]; then
|
|
250
|
+
fix_file "$FILE"
|
|
251
|
+
# Also fix .ai-context when draft mode
|
|
252
|
+
if [[ -n "$DRAFT" && -f "$DRAFT/.ai-context.md" ]]; then
|
|
253
|
+
fix_file "$DRAFT/.ai-context.md"
|
|
254
|
+
fi
|
|
255
|
+
fi
|
|
256
|
+
|
|
257
|
+
# Check
|
|
258
|
+
build_slugs "$FILE"
|
|
259
|
+
broken=0
|
|
260
|
+
total=0
|
|
261
|
+
while IFS= read -r line; do
|
|
262
|
+
# crude extract — enough for validation
|
|
263
|
+
:
|
|
264
|
+
done < /dev/null
|
|
265
|
+
|
|
266
|
+
# Python check for reliability
|
|
267
|
+
broken_out="$(python3 - "$FILE" "${DRAFT:-$(dirname "$FILE")}" "$WIKI" <<'PY'
|
|
268
|
+
import sys, re
|
|
269
|
+
from pathlib import Path
|
|
270
|
+
src = Path(sys.argv[1])
|
|
271
|
+
draft = Path(sys.argv[2])
|
|
272
|
+
wiki = Path(sys.argv[3])
|
|
273
|
+
text = src.read_text(encoding="utf-8", errors="replace")
|
|
274
|
+
|
|
275
|
+
def gfm_slug(heading: str) -> str:
|
|
276
|
+
s = heading.strip().lower()
|
|
277
|
+
s = re.sub(r"[^\w\s-]", "", s, flags=re.UNICODE)
|
|
278
|
+
s = re.sub(r"\s+", "-", s)
|
|
279
|
+
s = re.sub(r"-+", "-", s).strip("-")
|
|
280
|
+
return s
|
|
281
|
+
|
|
282
|
+
slug_counts = {}
|
|
283
|
+
slugs = set()
|
|
284
|
+
for line in text.splitlines():
|
|
285
|
+
m = re.match(r"^(#{1,6})\s+(.+)$", line)
|
|
286
|
+
if not m: continue
|
|
287
|
+
base = gfm_slug(m.group(2))
|
|
288
|
+
n = slug_counts.get(base, 0)
|
|
289
|
+
slug_counts[base] = n + 1
|
|
290
|
+
slugs.add(base if n == 0 else f"{base}-{n}")
|
|
291
|
+
|
|
292
|
+
broken = []
|
|
293
|
+
ok = 0
|
|
294
|
+
for m in re.finditer(r"\[([^\]]*)\]\(([^)]+)\)", text):
|
|
295
|
+
label, href = m.group(1), m.group(2).strip()
|
|
296
|
+
if href.startswith(("http://","https://","mailto:")):
|
|
297
|
+
ok += 1; continue
|
|
298
|
+
if href.startswith("#"):
|
|
299
|
+
if href[1:] in slugs:
|
|
300
|
+
ok += 1
|
|
301
|
+
else:
|
|
302
|
+
broken.append(f"anchor {href} ({label[:40]})")
|
|
303
|
+
continue
|
|
304
|
+
path = href.split("#",1)[0]
|
|
305
|
+
if not path:
|
|
306
|
+
ok += 1; continue
|
|
307
|
+
cands = [src.parent/path, draft/path, Path(path)]
|
|
308
|
+
if any(c.exists() for c in cands):
|
|
309
|
+
ok += 1
|
|
310
|
+
else:
|
|
311
|
+
broken.append(f"file {href} ({label[:40]})")
|
|
312
|
+
print(f"OK={ok}")
|
|
313
|
+
print(f"BROKEN={len(broken)}")
|
|
314
|
+
for b in broken[:50]:
|
|
315
|
+
print(b)
|
|
316
|
+
PY
|
|
317
|
+
)"
|
|
318
|
+
|
|
319
|
+
echo "$broken_out"
|
|
320
|
+
bcount="$(echo "$broken_out" | sed -n 's/^BROKEN=//p' | head -1)"
|
|
321
|
+
if [[ "${bcount:-1}" != "0" ]]; then
|
|
322
|
+
echo "okf-fix-links: FAILED ($bcount broken in $FILE)" >&2
|
|
323
|
+
exit 1
|
|
324
|
+
fi
|
|
325
|
+
echo "okf-fix-links: clean ($FILE)"
|
|
326
|
+
exit 0
|