@drafthq/draft 2.8.3 → 3.0.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 (46) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +3 -3
  4. package/bin/README.md +13 -0
  5. package/core/methodology.md +17 -18
  6. package/core/shared/condensation.md +1 -1
  7. package/core/shared/draft-context-loading.md +4 -2
  8. package/core/shared/graph-query.md +4 -3
  9. package/core/templates/ai-context.md +1 -0
  10. package/core/templates/ai-profile.md +1 -0
  11. package/core/templates/architecture.md +1 -0
  12. package/core/templates/dependency-graph.md +2 -2
  13. package/core/templates/discovery.md +1 -0
  14. package/core/templates/guardrails.md +1 -0
  15. package/core/templates/hld.md +1 -0
  16. package/core/templates/lld.md +1 -0
  17. package/core/templates/plan.md +1 -0
  18. package/core/templates/product.md +1 -0
  19. package/core/templates/rca.md +1 -0
  20. package/core/templates/root-architecture.md +3 -3
  21. package/core/templates/root-product.md +2 -2
  22. package/core/templates/root-tech-stack.md +2 -2
  23. package/core/templates/service-index.md +3 -3
  24. package/core/templates/spec.md +1 -0
  25. package/core/templates/tech-matrix.md +2 -2
  26. package/core/templates/tech-stack.md +1 -0
  27. package/core/templates/workflow.md +1 -0
  28. package/integrations/agents/AGENTS.md +134 -918
  29. package/integrations/copilot/.github/copilot-instructions.md +134 -918
  30. package/package.json +1 -1
  31. package/scripts/lib.sh +4 -1
  32. package/scripts/tools/graph-init.sh +187 -0
  33. package/scripts/tools/graph-snapshot.sh +6 -1
  34. package/scripts/tools/okf-bundle.sh +141 -0
  35. package/scripts/tools/okf-check.sh +137 -0
  36. package/scripts/tools/okf-emit.sh +161 -0
  37. package/scripts/tools/skill-caps.conf +0 -1
  38. package/skills/GRAPH.md +7 -10
  39. package/skills/bughunt/SKILL.md +13 -0
  40. package/skills/discover/SKILL.md +2 -4
  41. package/skills/draft/SKILL.md +2 -2
  42. package/skills/draft/intent-mapping.md +3 -2
  43. package/skills/graph/SKILL.md +3 -3
  44. package/skills/init/SKILL.md +58 -19
  45. package/skills/init/references/architecture-spec.md +5 -5
  46. package/skills/index/SKILL.md +0 -848
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drafthq/draft",
3
- "version": "2.8.3",
3
+ "version": "3.0.0",
4
4
  "description": "Context-Driven Development for AI coding agents — install Draft into Claude Code, Cursor, Codex, or opencode.",
