@drafthq/draft 3.6.0 → 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/installer.js +1 -1
- package/cli/src/lib/cursor-registry.js +10 -0
- 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 +27 -17
- 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 +1 -0
- package/core/shared/graph-query.md +0 -2
- 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/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/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 +1272 -292
- package/integrations/copilot/.github/copilot-instructions.md +1272 -292
- package/package.json +1 -1
- package/scripts/fetch-memory-engine.sh +24 -6
- package/scripts/lib.sh +11 -4
- package/scripts/tools/_graph_queries.sh +10 -3
- package/scripts/tools/_lib.sh +33 -11
- package/scripts/tools/check-repo-size.sh +125 -0
- package/scripts/tools/check-skill-line-caps.sh +1 -2
- package/scripts/tools/check-template-noop.sh +0 -1
- package/scripts/tools/classify-files.sh +2 -2
- package/scripts/tools/cycle-detect.sh +16 -8
- package/scripts/tools/fix-whitespace.sh +3 -0
- package/scripts/tools/git-metadata.sh +6 -0
- package/scripts/tools/graph-arch.sh +1 -1
- package/scripts/tools/graph-callers.sh +6 -3
- package/scripts/tools/graph-impact.sh +4 -2
- package/scripts/tools/graph-init.sh +12 -8
- package/scripts/tools/graph-preflight.sh +3 -0
- package/scripts/tools/graph-query.sh +70 -7
- package/scripts/tools/graph-snapshot.sh +29 -5
- package/scripts/tools/hotspot-rank.sh +2 -1
- package/scripts/tools/install-smoke-test.sh +223 -0
- package/scripts/tools/mermaid-from-graph.sh +2 -1
- package/scripts/tools/migrate-track-frontmatter.sh +9 -10
- package/scripts/tools/okf-coverage-check.sh +4 -3
- package/scripts/tools/okf-fix-links.sh +18 -93
- package/scripts/tools/okf-plan-concepts.sh +5 -8
- package/scripts/tools/okf-render-views.sh +19 -14
- package/scripts/tools/okf-validate-quality.sh +13 -2
- package/scripts/tools/okf-validate.sh +13 -2
- package/scripts/tools/parse-git-log.sh +19 -1
- package/scripts/tools/resolve-tools.sh +24 -8
- package/scripts/tools/verify-citations.sh +1 -2
- package/scripts/tools/verify-doc-anchors.sh +13 -33
- package/scripts/tools/verify-graph-binary.sh +13 -3
- package/skills/GRAPH.md +21 -10
- package/skills/adr/SKILL.md +24 -6
- package/skills/assist-review/SKILL.md +2 -1
- package/skills/bughunt/SKILL.md +54 -11
- package/skills/bughunt/references/regression-tests.md +14 -4
- package/skills/change/SKILL.md +19 -9
- package/skills/coverage/SKILL.md +26 -10
- package/skills/debug/SKILL.md +12 -2
- package/skills/decompose/SKILL.md +39 -9
- package/skills/deep-review/SKILL.md +17 -4
- package/skills/deploy-checklist/SKILL.md +9 -2
- 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 +1 -1
- package/skills/impact/SKILL.md +3 -1
- package/skills/implement/SKILL.md +60 -18
- package/skills/incident-response/SKILL.md +5 -2
- package/skills/init/SKILL.md +102 -23
- package/skills/init/references/architecture-spec.md +31 -7
- package/skills/init/references/okf-emitter.md +3 -3
- package/skills/jira/SKILL.md +33 -13
- package/skills/jira/references/review.md +61 -19
- package/skills/learn/SKILL.md +29 -6
- package/skills/new-track/SKILL.md +73 -18
- package/skills/quick-review/SKILL.md +7 -2
- package/skills/revert/SKILL.md +11 -5
- package/skills/review/SKILL.md +123 -24
- package/skills/review/references/zero-setup-mode.md +117 -0
- package/skills/standup/SKILL.md +9 -1
- package/skills/status/SKILL.md +6 -6
- package/skills/tech-debt/SKILL.md +7 -1
- package/skills/testing-strategy/SKILL.md +10 -2
- package/skills/tour/SKILL.md +2 -1
- package/skills/upload/SKILL.md +3 -2
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
# are rejected before the engine ever sees them).
|
|
9
9
|
# --tool NAME --json '{...}' passthrough to any read-only engine tool
|
|
10
10
|
# (get_code_snippet, search_graph, get_graph_schema,
|
|
11
|
-
# trace_path, …). Destructive tools are rejected
|
|
11
|
+
# trace_path, …). Destructive tools are rejected, and
|
|
12
|
+
# a `query` field in --json is scanned for write
|
|
13
|
+
# verbs exactly like --cypher.
|
|
12
14
|
#
|
|
13
15
|
# Dialect limits (engine v0.8.x — see _graph_queries.sh for the full list):
|
|
14
16
|
# SAFE : fixed-length patterns, `=`, `<`, `STARTS WITH`, `NOT x STARTS WITH`,
|
|
@@ -46,7 +48,8 @@ Flags:
|
|
|
46
48
|
--cypher STR Read-only openCypher query (write verbs CREATE/MERGE/DELETE/SET/
|
|
47
49
|
REMOVE/DROP/DETACH are rejected). The {project} is injected.
|
|
48
50
|
--tool NAME Engine tool to call (read-only allowlist). Combine with --json.
|
|
49
|
-
--json STR JSON args for --tool (the project is injected if absent).
|
|
51
|
+
--json STR JSON args for --tool (the project is injected if absent). A
|
|
52
|
+
`query` field is write-verb checked like --cypher.
|
|
50
53
|
--help Show this help.
|
|
51
54
|
|
|
52
55
|
Dialect: avoid coalesce(), <>, NOT EXISTS, NOT(pattern), WITH-aggregation,
|
|
@@ -89,13 +92,74 @@ if [[ -n "$TOOL" ]]; then
|
|
|
89
92
|
}
|
|
90
93
|
fi
|
|
91
94
|
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
# Drop the contents of every quoted span before scanning for write verbs below.
|
|
96
|
+
# A raw substring grep can't tell a CREATE/SET/DELETE clause from a symbol
|
|
97
|
+
# literally named 'create', 'set', 'delete', etc. — exactly the method names
|
|
98
|
+
# real codebases are full of — so quoted spans must be blanked out first or
|
|
99
|
+
# every query about one of them gets wrongly rejected. Cypher quotes strings
|
|
100
|
+
# with either ' or " and identifiers with `, and the engine accepts all three,
|
|
101
|
+
# so all three must be handled. Escaped quotes (`\'`, produced by gq_escape in
|
|
102
|
+
# _graph_queries.sh) must not be read as closing a span early, and an
|
|
103
|
+
# unterminated span fails closed (rejected) rather than letting the rest of the
|
|
104
|
+
# query — write verbs included — go unscanned.
|
|
105
|
+
strip_quoted_spans() {
|
|
106
|
+
local s="$1" out="" i=0 len ch quote
|
|
107
|
+
len="${#s}"
|
|
108
|
+
while (( i < len )); do
|
|
109
|
+
ch="${s:i:1}"
|
|
110
|
+
if [[ "$ch" != "'" && "$ch" != '"' && "$ch" != '`' ]]; then
|
|
111
|
+
out+="$ch"
|
|
112
|
+
i=$((i + 1))
|
|
113
|
+
continue
|
|
114
|
+
fi
|
|
115
|
+
quote="$ch"
|
|
116
|
+
out+="$quote"
|
|
117
|
+
i=$((i + 1))
|
|
118
|
+
while :; do
|
|
119
|
+
(( i < len )) || return 1
|
|
120
|
+
ch="${s:i:1}"
|
|
121
|
+
if [[ "$ch" == '\' ]]; then
|
|
122
|
+
i=$((i + 2))
|
|
123
|
+
continue
|
|
124
|
+
fi
|
|
125
|
+
i=$((i + 1))
|
|
126
|
+
if [[ "$ch" == "$quote" ]]; then
|
|
127
|
+
out+="$quote"
|
|
128
|
+
break
|
|
129
|
+
fi
|
|
130
|
+
done
|
|
131
|
+
done
|
|
132
|
+
printf '%s' "$out"
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Reject write verbs BEFORE the engine sees the query.
|
|
136
|
+
reject_write_verbs() {
|
|
137
|
+
local stripped upper
|
|
138
|
+
stripped="$(strip_quoted_spans "$1")" || {
|
|
139
|
+
echo "ERROR: write verbs are not allowed (read-only passthrough)" >&2
|
|
140
|
+
exit 1
|
|
141
|
+
}
|
|
142
|
+
upper="$(printf '%s' "$stripped" | tr '[:lower:]' '[:upper:]')"
|
|
143
|
+
if printf '%s' "$upper" | grep -Eqw 'CREATE|MERGE|DELETE|SET|REMOVE|DROP|DETACH'; then
|
|
96
144
|
echo "ERROR: write verbs are not allowed (read-only passthrough)" >&2
|
|
97
145
|
exit 1
|
|
98
146
|
fi
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if [[ -n "$CYPHER" ]]; then
|
|
150
|
+
reject_write_verbs "$CYPHER"
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
# --tool carries Cypher too: query_graph takes it in the payload's `query` field,
|
|
154
|
+
# so guarding only --cypher left the read-only contract fully bypassable via
|
|
155
|
+
# `--tool query_graph --json '{"query":"MATCH (n) DETACH DELETE n"}'`. Scan any
|
|
156
|
+
# `query` a tool payload carries, not just query_graph's, so a future
|
|
157
|
+
# query-bearing tool is covered by construction. jq-gated: without jq nothing can
|
|
158
|
+
# reach the engine anyway (graph_bootstrap fails first).
|
|
159
|
+
if [[ -n "$TOOL" ]] && command -v jq >/dev/null 2>&1; then
|
|
160
|
+
echo "$TOOL_JSON" | jq -e . >/dev/null 2>&1 || { echo "ERROR: --json is not valid JSON" >&2; exit 1; }
|
|
161
|
+
TOOL_QUERY="$(echo "$TOOL_JSON" | jq -r '.query // empty' 2>/dev/null || true)"
|
|
162
|
+
[[ -z "$TOOL_QUERY" ]] || reject_write_verbs "$TOOL_QUERY"
|
|
99
163
|
fi
|
|
100
164
|
|
|
101
165
|
unavailable() { echo '{"source":"unavailable"}'; exit 2; }
|
|
@@ -108,7 +172,6 @@ if [[ -n "$CYPHER" ]]; then
|
|
|
108
172
|
printf '%s\n' "$RES"
|
|
109
173
|
else
|
|
110
174
|
# Inject the resolved project into the tool args unless the caller set one.
|
|
111
|
-
echo "$TOOL_JSON" | jq -e . >/dev/null 2>&1 || { echo "ERROR: --json is not valid JSON" >&2; exit 1; }
|
|
112
175
|
ARGS="$(echo "$TOOL_JSON" | jq -c --arg p "$PROJECT" 'if has("project") then . else . + {project:$p} end')"
|
|
113
176
|
RES="$(memory_cli "$TOOL" "$ARGS" 2>/dev/null || true)"
|
|
114
177
|
[[ -n "$RES" ]] || unavailable
|
|
@@ -73,17 +73,41 @@ command -v jq >/dev/null 2>&1 || { echo "jq required" >&2; exit 2; }
|
|
|
73
73
|
PROJECT="$(memory_ensure_index "$REPO_ABS" || true)"
|
|
74
74
|
[[ -n "$PROJECT" ]] || { echo "could not index repo — nothing written" >&2; exit 2; }
|
|
75
75
|
|
|
76
|
+
# ...then ALWAYS re-index. memory_ensure_index calls index_repository only when the
|
|
77
|
+
# project is ABSENT — correct for the graph-*.sh query wrappers, which must stay
|
|
78
|
+
# cheap — so on an already-indexed repo this tool used to write a gate marker with a
|
|
79
|
+
# fresh `generated_at` over a frozen index: a deleted symbol stayed resolvable, a new
|
|
80
|
+
# one never appeared, and nothing in the output said so. Refreshing is this tool's
|
|
81
|
+
# entire job. The engine indexes incrementally, so the repeat call is cheap.
|
|
82
|
+
REFRESHED="$(memory_index_bounded "$REPO_ABS" 2>/dev/null | jq -r '.project // empty' 2>/dev/null || true)"
|
|
83
|
+
[[ -n "$REFRESHED" ]] && PROJECT="$REFRESHED"
|
|
84
|
+
|
|
76
85
|
mkdir -p "$OUT"
|
|
77
86
|
|
|
78
87
|
# Prune any stale fat-snapshot artifacts from a prior (pre-engine-only) run so a
|
|
79
88
|
# re-index migrates the repo to the thin model.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
#
|
|
90
|
+
# Gated on $OUT carrying positive evidence that Draft owns it. --out is
|
|
91
|
+
# caller-supplied and the mkdir -p above will happily create a typo'd path, so an
|
|
92
|
+
# ungated `rm -rf "$OUT/okf"` turns a mistyped flag into data loss. Evidence is
|
|
93
|
+
# the default location, a marker from a previous run, or a prior fat snapshot
|
|
94
|
+
# (which always carried architecture.json / hotspots.jsonl).
|
|
95
|
+
draft_owns_out_dir() {
|
|
96
|
+
[[ "$OUT" == "$REPO_ABS/draft/graph" ]] && return 0
|
|
97
|
+
[[ -f "$OUT/schema.yaml" ]] && return 0
|
|
98
|
+
[[ -f "$OUT/architecture.json" || -f "$OUT/hotspots.jsonl" ]] && return 0
|
|
99
|
+
return 1
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if draft_owns_out_dir; then
|
|
103
|
+
rm -f "$OUT/architecture.json" "$OUT/hotspots.jsonl" \
|
|
104
|
+
"$OUT/module-deps.mermaid" "$OUT/proto-map.mermaid" 2>/dev/null || true
|
|
105
|
+
rm -rf "$OUT/okf" 2>/dev/null || true
|
|
106
|
+
fi
|
|
83
107
|
|
|
84
108
|
# schema.yaml — provenance + gate. Counts are point-of-index provenance only;
|
|
85
109
|
# the live engine is authoritative.
|
|
86
|
-
STATUS_JSON="$(memory_cli index_status "
|
|
110
|
+
STATUS_JSON="$(memory_cli index_status "$(jq -n --arg p "$PROJECT" '{project:$p}')" || echo '{}')"
|
|
87
111
|
# Tolerate field-name variation AND non-JSON output across engine versions;
|
|
88
112
|
# counts are provenance only and must never abort the gate-marker write.
|
|
89
113
|
NODES="$(echo "$STATUS_JSON" | jq -r '.nodes // .node_count // .total_nodes // 0' 2>/dev/null || echo 0)"
|
|
@@ -94,7 +118,7 @@ VER="$("$MEMORY_BIN" --version 2>/dev/null | awk '{print $NF}' || echo unknown)"
|
|
|
94
118
|
# incrementally (content-based, git-aware), so re-indexing only touches changed
|
|
95
119
|
# files. detect_changes reports that working-tree delta — recorded as provenance
|
|
96
120
|
# and echoed so a refresh shows what moved. Best-effort: never aborts the write.
|
|
97
|
-
CHANGES_JSON="$(memory_cli detect_changes "
|
|
121
|
+
CHANGES_JSON="$(memory_cli detect_changes "$(jq -n --arg p "$PROJECT" '{project:$p}')" 2>/dev/null || echo '{}')"
|
|
98
122
|
echo "$CHANGES_JSON" | jq -e . >/dev/null 2>&1 || CHANGES_JSON='{}'
|
|
99
123
|
CHANGED_FILES="$(echo "$CHANGES_JSON" | jq -r '.changed_count // (.changed_files | length?) // 0' 2>/dev/null || echo 0)"
|
|
100
124
|
IMPACTED="$(echo "$CHANGES_JSON" | jq -r '(.impacted_symbols | length?) // 0' 2>/dev/null || echo 0)"
|
|
@@ -65,7 +65,8 @@ unavailable() { echo '{"hotspots":[],"source":"unavailable"}'; exit 2; }
|
|
|
65
65
|
|
|
66
66
|
graph_bootstrap "$REPO" || unavailable
|
|
67
67
|
|
|
68
|
-
ARCH_JSON="$(memory_cli get_architecture
|
|
68
|
+
ARCH_JSON="$(memory_cli get_architecture \
|
|
69
|
+
"$(jq -n --arg p "$PROJECT" '{project:$p, aspects:["hotspots"]}')" || true)"
|
|
69
70
|
[[ -n "$ARCH_JSON" ]] || unavailable
|
|
70
71
|
echo "$ARCH_JSON" | jq -e . >/dev/null 2>&1 || unavailable
|
|
71
72
|
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# install-smoke-test.sh
|
|
3
|
+
#
|
|
4
|
+
# Exercises the real first-install path on a throwaway clone.
|
|
5
|
+
#
|
|
6
|
+
# The v2.8.3 install hang was invisible to `make test` because every suite runs
|
|
7
|
+
# against the working tree, and the working tree is never what a new user gets.
|
|
8
|
+
# A new user gets `git clone --depth 1` followed by manifest discovery and a
|
|
9
|
+
# writer pass over an empty HOME. This reproduces exactly that, in that order,
|
|
10
|
+
# so a regression in any of those steps fails a build instead of a user.
|
|
11
|
+
#
|
|
12
|
+
# Requires: git, node 18+. Does NOT require the `claude` CLI and never touches
|
|
13
|
+
# the network — the graph-engine fetch is stubbed out via --no-graph.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# scripts/tools/install-smoke-test.sh # clone this repo, run all checks
|
|
17
|
+
# scripts/tools/install-smoke-test.sh --repo <path> # smoke-test another checkout
|
|
18
|
+
# scripts/tools/install-smoke-test.sh --keep # leave the sandbox for inspection
|
|
19
|
+
# scripts/tools/install-smoke-test.sh --json
|
|
20
|
+
#
|
|
21
|
+
# Exit codes:
|
|
22
|
+
# 0 install path is healthy
|
|
23
|
+
# 1 a check failed
|
|
24
|
+
# 2 usage / runtime error
|
|
25
|
+
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
usage() {
|
|
29
|
+
cat <<'EOF'
|
|
30
|
+
install-smoke-test.sh — clean-clone install smoke test
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--repo <path> Repository to clone (default: this repo)
|
|
34
|
+
--keep Do not delete the sandbox on exit
|
|
35
|
+
--json Emit JSON instead of human-readable output
|
|
36
|
+
--help, -h Show this message
|
|
37
|
+
EOF
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
|
|
41
|
+
usage
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
46
|
+
# shellcheck source=_lib.sh
|
|
47
|
+
source "$SCRIPT_DIR/_lib.sh"
|
|
48
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
49
|
+
KEEP=0
|
|
50
|
+
EMIT_JSON=0
|
|
51
|
+
|
|
52
|
+
while [[ $# -gt 0 ]]; do
|
|
53
|
+
case "$1" in
|
|
54
|
+
--repo)
|
|
55
|
+
[[ -n "${2:-}" ]] || { echo "install-smoke-test: --repo requires a value" >&2; exit 2; }
|
|
56
|
+
REPO_ROOT="$2"; shift 2 ;;
|
|
57
|
+
--keep) KEEP=1; shift ;;
|
|
58
|
+
--json) EMIT_JSON=1; shift ;;
|
|
59
|
+
--help|-h) usage; exit 0 ;;
|
|
60
|
+
*) echo "install-smoke-test: unknown argument '$1'" >&2; usage >&2; exit 2 ;;
|
|
61
|
+
esac
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
[[ -d "$REPO_ROOT" ]] || { echo "install-smoke-test: no such directory: $REPO_ROOT" >&2; exit 2; }
|
|
65
|
+
command -v git >/dev/null || { echo "install-smoke-test: git not found" >&2; exit 2; }
|
|
66
|
+
command -v node >/dev/null || { echo "install-smoke-test: node not found" >&2; exit 2; }
|
|
67
|
+
|
|
68
|
+
SANDBOX="$(mktemp -d "${TMPDIR:-/tmp}/draft-install-smoke.XXXXXX")"
|
|
69
|
+
cleanup() { [[ "$KEEP" -eq 1 ]] || rm -rf "$SANDBOX"; }
|
|
70
|
+
trap cleanup EXIT
|
|
71
|
+
|
|
72
|
+
CLONE="$SANDBOX/clone"
|
|
73
|
+
FAKE_HOME="$SANDBOX/home"
|
|
74
|
+
PROJECT="$SANDBOX/project"
|
|
75
|
+
mkdir -p "$FAKE_HOME" "$PROJECT"
|
|
76
|
+
|
|
77
|
+
FAILURES=()
|
|
78
|
+
RESULTS=()
|
|
79
|
+
|
|
80
|
+
# Collapse a file to one truncated line. Done in-shell rather than with
|
|
81
|
+
# `| cut -c1-200` because an early-closing reader plus `pipefail` turns a
|
|
82
|
+
# harmless truncation into a SIGPIPE failure of the whole script.
|
|
83
|
+
oneline() {
|
|
84
|
+
local text
|
|
85
|
+
text="$(tr '\n' ' ' < "$1")"
|
|
86
|
+
printf '%s' "${text:0:200}"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
lastlines() {
|
|
90
|
+
local text
|
|
91
|
+
text="$(tail -n "$2" "$1" | tr '\n' ' ')"
|
|
92
|
+
printf '%s' "${text:0:200}"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
record() {
|
|
96
|
+
local name="$1" ok="$2" detail="${3:-}"
|
|
97
|
+
RESULTS+=("$name|$ok|$detail")
|
|
98
|
+
if [[ "$ok" == "true" ]]; then
|
|
99
|
+
[[ "$EMIT_JSON" -eq 1 ]] || echo " PASS: $name${detail:+ — $detail}"
|
|
100
|
+
else
|
|
101
|
+
[[ "$EMIT_JSON" -eq 1 ]] || echo " FAIL: $name${detail:+ — $detail}"
|
|
102
|
+
FAILURES+=("$name")
|
|
103
|
+
fi
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
[[ "$EMIT_JSON" -eq 1 ]] || echo "=== Install smoke test (sandbox: $SANDBOX) ==="
|
|
107
|
+
|
|
108
|
+
# 1. Shallow clone — the literal first step of `plugin marketplace add`.
|
|
109
|
+
# file:// forces the real transfer path; a plain local path would hardlink
|
|
110
|
+
# and hide size problems.
|
|
111
|
+
if git clone --depth 1 --quiet "file://$REPO_ROOT" "$CLONE" 2>"$SANDBOX/clone.err"; then
|
|
112
|
+
record "shallow clone succeeds" "true"
|
|
113
|
+
else
|
|
114
|
+
record "shallow clone succeeds" "false" "$(oneline "$SANDBOX/clone.err")"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
if [[ -d "$CLONE" ]]; then
|
|
118
|
+
clone_mb="$(du -sm "$CLONE" | cut -f1)"
|
|
119
|
+
record "clone size recorded" "true" "${clone_mb} MB on disk"
|
|
120
|
+
|
|
121
|
+
# 2. Manifest discovery — install fails here if plugin.json is malformed or
|
|
122
|
+
# points at a skills directory the clone does not contain.
|
|
123
|
+
if node -e '
|
|
124
|
+
const fs = require("fs"), path = require("path");
|
|
125
|
+
const root = process.argv[1];
|
|
126
|
+
const plugin = JSON.parse(fs.readFileSync(path.join(root, ".claude-plugin/plugin.json"), "utf8"));
|
|
127
|
+
const market = JSON.parse(fs.readFileSync(path.join(root, ".claude-plugin/marketplace.json"), "utf8"));
|
|
128
|
+
if (!plugin.name) throw new Error("plugin.json has no name");
|
|
129
|
+
if (!Array.isArray(market.plugins) || market.plugins.length === 0) throw new Error("marketplace.json lists no plugins");
|
|
130
|
+
const skills = typeof plugin.skills === "string" ? plugin.skills : "./skills/";
|
|
131
|
+
const dir = path.join(root, skills);
|
|
132
|
+
if (!fs.existsSync(dir)) throw new Error("skills dir missing from clone: " + skills);
|
|
133
|
+
if (fs.readdirSync(dir).length === 0) throw new Error("skills dir is empty in clone");
|
|
134
|
+
' "$CLONE" 2>"$SANDBOX/manifest.err"; then
|
|
135
|
+
record "plugin manifests resolve in the clone" "true"
|
|
136
|
+
else
|
|
137
|
+
record "plugin manifests resolve in the clone" "false" "$(oneline "$SANDBOX/manifest.err")"
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# 3. Every shipped skill must carry name+description frontmatter, or the
|
|
141
|
+
# host silently drops it at discovery time.
|
|
142
|
+
bad_skills=0
|
|
143
|
+
skill_count=0
|
|
144
|
+
while IFS= read -r skill; do
|
|
145
|
+
skill_count=$((skill_count + 1))
|
|
146
|
+
head -n 1 "$skill" | grep -q '^---$' || { bad_skills=$((bad_skills + 1)); continue; }
|
|
147
|
+
fm="$(sed -n '2,/^---$/p' "$skill")"
|
|
148
|
+
grep -q '^name:' <<< "$fm" || { bad_skills=$((bad_skills + 1)); continue; }
|
|
149
|
+
grep -q '^description:' <<< "$fm" || bad_skills=$((bad_skills + 1))
|
|
150
|
+
done < <(find "$CLONE/skills" -name SKILL.md -type f 2>/dev/null | sort)
|
|
151
|
+
|
|
152
|
+
if [[ "$skill_count" -eq 0 ]]; then
|
|
153
|
+
record "shipped skills discoverable" "false" "no SKILL.md files in clone"
|
|
154
|
+
else
|
|
155
|
+
record "shipped skills discoverable" \
|
|
156
|
+
"$([[ "$bad_skills" -eq 0 ]] && echo true || echo false)" \
|
|
157
|
+
"$skill_count skills, $bad_skills malformed"
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# 4. Writer pass against an empty HOME, per host. --dry-run plans every
|
|
161
|
+
# write without touching disk; --no-graph keeps it offline.
|
|
162
|
+
hosts="$(cd "$CLONE" && node -e '
|
|
163
|
+
const { hosts } = require("./cli/src/hosts");
|
|
164
|
+
console.log(hosts.map((h) => h.id).join(" "));
|
|
165
|
+
' 2>/dev/null || true)"
|
|
166
|
+
|
|
167
|
+
if [[ -z "$hosts" ]]; then
|
|
168
|
+
record "host list enumerable" "false" "cli/src/hosts did not load"
|
|
169
|
+
else
|
|
170
|
+
record "host list enumerable" "true" "$hosts"
|
|
171
|
+
for host in $hosts; do
|
|
172
|
+
if (cd "$PROJECT" && HOME="$FAKE_HOME" node "$CLONE/cli/bin/draft.js" \
|
|
173
|
+
install "$host" --dry-run --no-graph >"$SANDBOX/$host.log" 2>&1); then
|
|
174
|
+
record "install --dry-run: $host" "true"
|
|
175
|
+
else
|
|
176
|
+
record "install --dry-run: $host" "false" "$(lastlines "$SANDBOX/$host.log" 3)"
|
|
177
|
+
fi
|
|
178
|
+
done
|
|
179
|
+
# A dry run that writes is worse than one that fails — it means the real
|
|
180
|
+
# installer's plan/apply split leaks. Both destinations must be checked:
|
|
181
|
+
# codex and opencode default to project scope and write AGENTS.md into the
|
|
182
|
+
# cwd, so a HOME-only assertion misses the likeliest leak.
|
|
183
|
+
record "dry run left HOME untouched" \
|
|
184
|
+
"$([[ -z "$(ls -A "$FAKE_HOME")" ]] && echo true || echo false)" \
|
|
185
|
+
"$(ls -A "$FAKE_HOME" | tr '\n' ' ')"
|
|
186
|
+
record "dry run left the project dir untouched" \
|
|
187
|
+
"$([[ -z "$(ls -A "$PROJECT")" ]] && echo true || echo false)" \
|
|
188
|
+
"$(ls -A "$PROJECT" | tr '\n' ' ')"
|
|
189
|
+
fi
|
|
190
|
+
|
|
191
|
+
# 5. Engine fetcher is present and self-documenting (never invoked here —
|
|
192
|
+
# it downloads).
|
|
193
|
+
if [[ -x "$CLONE/scripts/fetch-memory-engine.sh" ]]; then
|
|
194
|
+
record "graph engine fetcher shipped and executable" "true"
|
|
195
|
+
else
|
|
196
|
+
record "graph engine fetcher shipped and executable" "false" "scripts/fetch-memory-engine.sh missing or not executable"
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
if [[ "$EMIT_JSON" -eq 1 ]]; then
|
|
201
|
+
printf '{"sandbox":"%s","failures":%d,"checks":[' "$SANDBOX" "${#FAILURES[@]}"
|
|
202
|
+
sep=""
|
|
203
|
+
for r in "${RESULTS[@]}"; do
|
|
204
|
+
IFS='|' read -r name ok detail <<< "$r"
|
|
205
|
+
# json_escape (from _lib.sh) also handles backslashes and control chars —
|
|
206
|
+
# a Windows path or a stack trace in a captured stderr line used to emit
|
|
207
|
+
# invalid JSON.
|
|
208
|
+
printf '%s{"name":"%s","ok":%s,"detail":"%s"}' "$sep" "$(json_escape "$name")" "$ok" "$(json_escape "$detail")"
|
|
209
|
+
sep=","
|
|
210
|
+
done
|
|
211
|
+
printf ']}\n'
|
|
212
|
+
else
|
|
213
|
+
echo ""
|
|
214
|
+
if [[ "${#FAILURES[@]}" -eq 0 ]]; then
|
|
215
|
+
echo "Install path healthy — ${#RESULTS[@]} checks passed."
|
|
216
|
+
else
|
|
217
|
+
echo "Install path BROKEN — ${#FAILURES[@]} of ${#RESULTS[@]} checks failed:"
|
|
218
|
+
printf ' - %s\n' "${FAILURES[@]}"
|
|
219
|
+
fi
|
|
220
|
+
fi
|
|
221
|
+
|
|
222
|
+
[[ "${#FAILURES[@]}" -eq 0 ]] || exit 1
|
|
223
|
+
exit 0
|
|
@@ -92,7 +92,8 @@ render_co_change() {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
render_proto_map() {
|
|
95
|
-
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 '{}')"
|
|
96
97
|
local edges; edges="$(echo "${res:-{\}}" | jq -r '(.routes // [])[] | " \"" + ((.method // "")|tostring) + " " + ((.path // "")|tostring) + "\" --> \"" + ((.handler // "?")|tostring) + "\""' 2>/dev/null || true)"
|
|
97
98
|
if [[ -z "$edges" ]]; then return 1; fi
|
|
98
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
|
|
@@ -225,7 +220,11 @@ EOF
|
|
|
225
220
|
(( BACKUP )) && cp "$path" "$path.bak"
|
|
226
221
|
# %s\n restores the EOF newline stripped by command substitution
|
|
227
222
|
# (same pattern as fix-whitespace.sh).
|
|
228
|
-
local _tmp; _tmp="$(mktemp "${path}.XXXXXX")"
|
|
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"
|
|
229
228
|
printf 'migrate: stripped ephemeral frontmatter from %s\n' "$path"
|
|
230
229
|
fi
|
|
231
230
|
fi
|
|
@@ -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
|
|
@@ -20,7 +20,6 @@ DRAFT=""
|
|
|
20
20
|
FILE=""
|
|
21
21
|
WIKI=""
|
|
22
22
|
DO_FIX=0
|
|
23
|
-
DO_CHECK=1
|
|
24
23
|
|
|
25
24
|
usage() {
|
|
26
25
|
cat <<'EOF'
|
|
@@ -35,7 +34,7 @@ Flags:
|
|
|
35
34
|
--file FILE single markdown file to rewrite/check
|
|
36
35
|
--wiki DIR wiki bundle (required with --file for basename map)
|
|
37
36
|
--fix rewrite broken/ambiguous links in place
|
|
38
|
-
--check
|
|
37
|
+
--check accepted for symmetry with --fix; checking is unconditional
|
|
39
38
|
--help
|
|
40
39
|
|
|
41
40
|
Exit: 0 clean, 1 broken links remain, 2 bad invocation.
|
|
@@ -48,7 +47,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
48
47
|
--file) FILE="${2:?--file requires a value}"; shift 2;;
|
|
49
48
|
--wiki) WIKI="${2:?--wiki requires a value}"; shift 2;;
|
|
50
49
|
--fix) DO_FIX=1; shift;;
|
|
51
|
-
--check)
|
|
50
|
+
--check) shift;; # checking always runs; accepted so callers can be explicit
|
|
52
51
|
--help|-h) usage; exit 0;;
|
|
53
52
|
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 2;;
|
|
54
53
|
*) echo "Unexpected arg: $1" >&2; exit 2;;
|
|
@@ -65,83 +64,17 @@ fi
|
|
|
65
64
|
[[ -n "$FILE" && -f "$FILE" ]] || { echo "ERROR: --file or --draft/architecture.md required" >&2; exit 2; }
|
|
66
65
|
[[ -n "$WIKI" && -d "$WIKI" ]] || { echo "ERROR: wiki dir required" >&2; exit 2; }
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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]}"
|
|
67
|
+
command -v python3 >/dev/null 2>&1 || {
|
|
68
|
+
echo "ERROR: python3 is required by ${0##*/} (link rewriting and validation)" >&2
|
|
69
|
+
exit 2
|
|
103
70
|
}
|
|
104
71
|
|
|
105
|
-
#
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
}
|
|
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.
|
|
124
75
|
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
}
|
|
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.
|
|
145
78
|
|
|
146
79
|
fix_file() {
|
|
147
80
|
local src="$1"
|
|
@@ -239,11 +172,8 @@ def repl(m):
|
|
|
239
172
|
new = re.sub(r"\[([^\]]*)\]\(([^)]+)\)", repl, text)
|
|
240
173
|
out.write_text(new if new.endswith("\n") else new + "\n", encoding="utf-8")
|
|
241
174
|
PY
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
else
|
|
245
|
-
rm -f "$tmp"
|
|
246
|
-
fi
|
|
175
|
+
apply_dest_mode "$tmp" "$src" # mktemp is 0600; mv would strip the dest's mode
|
|
176
|
+
mv "$tmp" "$src"
|
|
247
177
|
}
|
|
248
178
|
|
|
249
179
|
if [[ $DO_FIX -eq 1 ]]; then
|
|
@@ -254,16 +184,7 @@ if [[ $DO_FIX -eq 1 ]]; then
|
|
|
254
184
|
fi
|
|
255
185
|
fi
|
|
256
186
|
|
|
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
|
|
187
|
+
# Check — the single source of link/anchor truth
|
|
267
188
|
broken_out="$(python3 - "$FILE" "${DRAFT:-$(dirname "$FILE")}" "$WIKI" <<'PY'
|
|
268
189
|
import sys, re
|
|
269
190
|
from pathlib import Path
|
|
@@ -304,7 +225,11 @@ for m in re.finditer(r"\[([^\]]*)\]\(([^)]+)\)", text):
|
|
|
304
225
|
path = href.split("#",1)[0]
|
|
305
226
|
if not path:
|
|
306
227
|
ok += 1; continue
|
|
307
|
-
|
|
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]
|
|
308
233
|
if any(c.exists() for c in cands):
|
|
309
234
|
ok += 1
|
|
310
235
|
else:
|
|
@@ -175,7 +175,7 @@ plan_from_cargo_workspace() {
|
|
|
175
175
|
[[ -f "$cargo" ]] || return 1
|
|
176
176
|
# Only treat as workspace if [workspace] present with members.
|
|
177
177
|
grep -qE '^\[workspace\]' "$cargo" || return 1
|
|
178
|
-
grep -qE 'members
|
|
178
|
+
grep -qE 'members[[:space:]]*=' "$cargo" || return 1
|
|
179
179
|
|
|
180
180
|
local members=()
|
|
181
181
|
# Extract quoted paths inside the members = [ ... ] array (possibly multi-line).
|
|
@@ -424,13 +424,10 @@ else
|
|
|
424
424
|
plan_from_cargo_workspace || true
|
|
425
425
|
plan_from_npm_workspaces || true
|
|
426
426
|
plan_from_go_modules || true
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
DISCOVERY_META+=("heuristic")
|
|
432
|
-
fi
|
|
433
|
-
# If graph failed but we have cargo/npm/go, still success.
|
|
427
|
+
plan_from_graph || true
|
|
428
|
+
# Last resort only: any of cargo/npm/go/graph succeeding leaves E_ID non-empty
|
|
429
|
+
# and skips this. Re-running the heuristic a second time can never add what
|
|
430
|
+
# the first pass didn't find, so there is one call site, not two.
|
|
434
431
|
if [[ ${#E_ID[@]} -eq 0 ]]; then
|
|
435
432
|
plan_from_heuristic
|
|
436
433
|
DISCOVERY_META+=("heuristic")
|