@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.
Files changed (143) 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/README.md +28 -49
  5. package/bin/README.md +47 -1
  6. package/cli/src/hosts/cursor.js +3 -0
  7. package/cli/src/installer.js +8 -4
  8. package/cli/src/lib/cursor-registry.js +10 -0
  9. package/cli/src/lib/fsx.js +3 -0
  10. package/cli/src/lib/marker.js +8 -3
  11. package/core/agents/architect.md +21 -9
  12. package/core/agents/debugger.md +5 -0
  13. package/core/agents/ops.md +7 -2
  14. package/core/agents/planner.md +7 -0
  15. package/core/agents/rca.md +14 -3
  16. package/core/agents/reviewer.md +1 -0
  17. package/core/agents/writer.md +6 -0
  18. package/core/knowledge-base.md +22 -3
  19. package/core/methodology.md +30 -5
  20. package/core/shared/condensation.md +28 -18
  21. package/core/shared/context-verify.md +3 -1
  22. package/core/shared/cross-skill-dispatch.md +7 -3
  23. package/core/shared/draft-context-loading.md +3 -0
  24. package/core/shared/git-report-metadata.md +2 -1
  25. package/core/shared/graph-query.md +24 -26
  26. package/core/shared/graph-usage-report.md +1 -1
  27. package/core/shared/jira-sync.md +10 -6
  28. package/core/shared/okf-retrieval.md +1 -1
  29. package/core/shared/parallel-analysis.md +14 -7
  30. package/core/shared/pattern-learning.md +2 -0
  31. package/core/shared/template-contract.md +1 -1
  32. package/core/shared/tool-resolver.md +20 -15
  33. package/core/shared/vcs-commands.md +5 -2
  34. package/core/templates/ai-context.md +3 -2
  35. package/core/templates/ai-profile.md +7 -0
  36. package/core/templates/architecture.md +6 -2
  37. package/core/templates/discovery.md +1 -1
  38. package/core/templates/guardrails.md +9 -0
  39. package/core/templates/hld.md +5 -4
  40. package/core/templates/intake-questions.md +45 -0
  41. package/core/templates/jira.md +11 -2
  42. package/core/templates/lld.md +8 -5
  43. package/core/templates/okf/concept.md +0 -1
  44. package/core/templates/plan.md +1 -1
  45. package/core/templates/product.md +11 -2
  46. package/core/templates/rca.md +6 -0
  47. package/core/templates/service-index.md +1 -0
  48. package/core/templates/spec.md +15 -0
  49. package/core/templates/tech-stack.md +12 -0
  50. package/core/templates/workflow.md +20 -0
  51. package/integrations/agents/AGENTS.md +1449 -817
  52. package/integrations/copilot/.github/copilot-instructions.md +1449 -817
  53. package/package.json +1 -1
  54. package/scripts/fetch-memory-engine.sh +25 -7
  55. package/scripts/lib.sh +90 -6
  56. package/scripts/tools/_graph_queries.sh +22 -8
  57. package/scripts/tools/_lib.sh +81 -11
  58. package/scripts/tools/adr-index.sh +1 -1
  59. package/scripts/tools/check-repo-size.sh +125 -0
  60. package/scripts/tools/check-skill-line-caps.sh +3 -4
  61. package/scripts/tools/check-template-noop.sh +0 -1
  62. package/scripts/tools/check-track-hygiene.sh +41 -21
  63. package/scripts/tools/classify-files.sh +3 -3
  64. package/scripts/tools/cycle-detect.sh +18 -17
  65. package/scripts/tools/detect-test-framework.sh +1 -1
  66. package/scripts/tools/emit-skill-metrics.sh +7 -2
  67. package/scripts/tools/fix-whitespace.sh +3 -0
  68. package/scripts/tools/freshness-check.sh +2 -2
  69. package/scripts/tools/git-metadata.sh +12 -6
  70. package/scripts/tools/graph-arch.sh +5 -11
  71. package/scripts/tools/graph-callers.sh +10 -15
  72. package/scripts/tools/graph-deps.sh +3 -10
  73. package/scripts/tools/graph-errors.sh +4 -11
  74. package/scripts/tools/graph-hierarchy.sh +4 -11
  75. package/scripts/tools/graph-impact.sh +21 -19
  76. package/scripts/tools/graph-init.sh +13 -9
  77. package/scripts/tools/graph-preflight.sh +4 -1
  78. package/scripts/tools/graph-query.sh +74 -18
  79. package/scripts/tools/graph-risk.sh +3 -10
  80. package/scripts/tools/graph-search.sh +4 -11
  81. package/scripts/tools/graph-snapshot.sh +38 -9
  82. package/scripts/tools/graph-snippet.sh +3 -10
  83. package/scripts/tools/graph-tests.sh +3 -10
  84. package/scripts/tools/graph-traces.sh +3 -10
  85. package/scripts/tools/hotspot-rank.sh +6 -12
  86. package/scripts/tools/install-smoke-test.sh +223 -0
  87. package/scripts/tools/mermaid-from-graph.sh +5 -11
  88. package/scripts/tools/migrate-track-frontmatter.sh +11 -10
  89. package/scripts/tools/okf-coverage-check.sh +8 -7
  90. package/scripts/tools/okf-emit-catalog.sh +256 -0
  91. package/scripts/tools/okf-fix-links.sh +251 -0
  92. package/scripts/tools/okf-plan-concepts.sh +242 -14
  93. package/scripts/tools/okf-render-views.sh +63 -21
  94. package/scripts/tools/okf-validate-all.sh +20 -9
  95. package/scripts/tools/okf-validate-quality.sh +15 -10
  96. package/scripts/tools/okf-validate.sh +20 -6
  97. package/scripts/tools/parse-git-log.sh +23 -5
  98. package/scripts/tools/parse-reports.sh +1 -1
  99. package/scripts/tools/render-track.sh +1 -1
  100. package/scripts/tools/resolve-tools.sh +24 -8
  101. package/scripts/tools/run-coverage.sh +1 -1
  102. package/scripts/tools/scan-markers.sh +4 -4
  103. package/scripts/tools/validate-frontmatter.sh +2 -2
  104. package/scripts/tools/verify-citations.sh +3 -4
  105. package/scripts/tools/verify-doc-anchors.sh +14 -34
  106. package/scripts/tools/verify-graph-binary.sh +15 -5
  107. package/skills/GRAPH.md +21 -10
  108. package/skills/adr/SKILL.md +25 -7
  109. package/skills/assist-review/SKILL.md +2 -1
  110. package/skills/bughunt/SKILL.md +55 -12
  111. package/skills/bughunt/references/regression-tests.md +14 -4
  112. package/skills/change/SKILL.md +19 -9
  113. package/skills/coverage/SKILL.md +28 -12
  114. package/skills/debug/SKILL.md +13 -3
  115. package/skills/decompose/SKILL.md +42 -12
  116. package/skills/deep-review/SKILL.md +19 -6
  117. package/skills/deploy-checklist/SKILL.md +10 -3
  118. package/skills/documentation/SKILL.md +12 -1
  119. package/skills/draft/SKILL.md +40 -29
  120. package/skills/draft/context-files.md +8 -1
  121. package/skills/draft/quality-guide.md +10 -2
  122. package/skills/graph/SKILL.md +2 -2
  123. package/skills/impact/SKILL.md +4 -2
  124. package/skills/implement/SKILL.md +62 -20
  125. package/skills/incident-response/SKILL.md +5 -2
  126. package/skills/init/SKILL.md +112 -30
  127. package/skills/init/references/architecture-spec.md +32 -8
  128. package/skills/init/references/okf-emitter.md +42 -78
  129. package/skills/jira/SKILL.md +33 -13
  130. package/skills/jira/references/review.md +61 -19
  131. package/skills/learn/SKILL.md +30 -7
  132. package/skills/new-track/SKILL.md +73 -18
  133. package/skills/quick-review/SKILL.md +8 -3
  134. package/skills/revert/SKILL.md +11 -5
  135. package/skills/review/SKILL.md +126 -27
  136. package/skills/review/references/zero-setup-mode.md +117 -0
  137. package/skills/standup/SKILL.md +10 -2
  138. package/skills/status/SKILL.md +7 -7
  139. package/skills/tech-debt/SKILL.md +9 -3
  140. package/skills/testing-strategy/SKILL.md +10 -2
  141. package/skills/tour/SKILL.md +2 -1
  142. package/skills/upload/SKILL.md +4 -3
  143. package/core/templates/track-architecture.md +0 -311