5
5
  "bin": {
6
6
  "draft": "cli/bin/draft.js"
package/scripts/lib.sh CHANGED
@@ -24,7 +24,6 @@ TOOLS_DIR="$ROOT_DIR/scripts/tools"
24
24
  SKILL_ORDER=(
25
25
  draft
26
26
  init
27
- index
28
27
  graph
29
28
  new-track
30
29
  decompose
@@ -158,6 +157,10 @@ TOOLS=(
158
157
  "manage-symlinks.sh"
159
158
  "mermaid-from-graph.sh"
160
159
  "graph-snapshot.sh"
160
+ "graph-init.sh"
161
+ "okf-emit.sh"
162
+ "okf-bundle.sh"
163
+ "okf-check.sh"
161
164
  "graph-impact.sh"
162
165
  "graph-callers.sh"
163
166
  "validate-frontmatter.sh"
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env bash
2
+ # graph-init.sh — scope-aware, root-first knowledge-graph builder for /draft:init.
3
+ #
4
+ # Ensures the whole-repo "code graph knowledge memory" exists at the repository
5
+ # ROOT (the spine — the single structural source of truth), then builds a
6
+ # scope-local snapshot and links a sub-module's graph up to the root.
7
+ #
8
+ # Model:
9
+ # - ROOT resolution: nearest ancestor ABOVE scope containing draft/ (bounded by
10
+ # the git toplevel) → git toplevel → scope itself (no git / module-local).
11
+ # - The engine is the default capability tier. If the codebase-memory-mcp binary
12
+ # is missing it is fetched (blocking) unless --no-fetch or DRAFT_MEMORY_DISABLE.
13
+ # - Root init (scope == root): build the whole-repo snapshot at <root>/draft/graph/.
14
+ # - Module init (scope != root): unless --module-only, (re)build the root snapshot
15
+ # first (the spine — index time is accepted, incremental once warm), then build
16
+ # <scope>/draft/graph/ and write root-link.json pointing up to the root snapshot.
17
+ #
18
+ # The committed snapshot (draft/graph/) is the git-tracked memory; the engine's
19
+ # ~/.cache index is a disposable accelerator and is never committed.
20
+ #
21
+ # Usage: scripts/tools/graph-init.sh [--scope DIR] [--module-only] [--no-fetch] [--json]
22
+ # Exit codes: 0 OK, 1 invocation error, 2 graph engine unavailable.
23
+ set -euo pipefail
24
+
25
+ TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
26
+ # shellcheck source=_lib.sh
27
+ source "$TOOLS_DIR/_lib.sh"
28
+
29
+ SCOPE="."
30
+ MODULE_ONLY=0
31
+ NO_FETCH=0
32
+ EMIT_JSON=0
33
+
34
+ usage() {
35
+ cat <<'EOF'
36
+ graph-init.sh — scope-aware, root-first knowledge-graph builder.
37
+
38
+ Usage:
39
+ scripts/tools/graph-init.sh [--scope DIR] [--module-only] [--no-fetch] [--json]
40
+
41
+ Flags:
42
+ --scope DIR Directory init was invoked in (default: cwd).
43
+ --module-only Do not touch the root; build only the module snapshot and mark
44
+ its root link "pending".
45
+ --no-fetch Never download the engine; degrade if it is absent (CI/tests).
46
+ --json Emit a machine-readable summary instead of a human report.
47
+ --help Show this help.
48
+
49
+ Exit 0 on success, 2 when the graph engine is unavailable (no snapshot built).
50
+ EOF
51
+ }
52
+
53
+ while [[ $# -gt 0 ]]; do
54
+ case "$1" in
55
+ --scope) SCOPE="$2"; shift 2;;
56
+ --module-only) MODULE_ONLY=1; shift;;
57
+ --no-fetch) NO_FETCH=1; shift;;
58
+ --json) EMIT_JSON=1; shift;;
59
+ --help|-h) usage; exit 0;;
60
+ *) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
61
+ esac
62
+ done
63
+
64
+ [[ -d "$SCOPE" ]] || { echo "ERROR: --scope '$SCOPE' is not a directory" >&2; exit 1; }
65
+ SCOPE_ABS="$(cd "$SCOPE" && pwd)"
66
+ SELF_REPO="$(cd "$TOOLS_DIR/../.." && pwd)"
67
+
68
+ # --- Resolve ROOT (bounded by the git toplevel; never escapes the repo) ---
69
+ GIT_TOP="$(git -C "$SCOPE_ABS" rev-parse --show-toplevel 2>/dev/null || true)"
70
+ resolve_root() {
71
+ if [[ -z "$GIT_TOP" ]]; then printf '%s' "$SCOPE_ABS"; return; fi # no git → module-local
72
+ local d="$SCOPE_ABS"
73
+ while [[ "$d" != "$GIT_TOP" && "$d" != "/" ]]; do
74
+ d="$(dirname "$d")"
75
+ if [[ "$d" != "$SCOPE_ABS" && -d "$d/draft" ]]; then printf '%s' "$d"; return; fi
76
+ done
77
+ printf '%s' "$GIT_TOP"
78
+ }
79
+ ROOT_ABS="$(resolve_root)"
80
+ IS_ROOT=0
81
+ [[ "$SCOPE_ABS" == "$ROOT_ABS" ]] && IS_ROOT=1
82
+
83
+ # --- Ensure the engine (the default tier); fetch when missing unless told not to ---
84
+ ensure_engine() {
85
+ [[ -z "${DRAFT_MEMORY_DISABLE:-}" ]] || return 1
86
+ find_memory_bin "$SCOPE_ABS" "$SELF_REPO" && return 0
87
+ [[ "$NO_FETCH" -eq 0 ]] || return 1
88
+ echo "Graph engine not found — fetching it (one-time download; this may take a while)..." >&2
89
+ "$SELF_REPO/scripts/fetch-memory-engine.sh" >&2 2>&1 || true
90
+ find_memory_bin "$SCOPE_ABS" "$SELF_REPO"
91
+ }
92
+
93
+ engine_unavailable() {
94
+ if [[ "$EMIT_JSON" -eq 1 ]]; then
95
+ printf '{"status":"unavailable","root":"%s","scope":"%s","is_root":%s}\n' \
96
+ "$ROOT_ABS" "$SCOPE_ABS" "$IS_ROOT"
97
+ else
98
+ echo "WARNING: knowledge-graph engine (codebase-memory-mcp) is unavailable — no graph built." >&2
99
+ echo " The engine is Draft's default capability tier. Install it with:" >&2
100
+ echo " scripts/fetch-memory-engine.sh" >&2
101
+ echo " or put codebase-memory-mcp on PATH. Set DRAFT_MEMORY_DISABLE=1 to silence this." >&2
102
+ echo " Committed draft/graph/ snapshots (if present) still provide structural context." >&2
103
+ fi
104
+ exit 2
105
+ }
106
+
107
+ # Build a committed snapshot for a repo dir; returns graph-snapshot's exit code.
108
+ # Snapshot progress goes to stderr so stdout stays clean for --json consumers.
109
+ build_snapshot() {
110
+ local rc=0
111
+ "$TOOLS_DIR/graph-snapshot.sh" --repo "$1" 1>&2 || rc=$?
112
+ return "$rc"
113
+ }
114
+
115
+ # Path from <module>/draft/graph back to <root>/draft/graph (module is under root).
116
+ root_link_relpath() {
117
+ local sub="${SCOPE_ABS#"$ROOT_ABS"/}"
118
+ local ups=2 seg
119
+ IFS='/' read -ra seg <<< "$sub"
120
+ ups=$(( ${#seg[@]} + 2 ))
121
+ local i out=""
122
+ for ((i = 0; i < ups; i++)); do out+="../"; done
123
+ printf '%sdraft/graph' "$out"
124
+ }
125
+
126
+ write_root_link() {
127
+ local status="$1"
128
+ local mod_graph="$SCOPE_ABS/draft/graph"
129
+ mkdir -p "$mod_graph"
130
+ local rel root_project="unknown" root_commit ts schema="$ROOT_ABS/draft/graph/schema.yaml"
131
+ rel="$(root_link_relpath)"
132
+ if [[ -f "$schema" ]]; then
133
+ root_project="$(grep -m1 '^project:' "$schema" 2>/dev/null | sed 's/^project:[[:space:]]*//; s/^"//; s/"$//' || true)"
134
+ [[ -n "$root_project" ]] || root_project="unknown"
135
+ fi
136
+ root_commit="$(git -C "$ROOT_ABS" rev-parse --verify --quiet HEAD 2>/dev/null || echo none)"
137
+ ts="$(date -Iseconds 2>/dev/null || date)"
138
+ cat > "$mod_graph/root-link.json" <<EOF
139
+ {
140
+ "root_graph": "$rel",
141
+ "root_abs": "$ROOT_ABS/draft/graph",
142
+ "root_project": "${root_project:-unknown}",
143
+ "root_commit": "$root_commit",
144
+ "status": "$status",
145
+ "linked_at": "$ts",
146
+ "linked_by": "graph-init.sh",
147
+ "note": "Root is the authoritative whole-repo graph. Follow root_graph for cross-module understanding."
148
+ }
149
+ EOF
150
+ }
151
+
152
+ ensure_engine || engine_unavailable
153
+
154
+ ROOT_BUILT=0
155
+ MODULE_BUILT=0
156
+ LINK_STATUS="none"
157
+
158
+ if [[ "$IS_ROOT" -eq 1 ]]; then
159
+ build_snapshot "$ROOT_ABS" && ROOT_BUILT=1
160
+ else
161
+ if [[ "$MODULE_ONLY" -eq 0 ]]; then
162
+ echo "Sub-module of $ROOT_ABS — ensuring the root code-graph spine first..." >&2
163
+ build_snapshot "$ROOT_ABS" && ROOT_BUILT=1
164
+ fi
165
+ build_snapshot "$SCOPE_ABS" && MODULE_BUILT=1
166
+ if [[ -f "$ROOT_ABS/draft/graph/schema.yaml" ]]; then
167
+ LINK_STATUS="linked"
168
+ else
169
+ LINK_STATUS="pending"
170
+ fi
171
+ write_root_link "$LINK_STATUS"
172
+ fi
173
+
174
+ if [[ "$EMIT_JSON" -eq 1 ]]; then
175
+ printf '{"status":"ok","root":"%s","scope":"%s","is_root":%s,"root_built":%s,"module_built":%s,"link_status":"%s"}\n' \
176
+ "$ROOT_ABS" "$SCOPE_ABS" "$IS_ROOT" "$ROOT_BUILT" "$MODULE_BUILT" "$LINK_STATUS"
177
+ else
178
+ echo "--- graph-init ---"
179
+ echo "Root: $ROOT_ABS$([[ $IS_ROOT -eq 1 ]] && echo ' (this scope is the root)')"
180
+ [[ "$IS_ROOT" -eq 1 ]] && echo "Built whole-repo spine: $ROOT_ABS/draft/graph/"
181
+ if [[ "$IS_ROOT" -eq 0 ]]; then
182
+ [[ "$ROOT_BUILT" -eq 1 ]] && echo "Root spine: refreshed $ROOT_ABS/draft/graph/"
183
+ echo "Module: $SCOPE_ABS/draft/graph/"
184
+ echo "Root link: $LINK_STATUS ($SCOPE_ABS/draft/graph/root-link.json)"
185
+ fi
186
+ fi
187
+ exit 0
@@ -13,6 +13,7 @@
13
13
  # hotspots.jsonl fan-in-ranked symbols, one JSON object per line
14
14
  # module-deps.mermaid co-change coupling diagram
15
15
  # proto-map.mermaid detected-route diagram
16
+ # okf/ Open Knowledge Format bundle (portable markdown mirror)
16
17
  #
17
18
  # Usage: scripts/tools/graph-snapshot.sh [--repo DIR] [--out DIR]
18
19
  # Exit codes: 0 OK, 1 invocation error, 2 graph engine unavailable.
@@ -76,7 +77,10 @@ echo "$ARCH" | jq '.' > "$OUT/architecture.json"
76
77
  "$TOOLS_DIR/mermaid-from-graph.sh" --repo "$REPO_ABS" --diagram module-deps > "$OUT/module-deps.mermaid" 2>/dev/null || true
77
78
  "$TOOLS_DIR/mermaid-from-graph.sh" --repo "$REPO_ABS" --diagram proto-map > "$OUT/proto-map.mermaid" 2>/dev/null || true
78
79
 
79
- # 4. schema.yamlmetadata + gate for skill graph use
80
+ # 4. OKF bundle (best-effort) portable Open Knowledge Format mirror of the graph.
81
+ "$TOOLS_DIR/okf-emit.sh" --repo "$REPO_ABS" --snapshot "$OUT" --out "$OUT/okf" >/dev/null 2>&1 || true
82
+
83
+ # 5. schema.yaml — metadata + gate for skill graph use
80
84
  NODES="$(echo "$ARCH" | jq -r '.total_nodes // 0')"
81
85
  EDGES="$(echo "$ARCH" | jq -r '.total_edges // 0')"
82
86
  HOTN="$(wc -l < "$OUT/hotspots.jsonl" | tr -d ' ')"
@@ -96,6 +100,7 @@ artifacts:
96
100
  - hotspots.jsonl
97
101
  - module-deps.mermaid
98
102
  - proto-map.mermaid
103
+ - okf/
99
104
  EOF
100
105
 
101
106
  echo "Snapshot written to $OUT (nodes=$NODES edges=$EDGES hotspots=$HOTN)"
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+ # okf-bundle.sh — make a draft/ context directory an Open Knowledge Format bundle.
3
+ #
4
+ # OKF (Google Cloud, open spec) treats a directory of markdown files with YAML
5
+ # frontmatter as a knowledge bundle: one file per concept, the file path is the
6
+ # concept's identity, concepts cross-link with markdown links, and index.md is
7
+ # the navigable root. Draft's project-doc files already carry the required
8
+ # `type` frontmatter (architecture.md, .ai-context.md, product.md, ...); this
9
+ # tool writes the bundle's root index.md so the whole draft/ tree is a portable,
10
+ # vendor-neutral OKF bundle.
11
+ # https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing
12
+ #
13
+ # Writes <dir>/index.md (the OKF bundle-root index, §6/§11) linking every concept
14
+ # file present, the tracks, and the graph sub-bundle (graph/okf/). Validate the
15
+ # result with okf-check.sh (the OKF v0.1 conformance checker).
16
+ #
17
+ # Usage: scripts/tools/okf-bundle.sh [--dir DIR]
18
+ # Exit codes: 0 OK, 1 invocation error, 2 dir missing.
19
+ set -euo pipefail
20
+
21
+ DIR="draft"
22
+
23
+ usage() {
24
+ cat <<'EOF'
25
+ okf-bundle.sh — write the OKF bundle-root index.md for a draft/ context bundle.
26
+
27
+ Usage:
28
+ scripts/tools/okf-bundle.sh [--dir DIR]
29
+
30
+ Flags:
31
+ --dir DIR Bundle root (default: draft).
32
+ --help Show this help.
33
+
34
+ Writes <dir>/index.md cross-linking every concept present, the tracks, and the
35
+ graph sub-bundle. Validate conformance with `okf-check.sh --dir <dir>`.
36
+ Exit 0 OK, 1 invocation error, 2 when <dir> is absent.
37
+ EOF
38
+ }
39
+
40
+ while [[ $# -gt 0 ]]; do
41
+ case "$1" in
42
+ --dir) DIR="$2"; shift 2;;
43
+ --help|-h) usage; exit 0;;
44
+ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
45
+ *) echo "Unexpected arg: $1" >&2; usage >&2; exit 1;;
46
+ esac
47
+ done
48
+
49
+ [[ -d "$DIR" ]] || { echo "ERROR: --dir '$DIR' is not a directory" >&2; exit 2; }
50
+
51
+ # Canonical Draft concepts, ordered: "filename|label|expected-type".
52
+ # Each present file is linked from index.md and checked for `type:` under --check.
53
+ CONCEPTS=(
54
+ ".ai-profile.md|AI Profile|Profile"
55
+ ".ai-context.md|AI Context Map|ContextMap"
56
+ "architecture.md|Architecture|Architecture"
57
+ "product.md|Product|Product"
58
+ "tech-stack.md|Tech Stack|TechStack"
59
+ "workflow.md|Workflow|Workflow"
60
+ "guardrails.md|Guardrails|Guardrails"
61
+ "service-index.md|Service Index|ServiceIndex"
62
+ )
63
+
64
+ # read_fm_field FILE FIELD — print a top-level frontmatter scalar (quotes stripped).
65
+ read_fm_field() {
66
+ awk -v field="$2" '
67
+ NR==1 { if ($0 != "---") exit; next }
68
+ /^---[[:space:]]*$/ { exit }
69
+ {
70
+ if (index($0, field ":") == 1) {
71
+ sub("^" field ":[[:space:]]*", "")
72
+ gsub(/^"|"$/, "")
73
+ print
74
+ exit
75
+ }
76
+ }
77
+ ' "$1"
78
+ }
79
+
80
+ # --- write the bundle root index.md ---
81
+ PROJECT=""
82
+ for probe in architecture.md .ai-context.md product.md .ai-profile.md; do
83
+ if [[ -f "$DIR/$probe" ]]; then
84
+ PROJECT="$(read_fm_field "$DIR/$probe" project)"
85
+ [[ -n "$PROJECT" ]] && break
86
+ fi
87
+ done
88
+ [[ -n "$PROJECT" && "$PROJECT" != "{PROJECT_NAME}" ]] || PROJECT="$(basename "$(cd "$DIR/.." && pwd)")"
89
+
90
+ INDEX="$DIR/index.md"
91
+ {
92
+ # Bundle-root index.md: frontmatter is permitted only to declare okf_version (§11).
93
+ printf -- '---\n'
94
+ printf 'okf_version: "0.1"\n'
95
+ printf -- '---\n\n'
96
+ printf '# %s — Draft Context Bundle\n\n' "$PROJECT"
97
+ printf 'Open Knowledge Format (OKF) bundle root. Each concept below is a markdown file with a `type` frontmatter field; the links form the navigable knowledge graph.\n'
98
+
99
+ # Context concepts (only if at least one is present)
100
+ context=""
101
+ for entry in "${CONCEPTS[@]}"; do
102
+ IFS='|' read -r fname label expected <<< "$entry"
103
+ [[ -f "$DIR/$fname" ]] || continue
104
+ t="$(read_fm_field "$DIR/$fname" type)"
105
+ [[ -n "$t" ]] || t="$expected"
106
+ d="$(read_fm_field "$DIR/$fname" description)"
107
+ desc="${d:-$t concept}"
108
+ context+="$(printf -- '* [%s](%s) - %s' "$label" "$fname" "$desc")"$'\n'
109
+ done
110
+ if [[ -n "$context" ]]; then
111
+ printf '\n# Context\n\n%s' "$context"
112
+ fi
113
+
114
+ # Tracks
115
+ if [[ -f "$DIR/tracks.md" || -d "$DIR/tracks" ]]; then
116
+ printf '\n# Tracks\n\n'
117
+ [[ -f "$DIR/tracks.md" ]] && printf -- '* [Track Index](tracks.md) - active, completed, and archived tracks\n'
118
+ if [[ -d "$DIR/tracks" ]]; then
119
+ for td in "$DIR"/tracks/*/; do
120
+ [[ -d "$td" ]] || continue
121
+ id="$(basename "$td")"
122
+ [[ -f "$td/spec.md" ]] || continue
123
+ title="$id"
124
+ if command -v jq >/dev/null 2>&1 && [[ -f "$td/metadata.json" ]]; then
125
+ mt="$(jq -r '.title // empty' "$td/metadata.json" 2>/dev/null || true)"
126
+ [[ -n "$mt" ]] && title="$mt"
127
+ fi
128
+ printf -- '* [%s](tracks/%s/spec.md) - track %s\n' "$title" "$id" "$id"
129
+ done
130
+ fi
131
+ fi
132
+
133
+ # Knowledge graph sub-bundle
134
+ if [[ -f "$DIR/graph/okf/index.md" ]]; then
135
+ printf '\n# Knowledge graph\n\n'
136
+ printf -- '* [Graph bundle](graph/okf/index.md) - structural knowledge graph (modules, dependencies, hotspots)\n'
137
+ fi
138
+ } > "$INDEX"
139
+
140
+ echo "OKF bundle root written to $INDEX"
141
+ exit 0
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bash
2
+ # okf-check.sh — validate a directory against the Open Knowledge Format v0.1 spec.
3
+ #
4
+ # Implements the §9 conformance criteria of OKF v0.1
5
+ # (https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md):
6
+ #
7
+ # §9.1 Every non-reserved .md file has a parseable YAML frontmatter block.
8
+ # §9.2 Every such frontmatter block has a non-empty `type` field.
9
+ # §9.3 Reserved files follow their structure when present:
10
+ # index.md (§6) — contains NO frontmatter, EXCEPT the bundle-root
11
+ # index.md MAY carry frontmatter holding only
12
+ # `okf_version` (§11).
13
+ # log.md (§7) — `## ` date headings MUST be ISO 8601 (YYYY-MM-DD).
14
+ #
15
+ # Consumers are required to be permissive, so this checker only enforces the
16
+ # three hard rules above; everything else in the spec is soft guidance.
17
+ #
18
+ # Usage: scripts/tools/okf-check.sh [--dir DIR] [--quiet]
19
+ # Exit codes: 0 conformant, 1 violations found, 2 dir missing.
20
+ set -euo pipefail
21
+
22
+ DIR="draft"
23
+ QUIET=0
24
+
25
+ usage() {
26
+ cat <<'EOF'
27
+ okf-check.sh — validate a directory against Open Knowledge Format v0.1 (§9).
28
+
29
+ Usage:
30
+ scripts/tools/okf-check.sh [--dir DIR] [--quiet]
31
+
32
+ Flags:
33
+ --dir DIR Bundle root to validate (default: draft).
34
+ --quiet Print only the summary line, not per-file violations.
35
+ --help Show this help.
36
+
37
+ Exit 0 when conformant, 1 when violations are found, 2 when DIR is absent.
38
+ EOF
39
+ }
40
+
41
+ while [[ $# -gt 0 ]]; do
42
+ case "$1" in
43
+ --dir) DIR="$2"; shift 2;;
44
+ --quiet) QUIET=1; shift;;
45
+ --help|-h) usage; exit 0;;
46
+ -*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
47
+ *) echo "Unexpected arg: $1" >&2; usage >&2; exit 1;;
48
+ esac
49
+ done
50
+
51
+ [[ -d "$DIR" ]] || { echo "ERROR: --dir '$DIR' is not a directory" >&2; exit 2; }
52
+ DIR="${DIR%/}"
53
+
54
+ # fm_scan FILE -> "STATUS|TYPE|KEYS" (pipe-delimited; '|' is not IFS-whitespace,
55
+ # so empty TYPE/KEYS fields survive `read` instead of collapsing).
56
+ # STATUS: nofm (no frontmatter) | ok (closed block) | unterminated
57
+ # TYPE: value of the top-level `type:` key, if any
58
+ # KEYS: comma-separated top-level frontmatter keys
59
+ fm_scan() {
60
+ awk '
61
+ NR==1 { if ($0 != "---") { print "nofm||"; exit } ; inblock=1; next }
62
+ inblock && /^---[[:space:]]*$/ { print "ok|" type "|" keys; closed=1; exit }
63
+ inblock {
64
+ if (match($0, /^[A-Za-z_][A-Za-z0-9_]*:/)) {
65
+ k = substr($0, 1, RLENGTH-1)
66
+ keys = keys (keys=="" ? "" : ",") k
67
+ if (k == "type") {
68
+ v = substr($0, RLENGTH+1)
69
+ gsub(/^[ \t]+|[ \t]+$/, "", v)
70
+ gsub(/^"|"$/, "", v)
71
+ type = v
72
+ }
73
+ }
74
+ next
75
+ }
76
+ END { if (inblock && !closed) print "unterminated|" type "|" keys }
77
+ ' "$1"
78
+ }
79
+
80
+ violations=0
81
+ concepts=0
82
+ reserved=0
83
+
84
+ report() { # relpath message
85
+ violations=$((violations + 1))
86
+ [[ "$QUIET" == "1" ]] || echo "FAIL $1: $2" >&2
87
+ }
88
+
89
+ while IFS= read -r -d '' file; do
90
+ rel="${file#"$DIR"/}"
91
+ base="$(basename "$file")"
92
+
93
+ case "$base" in
94
+ index.md)
95
+ reserved=$((reserved + 1))
96
+ IFS='|' read -r status _ keys < <(fm_scan "$file")
97
+ if [[ "$status" != "nofm" ]]; then
98
+ if [[ "$rel" == "index.md" ]]; then
99
+ # Bundle-root index.md: frontmatter allowed, but only okf_version (§11).
100
+ IFS=',' read -ra ks <<< "$keys"
101
+ for k in "${ks[@]}"; do
102
+ [[ -z "$k" || "$k" == "okf_version" ]] && continue
103
+ report "$rel" "root index.md frontmatter may only hold 'okf_version' (§11); found '$k'"
104
+ done
105
+ else
106
+ report "$rel" "index.md must not contain frontmatter (§6)"
107
+ fi
108
+ fi
109
+ ;;
110
+ log.md)
111
+ reserved=$((reserved + 1))
112
+ while IFS= read -r h; do
113
+ if ! [[ "$h" =~ ^##[[:space:]]+[0-9]{4}-[0-9]{2}-[0-9]{2} ]]; then
114
+ report "$rel" "log.md date heading not ISO 8601 (§7): '$h'"
115
+ fi
116
+ done < <(grep -E '^## ' "$file" 2>/dev/null || true)
117
+ ;;
118
+ *)
119
+ concepts=$((concepts + 1))
120
+ IFS='|' read -r status type _ < <(fm_scan "$file")
121
+ case "$status" in
122
+ nofm) report "$rel" "missing YAML frontmatter block (§9.1)";;
123
+ unterminated) report "$rel" "unterminated frontmatter block — no closing '---' (§9.1)";;
124
+ ok)
125
+ [[ -n "$type" ]] || report "$rel" "frontmatter missing required non-empty 'type' (§9.2)"
126
+ ;;
127
+ esac
128
+ ;;
129
+ esac
130
+ done < <(find "$DIR" -type f -name '*.md' -print0 | sort -z)
131
+
132
+ if [[ "$violations" -eq 0 ]]; then
133
+ echo "OKF v0.1 conformant — $concepts concept file(s), $reserved reserved file(s), 0 violations. ($DIR)"
134
+ exit 0
135
+ fi
136
+ echo "OKF v0.1 NON-CONFORMANT — $violations violation(s) across $concepts concept + $reserved reserved file(s). ($DIR)" >&2
137
+ exit 1