@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.
Files changed (86) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/cli/src/hosts/cursor.js +3 -0
  5. package/cli/src/installer.js +7 -3
  6. package/cli/src/lib/fsx.js +3 -0
  7. package/cli/src/lib/marker.js +8 -3
  8. package/core/shared/condensation.md +1 -1
  9. package/core/shared/git-report-metadata.md +1 -1
  10. package/core/shared/graph-query.md +24 -24
  11. package/core/shared/tool-resolver.md +20 -15
  12. package/core/templates/okf/index.md +6 -10
  13. package/core/templates/okf/section-index.md +2 -11
  14. package/core/templates/plan.md +1 -1
  15. package/integrations/agents/AGENTS.md +142 -492
  16. package/integrations/copilot/.github/copilot-instructions.md +142 -492
  17. package/package.json +1 -1
  18. package/scripts/fetch-memory-engine.sh +1 -1
  19. package/scripts/lib.sh +79 -2
  20. package/scripts/tools/_graph_queries.sh +12 -5
  21. package/scripts/tools/_lib.sh +56 -8
  22. package/scripts/tools/adr-index.sh +1 -1
  23. package/scripts/tools/check-skill-line-caps.sh +2 -2
  24. package/scripts/tools/check-track-hygiene.sh +41 -21
  25. package/scripts/tools/classify-files.sh +1 -1
  26. package/scripts/tools/cycle-detect.sh +2 -9
  27. package/scripts/tools/detect-test-framework.sh +1 -1
  28. package/scripts/tools/emit-skill-metrics.sh +7 -2
  29. package/scripts/tools/freshness-check.sh +2 -2
  30. package/scripts/tools/git-metadata.sh +6 -6
  31. package/scripts/tools/graph-arch.sh +4 -10
  32. package/scripts/tools/graph-callers.sh +4 -12
  33. package/scripts/tools/graph-deps.sh +3 -10
  34. package/scripts/tools/graph-errors.sh +4 -11
  35. package/scripts/tools/graph-hierarchy.sh +4 -11
  36. package/scripts/tools/graph-impact.sh +17 -17
  37. package/scripts/tools/graph-init.sh +1 -1
  38. package/scripts/tools/graph-preflight.sh +1 -1
  39. package/scripts/tools/graph-query.sh +5 -12
  40. package/scripts/tools/graph-risk.sh +3 -10
  41. package/scripts/tools/graph-search.sh +4 -11
  42. package/scripts/tools/graph-snapshot.sh +9 -4
  43. package/scripts/tools/graph-snippet.sh +3 -10
  44. package/scripts/tools/graph-tests.sh +3 -10
  45. package/scripts/tools/graph-traces.sh +3 -10
  46. package/scripts/tools/hotspot-rank.sh +4 -11
  47. package/scripts/tools/mermaid-from-graph.sh +3 -10
  48. package/scripts/tools/migrate-track-frontmatter.sh +3 -1
  49. package/scripts/tools/okf-coverage-check.sh +24 -6
  50. package/scripts/tools/okf-emit-catalog.sh +256 -0
  51. package/scripts/tools/okf-fix-links.sh +326 -0
  52. package/scripts/tools/okf-plan-concepts.sh +245 -14
  53. package/scripts/tools/okf-render-views.sh +65 -13
  54. package/scripts/tools/okf-validate-all.sh +20 -9
  55. package/scripts/tools/okf-validate-quality.sh +2 -8
  56. package/scripts/tools/okf-validate.sh +20 -10
  57. package/scripts/tools/parse-git-log.sh +4 -4
  58. package/scripts/tools/parse-reports.sh +1 -1
  59. package/scripts/tools/render-track.sh +1 -1
  60. package/scripts/tools/run-coverage.sh +1 -1
  61. package/scripts/tools/scan-markers.sh +4 -4
  62. package/scripts/tools/validate-frontmatter.sh +2 -2
  63. package/scripts/tools/verify-citations.sh +2 -2
  64. package/scripts/tools/verify-doc-anchors.sh +1 -1
  65. package/scripts/tools/verify-graph-binary.sh +2 -2
  66. package/skills/adr/SKILL.md +1 -1
  67. package/skills/bughunt/SKILL.md +1 -1
  68. package/skills/coverage/SKILL.md +2 -2
  69. package/skills/debug/SKILL.md +1 -1
  70. package/skills/decompose/SKILL.md +3 -3
  71. package/skills/deep-review/SKILL.md +2 -2
  72. package/skills/deploy-checklist/SKILL.md +1 -1
  73. package/skills/graph/SKILL.md +1 -1
  74. package/skills/impact/SKILL.md +1 -1
  75. package/skills/implement/SKILL.md +2 -2
  76. package/skills/init/SKILL.md +10 -7
  77. package/skills/init/references/architecture-spec.md +1 -1
  78. package/skills/init/references/okf-emitter.md +51 -76
  79. package/skills/learn/SKILL.md +1 -1
  80. package/skills/quick-review/SKILL.md +1 -1
  81. package/skills/review/SKILL.md +3 -3
  82. package/skills/standup/SKILL.md +1 -1
  83. package/skills/status/SKILL.md +1 -1
  84. package/skills/tech-debt/SKILL.md +2 -2
  85. package/skills/upload/SKILL.md +1 -1
  86. 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