@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
|
@@ -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
|
-
|
|
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
|
|
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
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
145
|
-
while IFS=$'\t' read -r cid required reason
|
|
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
|