@@ -45,8 +45,8 @@ EOF
45
45
 
46
46
  while [[ $# -gt 0 ]]; do
47
47
  case "$1" in
48
- --repo) REPO="$2"; shift 2;;
49
- --diagram) DIAGRAM="$2"; shift 2;;
48
+ --repo) REPO="${2:?--repo requires a value}"; shift 2;;
49
+ --diagram) DIAGRAM="${2:?--diagram requires a value}"; shift 2;;
50
50
  --help|-h) usage; exit 0;;
51
51
  *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
52
52
  esac
@@ -57,9 +57,6 @@ if [[ ! -d "$REPO" ]]; then
57
57
  exit 1
58
58
  fi
59
59
 
60
- REPO_ABS="$(cd "$REPO" && pwd)"
61
- SELF_REPO="$(cd "$(dirname "$0")/../.." && pwd)"
62
-
63
60
  stub() {
64
61
  cat <<'EOF'
65
62
  ```mermaid
@@ -71,11 +68,7 @@ EOF
71
68
  exit 2
72
69
  }
73
70
 
74
- find_memory_bin "$REPO_ABS" "$SELF_REPO" || stub
75
- command -v jq >/dev/null 2>&1 || stub
76
-
77
- PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
78
- [[ -n "$PROJECT" ]] || stub
71
+ graph_bootstrap "$REPO" || stub
79
72
 
80
73
  # module-deps: real IMPORTS edges (the auto-derived dependency graph). Self-imports
81
74
  # (src == dst) are dropped so the diagram is a true cross-file graph. Capped at 40
@@ -99,7 +92,8 @@ render_co_change() {
99
92
  }
100
93
 
101
94
  render_proto_map() {
102
- local res; res="$(memory_cli get_architecture "{\"project\":\"$PROJECT\",\"aspects\":[\"routes\"]}" || echo '{}')"
95
+ local res; res="$(memory_cli get_architecture \
96
+ "$(jq -n --arg p "$PROJECT" '{project:$p, aspects:["routes"]}')" || echo '{}')"
103
97
  local edges; edges="$(echo "${res:-{\}}" | jq -r '(.routes // [])[] | " \"" + ((.method // "")|tostring) + " " + ((.path // "")|tostring) + "\" --> \"" + ((.handler // "?")|tostring) + "\""' 2>/dev/null || true)"
104
98
  if [[ -z "$edges" ]]; then return 1; fi
105
99
  printf '```mermaid\nflowchart LR\n%s\n```\n' "$edges"
@@ -69,15 +69,10 @@ EPHEMERAL_KEYS=(
69
69
  "scope_excludes"
70
70
  )
71
71
 
72
- # Stable keys that survive in markdown frontmatter.
73
- STABLE_KEYS=(
74
- "project"
75
- "module"
76
- "track_id"
77
- "generated_by"
78
- "generated_at"
79
- "links"
80
- )
72
+ # Keys that survive in markdown frontmatter: project, module, track_id,
73
+ # generated_by, generated_at, links. Recorded here for the reader only — the
74
+ # rewrite is driven entirely by EPHEMERAL_KEYS above (a drop-list, not an
75
+ # allow-list), so there is nothing to enumerate in code.
81
76
 
82
77
  # Strip ephemeral blocks from a markdown file's YAML frontmatter.
83
78
  # Approach: read the file; rewrite the frontmatter section so any line whose
@@ -223,7 +218,13 @@ EOF
223
218
  printf 'migrate: would strip ephemeral frontmatter from %s\n' "$path"
224
219
  else
225
220
  (( BACKUP )) && cp "$path" "$path.bak"
226
- local _tmp; _tmp="$(mktemp "${path}.XXXXXX")"; printf '%s' "$after" > "$_tmp" && mv -f "$_tmp" "$path"
221
+ # %s\n restores the EOF newline stripped by command substitution
222
+ # (same pattern as fix-whitespace.sh).
223
+ local _tmp; _tmp="$(mktemp "${path}.XXXXXX")"
224
+ printf '%s\n' "$after" > "$_tmp"
225
+ # mktemp creates 0600 and `mv` swaps the inode — carry the mode across.
226
+ apply_dest_mode "$_tmp" "$path"
227
+ mv -f "$_tmp" "$path"
227
228
  printf 'migrate: stripped ephemeral frontmatter from %s\n' "$path"
228
229
  fi
229
230
  fi
@@ -57,12 +57,12 @@ EOF
57
57
 
58
58
  while [[ $# -gt 0 ]]; do
59
59
  case "$1" in
60
- --plan) PLAN="$2"; shift 2;;
61
- --bundle) BUNDLE="$2"; shift 2;;
62
- --min-stub-lines) MIN_STUB_LINES="$2"; shift 2;;
60
+ --plan) PLAN="${2:?--plan requires a value}"; shift 2;;
61
+ --bundle) BUNDLE="${2:?--bundle requires a value}"; shift 2;;
62
+ --min-stub-lines) MIN_STUB_LINES="${2:?--min-stub-lines requires a value}"; shift 2;;
63
63
  --no-coverage-page) WRITE_PAGE=0; shift;;
64
64
  --json) JSON=1; shift;;
65
- --report) REPORT="$2"; shift 2;;
65
+ --report) REPORT="${2:?--report requires a value}"; shift 2;;
66
66
  --help|-h) usage; exit 0;;
67
67
  -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
68
68
  *) echo "Unexpected arg: $1" >&2; usage >&2; exit 1;;
@@ -90,7 +90,7 @@ DEFERRED=() # concept_id\treason
90
90
  EXPECTED_TOTAL=0; REQUIRED=0
91
91
 
92
92
  # Iterate expected entries.
93
- while IFS=$'\t' read -r cid required reason ftype fanin; do
93
+ while IFS=$'\t' read -r cid required reason _ftype fanin; do
94
94
  [[ -z "$cid" ]] && continue
95
95
  EXPECTED_TOTAL=$((EXPECTED_TOTAL + 1))
96
96
  if [[ "$required" == "true" ]]; then
@@ -141,8 +141,8 @@ write_coverage_page() {
141
141
  echo ""
142
142
  echo "| Component | Wiki page | Status | Fan-in |"
143
143
  echo "|-----------|-----------|--------|--------|"
144
- local cid status fanin link
145
- while IFS=$'\t' read -r cid required reason ftype fanin; do
144
+ local cid fanin link
145
+ while IFS=$'\t' read -r cid required reason _ftype fanin; do
146
146
  [[ -z "$cid" ]] && continue
147
147
  if [[ "$required" == "true" ]]; then
148
148
  if [[ -f "$BUNDLE/$cid" ]]; then
@@ -167,6 +167,7 @@ write_coverage_page() {
167
167
  fi
168
168
  done < <(jq -r '.expected[] | [.concept_id, (.required|tostring), (.reason_if_deferred // "-"), (.type // "Module"), (.fan_in // 0 | tostring)] | @tsv' "$PLAN")
169
169
  } > "$tmp"
170
+ apply_dest_mode "$tmp" "$out" # mktemp is 0600; mv would strip the dest's mode
170
171
  mv "$tmp" "$out"
171
172
  }
172
173
  [[ $WRITE_PAGE -eq 1 ]] && write_coverage_page
@@ -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,251 @@
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
+
24
+ usage() {
25
+ cat <<'EOF'
26
+ okf-fix-links.sh — fix/validate markdown links in Draft OKF outputs.
27
+
28
+ Usage:
29
+ okf-fix-links.sh --draft DIR [--fix] [--check]
30
+ okf-fix-links.sh --file architecture.md --wiki DIR/wiki [--fix]
31
+
32
+ Flags:
33
+ --draft DIR draft/ directory (architecture.md + wiki/ + .ai-context.md)
34
+ --file FILE single markdown file to rewrite/check
35
+ --wiki DIR wiki bundle (required with --file for basename map)
36
+ --fix rewrite broken/ambiguous links in place
37
+ --check accepted for symmetry with --fix; checking is unconditional
38
+ --help
39
+
40
+ Exit: 0 clean, 1 broken links remain, 2 bad invocation.
41
+ EOF
42
+ }
43
+
44
+ while [[ $# -gt 0 ]]; do
45
+ case "$1" in
46
+ --draft) DRAFT="${2:?--draft requires a value}"; shift 2;;
47
+ --file) FILE="${2:?--file requires a value}"; shift 2;;
48
+ --wiki) WIKI="${2:?--wiki requires a value}"; shift 2;;
49
+ --fix) DO_FIX=1; shift;;
50
+ --check) shift;; # checking always runs; accepted so callers can be explicit
51
+ --help|-h) usage; exit 0;;
52
+ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 2;;
53
+ *) echo "Unexpected arg: $1" >&2; exit 2;;
54
+ esac
55
+ done
56
+
57
+ if [[ -n "$DRAFT" ]]; then
58
+ DRAFT="${DRAFT%/}"
59
+ [[ -d "$DRAFT" ]] || { echo "ERROR: --draft not a directory" >&2; exit 2; }
60
+ WIKI="${WIKI:-$DRAFT/wiki}"
61
+ FILE="${FILE:-$DRAFT/architecture.md}"
62
+ fi
63
+
64
+ [[ -n "$FILE" && -f "$FILE" ]] || { echo "ERROR: --file or --draft/architecture.md required" >&2; exit 2; }
65
+ [[ -n "$WIKI" && -d "$WIKI" ]] || { echo "ERROR: wiki dir required" >&2; exit 2; }
66
+
67
+ command -v python3 >/dev/null 2>&1 || {
68
+ echo "ERROR: python3 is required by ${0##*/} (link rewriting and validation)" >&2
69
+ exit 2
70
+ }
71
+
72
+ # Link resolution (basename disambiguation, section preference) lives in the
73
+ # Python pass inside fix_file — it builds its own basename map from the same
74
+ # wiki tree. The former bash twin, pick_target(), had no callers.
75
+
76
+ # Anchor and link validation lives in the Python pass at the bottom of this
77
+ # file — it is the only checker, and it computes its own slug table.
78
+
79
+ fix_file() {
80
+ local src="$1"
81
+ local tmp; tmp="$(mktemp)"
82
+ python3 - "$src" "$WIKI" "$tmp" <<'PY'
83
+ import sys, re
84
+ from pathlib import Path
85
+
86
+ src = Path(sys.argv[1])
87
+ wiki = Path(sys.argv[2])
88
+ out = Path(sys.argv[3])
89
+ text = src.read_text(encoding="utf-8", errors="replace")
90
+
91
+ # basename -> [wiki-relative draft paths]
92
+ bmap = {}
93
+ for p in wiki.rglob("*.md"):
94
+ rel = "wiki/" + str(p.relative_to(wiki)).replace("\\", "/")
95
+ bmap.setdefault(p.name, []).append(rel)
96
+
97
+ def pick(base, label):
98
+ cands = bmap.get(base, [])
99
+ if not cands:
100
+ return None
101
+ if len(cands) == 1:
102
+ return cands[0]
103
+ lab = label.lower()
104
+ def prefer(substr):
105
+ for c in cands:
106
+ if substr in c:
107
+ return c
108
+ return None
109
+ if any(k in lab for k in ("product", "feature", "user guide", "install", "onboarding")):
110
+ p = prefer("/features/")
111
+ if p: return p
112
+ if any(k in lab for k in ("build", "runbook", "ops", "source", "cargo")):
113
+ p = prefer("/overview/")
114
+ if p: return p
115
+ for s in ("/systems/", "/features/", "/overview/", "/reference/", "/entrypoints/"):
116
+ p = prefer(s)
117
+ if p: return p
118
+ return cands[0]
119
+
120
+ def gfm_slug(heading: str) -> str:
121
+ s = heading.strip().lower()
122
+ s = re.sub(r"[^\w\s-]", "", s, flags=re.UNICODE)
123
+ s = re.sub(r"\s+", "-", s)
124
+ s = re.sub(r"-+", "-", s).strip("-")
125
+ return s
126
+
127
+ # heading map for anchor fix
128
+ slug_counts = {}
129
+ heading_by_text = {}
130
+ for line in text.splitlines():
131
+ m = re.match(r"^(#{1,6})\s+(.+)$", line)
132
+ if not m:
133
+ continue
134
+ raw = m.group(2).strip()
135
+ base = gfm_slug(raw)
136
+ n = slug_counts.get(base, 0)
137
+ slug_counts[base] = n + 1
138
+ slug = base if n == 0 else f"{base}-{n}"
139
+ heading_by_text[raw] = slug
140
+
141
+ def repl(m):
142
+ label, href = m.group(1), m.group(2).strip()
143
+ # strip rustdoc
144
+ if "::" in href and not href.startswith(("http", "wiki/", "#", "../", "./")):
145
+ return f"`{label}`"
146
+ if href.startswith(("http://", "https://", "mailto:")):
147
+ return m.group(0)
148
+ if href.startswith("#"):
149
+ if label in heading_by_text and href[1:] != heading_by_text[label]:
150
+ return f"[{label}](#{heading_by_text[label]})"
151
+ g = gfm_slug(label)
152
+ # unique match on gfm of heading texts
153
+ for raw, slug in heading_by_text.items():
154
+ if gfm_slug(raw) == g and href[1:] != slug:
155
+ return f"[{label}](#{slug})"
156
+ return m.group(0)
157
+ path, sep, frag = href.partition("#")
158
+ frag_s = ("#" + frag) if frag else ""
159
+ if path.startswith("wiki/"):
160
+ return m.group(0)
161
+ m2 = re.match(r"^\.\./((?:systems|features|overview|entrypoints|reference)/.+)$", path)
162
+ if m2:
163
+ return f"[{label}](wiki/{m2.group(1)}{frag_s})"
164
+ if re.match(r"^(systems|features|overview|entrypoints|reference)/", path):
165
+ return f"[{label}](wiki/{path}{frag_s})"
166
+ if path.endswith(".md") and "/" not in path:
167
+ picked = pick(path, label)
168
+ if picked:
169
+ return f"[{label}]({picked}{frag_s})"
170
+ return m.group(0)
171
+
172
+ new = re.sub(r"\[([^\]]*)\]\(([^)]+)\)", repl, text)
173
+ out.write_text(new if new.endswith("\n") else new + "\n", encoding="utf-8")
174
+ PY
175
+ apply_dest_mode "$tmp" "$src" # mktemp is 0600; mv would strip the dest's mode
176
+ mv "$tmp" "$src"
177
+ }
178
+
179
+ if [[ $DO_FIX -eq 1 ]]; then
180
+ fix_file "$FILE"
181
+ # Also fix .ai-context when draft mode
182
+ if [[ -n "$DRAFT" && -f "$DRAFT/.ai-context.md" ]]; then
183
+ fix_file "$DRAFT/.ai-context.md"
184
+ fi
185
+ fi
186
+
187
+ # Check — the single source of link/anchor truth
188
+ broken_out="$(python3 - "$FILE" "${DRAFT:-$(dirname "$FILE")}" "$WIKI" <<'PY'
189
+ import sys, re
190
+ from pathlib import Path
191
+ src = Path(sys.argv[1])
192
+ draft = Path(sys.argv[2])
193
+ wiki = Path(sys.argv[3])
194
+ text = src.read_text(encoding="utf-8", errors="replace")
195
+
196
+ def gfm_slug(heading: str) -> str:
197
+ s = heading.strip().lower()
198
+ s = re.sub(r"[^\w\s-]", "", s, flags=re.UNICODE)
199
+ s = re.sub(r"\s+", "-", s)
200
+ s = re.sub(r"-+", "-", s).strip("-")
201
+ return s
202
+
203
+ slug_counts = {}
204
+ slugs = set()
205
+ for line in text.splitlines():
206
+ m = re.match(r"^(#{1,6})\s+(.+)$", line)
207
+ if not m: continue
208
+ base = gfm_slug(m.group(2))
209
+ n = slug_counts.get(base, 0)
210
+ slug_counts[base] = n + 1
211
+ slugs.add(base if n == 0 else f"{base}-{n}")
212
+
213
+ broken = []
214
+ ok = 0
215
+ for m in re.finditer(r"\[([^\]]*)\]\(([^)]+)\)", text):
216
+ label, href = m.group(1), m.group(2).strip()
217
+ if href.startswith(("http://","https://","mailto:")):
218
+ ok += 1; continue
219
+ if href.startswith("#"):
220
+ if href[1:] in slugs:
221
+ ok += 1
222
+ else:
223
+ broken.append(f"anchor {href} ({label[:40]})")
224
+ continue
225
+ path = href.split("#",1)[0]
226
+ if not path:
227
+ ok += 1; continue
228
+ # Resolve only against the two bases a link inside this document can mean:
229
+ # the document's own directory and the draft/ root. Never against the
230
+ # process CWD — a same-named file in the caller's cwd used to make a
231
+ # genuinely dangling link report clean (exit flipped 1 -> 0).
232
+ cands = [src.parent/path, draft/path]
233
+ if any(c.exists() for c in cands):
234
+ ok += 1
235
+ else:
236
+ broken.append(f"file {href} ({label[:40]})")
237
+ print(f"OK={ok}")
238
+ print(f"BROKEN={len(broken)}")
239
+ for b in broken[:50]:
240
+ print(b)
241
+ PY
242
+ )"
243
+
244
+ echo "$broken_out"
245
+ bcount="$(echo "$broken_out" | sed -n 's/^BROKEN=//p' | head -1)"
246
+ if [[ "${bcount:-1}" != "0" ]]; then
247
+ echo "okf-fix-links: FAILED ($bcount broken in $FILE)" >&2
248
+ exit 1
249
+ fi
250
+ echo "okf-fix-links: clean ($FILE)"
251
+ exit 0