@drafthq/draft 2.7.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.
- package/.claude-plugin/marketplace.json +38 -0
- package/.claude-plugin/plugin.json +26 -0
- package/LICENSE +21 -0
- package/README.md +272 -0
- package/bin/README.md +49 -0
- package/cli/bin/draft.js +13 -0
- package/cli/src/cli.js +113 -0
- package/cli/src/hosts/claude-code.js +46 -0
- package/cli/src/hosts/codex.js +33 -0
- package/cli/src/hosts/cursor.js +50 -0
- package/cli/src/hosts/index.js +24 -0
- package/cli/src/hosts/opencode.js +39 -0
- package/cli/src/installer.js +61 -0
- package/cli/src/lib/fsx.js +34 -0
- package/cli/src/lib/graph.js +23 -0
- package/cli/src/lib/log.js +32 -0
- package/cli/src/lib/paths.js +14 -0
- package/core/agents/architect.md +338 -0
- package/core/agents/debugger.md +193 -0
- package/core/agents/ops.md +104 -0
- package/core/agents/planner.md +158 -0
- package/core/agents/rca.md +314 -0
- package/core/agents/reviewer.md +256 -0
- package/core/agents/writer.md +110 -0
- package/core/guardrails/README.md +4 -0
- package/core/guardrails/code-quality.md +4 -0
- package/core/guardrails/dependency-triage.md +4 -0
- package/core/guardrails/design-norms.md +4 -0
- package/core/guardrails/language-standards.md +4 -0
- package/core/guardrails/review-checks.md +4 -0
- package/core/guardrails/secure-patterns.md +4 -0
- package/core/guardrails/security.md +4 -0
- package/core/guardrails.md +22 -0
- package/core/knowledge-base.md +127 -0
- package/core/methodology.md +1221 -0
- package/core/shared/condensation.md +224 -0
- package/core/shared/context-verify.md +44 -0
- package/core/shared/cross-skill-dispatch.md +127 -0
- package/core/shared/discovery-schema.md +75 -0
- package/core/shared/draft-context-loading.md +282 -0
- package/core/shared/git-report-metadata.md +106 -0
- package/core/shared/graph-query.md +239 -0
- package/core/shared/graph-usage-report.md +22 -0
- package/core/shared/jira-sync.md +170 -0
- package/core/shared/parallel-analysis.md +386 -0
- package/core/shared/parallel-fanout.md +10 -0
- package/core/shared/pattern-learning.md +146 -0
- package/core/shared/red-flags.md +58 -0
- package/core/shared/template-contract.md +22 -0
- package/core/shared/template-hygiene.md +10 -0
- package/core/shared/tool-resolver.md +10 -0
- package/core/shared/vcs-commands.md +97 -0
- package/core/shared/verification-gates.md +47 -0
- package/core/templates/CHANGELOG.md +70 -0
- package/core/templates/ai-context-export.md +8 -0
- package/core/templates/ai-context.md +270 -0
- package/core/templates/ai-profile.md +41 -0
- package/core/templates/architecture.md +203 -0
- package/core/templates/dependency-graph.md +103 -0
- package/core/templates/discovery.md +79 -0
- package/core/templates/guardrails.md +143 -0
- package/core/templates/hld.md +327 -0
- package/core/templates/intake-questions.md +403 -0
- package/core/templates/jira.md +119 -0
- package/core/templates/lld.md +283 -0
- package/core/templates/metadata.json +66 -0
- package/core/templates/plan.md +130 -0
- package/core/templates/product.md +110 -0
- package/core/templates/rca.md +86 -0
- package/core/templates/root-architecture.md +127 -0
- package/core/templates/root-product.md +53 -0
- package/core/templates/root-tech-stack.md +117 -0
- package/core/templates/service-index.md +55 -0
- package/core/templates/session-summary.md +8 -0
- package/core/templates/spec.md +165 -0
- package/core/templates/tech-matrix.md +101 -0
- package/core/templates/tech-stack.md +169 -0
- package/core/templates/track-architecture.md +311 -0
- package/core/templates/workflow.md +187 -0
- package/integrations/agents/AGENTS.md +24384 -0
- package/integrations/copilot/.github/copilot-instructions.md +24384 -0
- package/integrations/gemini/.gemini.md +26 -0
- package/package.json +53 -0
- package/scripts/fetch-memory-engine.sh +116 -0
- package/scripts/lib.sh +256 -0
- package/scripts/tools/_lib.sh +220 -0
- package/scripts/tools/adr-index.sh +117 -0
- package/scripts/tools/check-graph-usage-report.sh +95 -0
- package/scripts/tools/check-scope-conflicts.sh +139 -0
- package/scripts/tools/check-skill-line-caps.sh +115 -0
- package/scripts/tools/check-template-noop.sh +87 -0
- package/scripts/tools/check-track-hygiene.sh +230 -0
- package/scripts/tools/classify-files.sh +231 -0
- package/scripts/tools/cycle-detect.sh +75 -0
- package/scripts/tools/detect-test-framework.sh +135 -0
- package/scripts/tools/diff-templates-vs-tracks.sh +176 -0
- package/scripts/tools/emit-skill-metrics.sh +71 -0
- package/scripts/tools/fix-whitespace.sh +192 -0
- package/scripts/tools/freshness-check.sh +143 -0
- package/scripts/tools/git-metadata.sh +203 -0
- package/scripts/tools/graph-callers.sh +74 -0
- package/scripts/tools/graph-impact.sh +93 -0
- package/scripts/tools/graph-snapshot.sh +102 -0
- package/scripts/tools/hotspot-rank.sh +75 -0
- package/scripts/tools/manage-symlinks.sh +85 -0
- package/scripts/tools/mermaid-from-graph.sh +92 -0
- package/scripts/tools/migrate-track-frontmatter.sh +241 -0
- package/scripts/tools/parse-git-log.sh +135 -0
- package/scripts/tools/parse-reports.sh +114 -0
- package/scripts/tools/render-track.sh +145 -0
- package/scripts/tools/run-coverage.sh +153 -0
- package/scripts/tools/scan-markers.sh +144 -0
- package/scripts/tools/skill-caps.conf +24 -0
- package/scripts/tools/validate-frontmatter.sh +125 -0
- package/scripts/tools/verify-citations.sh +250 -0
- package/scripts/tools/verify-doc-anchors.sh +204 -0
- package/scripts/tools/verify-graph-binary.sh +154 -0
- package/skills/GRAPH.md +332 -0
- package/skills/adr/SKILL.md +374 -0
- package/skills/assist-review/SKILL.md +49 -0
- package/skills/bughunt/SKILL.md +668 -0
- package/skills/bughunt/references/regression-tests.md +399 -0
- package/skills/change/SKILL.md +267 -0
- package/skills/coverage/SKILL.md +336 -0
- package/skills/debug/SKILL.md +201 -0
- package/skills/decompose/SKILL.md +656 -0
- package/skills/deep-review/SKILL.md +326 -0
- package/skills/deploy-checklist/SKILL.md +254 -0
- package/skills/discover/SKILL.md +66 -0
- package/skills/docs/SKILL.md +42 -0
- package/skills/documentation/SKILL.md +197 -0
- package/skills/draft/SKILL.md +177 -0
- package/skills/draft/context-files.md +57 -0
- package/skills/draft/intent-mapping.md +37 -0
- package/skills/draft/quality-guide.md +51 -0
- package/skills/graph/SKILL.md +107 -0
- package/skills/impact/SKILL.md +86 -0
- package/skills/implement/SKILL.md +794 -0
- package/skills/incident-response/SKILL.md +245 -0
- package/skills/index/SKILL.md +848 -0
- package/skills/init/SKILL.md +1784 -0
- package/skills/init/references/architecture-spec.md +1259 -0
- package/skills/integrations/SKILL.md +53 -0
- package/skills/jira/SKILL.md +577 -0
- package/skills/jira/references/review.md +1322 -0
- package/skills/learn/SKILL.md +478 -0
- package/skills/new-track/SKILL.md +841 -0
- package/skills/ops/SKILL.md +57 -0
- package/skills/plan/SKILL.md +60 -0
- package/skills/quick-review/SKILL.md +216 -0
- package/skills/revert/SKILL.md +178 -0
- package/skills/review/SKILL.md +1114 -0
- package/skills/standup/SKILL.md +183 -0
- package/skills/status/SKILL.md +183 -0
- package/skills/tech-debt/SKILL.md +318 -0
- package/skills/testing-strategy/SKILL.md +195 -0
- package/skills/tour/SKILL.md +38 -0
- package/skills/upload/SKILL.md +117 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# diff-templates-vs-tracks.sh
|
|
3
|
+
#
|
|
4
|
+
# Surface tracks whose artifact set drifts from the current template schema.
|
|
5
|
+
# Used by WS-0 (Templates are the contract) and the deploy-checklist gate.
|
|
6
|
+
#
|
|
7
|
+
# Compares the file set + section headers + required-field count between
|
|
8
|
+
# core/templates/ and each tracks/*/ directory passed on the command line
|
|
9
|
+
# (or every tracks/* found under the current repo if no path is given).
|
|
10
|
+
#
|
|
11
|
+
# Exit codes:
|
|
12
|
+
# 0 no drift
|
|
13
|
+
# 1 drift detected (details on stderr)
|
|
14
|
+
# 2 usage / runtime error
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# scripts/tools/diff-templates-vs-tracks.sh # scan ./tracks/*
|
|
18
|
+
# scripts/tools/diff-templates-vs-tracks.sh tracks/foo bar/ # scan listed dirs
|
|
19
|
+
# scripts/tools/diff-templates-vs-tracks.sh --json ... # JSON output
|
|
20
|
+
#
|
|
21
|
+
# Notes:
|
|
22
|
+
# - "Drift" is a heuristic: missing required section headers, missing files,
|
|
23
|
+
# or known-removed fields still present.
|
|
24
|
+
# - Templates themselves are linted: a template that omits a header expected
|
|
25
|
+
# by the contract is also flagged.
|
|
26
|
+
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
|
|
29
|
+
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
|
|
30
|
+
echo "${0##*/} — Foundations quality tool (see core/ docs for full behavior)"
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
35
|
+
# shellcheck source=/dev/null
|
|
36
|
+
source "$SCRIPT_DIR/_lib.sh"
|
|
37
|
+
|
|
38
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
39
|
+
TEMPLATES_DIR="$REPO_ROOT/core/templates"
|
|
40
|
+
|
|
41
|
+
EMIT_JSON=0
|
|
42
|
+
TRACK_PATHS=()
|
|
43
|
+
|
|
44
|
+
usage() {
|
|
45
|
+
local stream=2 code=2
|
|
46
|
+
if [[ "${USAGE_HELP_MODE:-0}" == 1 ]]; then stream=1; code=0; fi
|
|
47
|
+
sed -n '2,22p' "$0" >&$stream
|
|
48
|
+
exit "$code"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Argument parsing
|
|
52
|
+
while (($#)); do
|
|
53
|
+
case "$1" in
|
|
54
|
+
-h|--help) USAGE_HELP_MODE=1 usage ;;
|
|
55
|
+
--json) EMIT_JSON=1; shift ;;
|
|
56
|
+
-*) printf 'Unknown flag: %s\n' "$1" >&2; usage ;;
|
|
57
|
+
*) TRACK_PATHS+=("$1"); shift ;;
|
|
58
|
+
esac
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
if ((${#TRACK_PATHS[@]} == 0)); then
|
|
62
|
+
while IFS= read -r p; do TRACK_PATHS+=("$p"); done < <(discover_track_dirs "$REPO_ROOT")
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Expected artifact files per track (all required at 2.0)
|
|
66
|
+
REQUIRED_FILES=(spec.md plan.md hld.md lld.md metadata.json discovery.md)
|
|
67
|
+
|
|
68
|
+
# Expected required section headers per markdown file. These are heuristics —
|
|
69
|
+
# present in templates at 2.0, must be present in any track that claims to
|
|
70
|
+
# conform. Match is case-insensitive and treats hyphen-or-space as equivalent
|
|
71
|
+
# so author-style variation (Mode Selection vs Mode-selection) doesn't trip
|
|
72
|
+
# the validator. The canonical form is the form in core/templates/.
|
|
73
|
+
required_headers_for() {
|
|
74
|
+
case "$1" in
|
|
75
|
+
spec.md) printf '%s' 'Problem Statement|Requirements|Acceptance Criteria|Risk Assessment|Open Questions' ;;
|
|
76
|
+
plan.md) printf '%s' 'Phase 0|Phase 1|Status Markers' ;;
|
|
77
|
+
hld.md) printf '%s' 'Background|Requirements|High Level Design|Detailed Design|Dependencies|Checklist|Deployment|Observability' ;;
|
|
78
|
+
lld.md) printf '%s' 'Background|Requirements|Low Level Design|Observability' ;;
|
|
79
|
+
discovery.md) printf '%s' 'Hotspots|Mode Selection|Open Questions|References' ;;
|
|
80
|
+
*) printf '%s' '' ;;
|
|
81
|
+
esac
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Strings that were removed at 2.0 — should not appear in conforming tracks.
|
|
85
|
+
REMOVED_FIELDS_RE='Author1|xxx@\.com|xxx@example\.com|^Status: \[x\] Complete$'
|
|
86
|
+
|
|
87
|
+
drift_count=0
|
|
88
|
+
declare -a drift_records=()
|
|
89
|
+
|
|
90
|
+
record_drift() {
|
|
91
|
+
local track="$1" kind="$2" detail="$3"
|
|
92
|
+
drift_records+=("$track|$kind|$detail")
|
|
93
|
+
drift_count=$((drift_count + 1))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
scan_track() {
|
|
97
|
+
local track_dir="$1"
|
|
98
|
+
local rel_track
|
|
99
|
+
rel_track="${track_dir#"$REPO_ROOT/"}"
|
|
100
|
+
|
|
101
|
+
for fname in "${REQUIRED_FILES[@]}"; do
|
|
102
|
+
if [[ ! -f "$track_dir/$fname" ]]; then
|
|
103
|
+
record_drift "$rel_track" "missing-file" "$fname"
|
|
104
|
+
fi
|
|
105
|
+
done
|
|
106
|
+
|
|
107
|
+
for fname in spec.md plan.md hld.md lld.md discovery.md; do
|
|
108
|
+
local fpath="$track_dir/$fname"
|
|
109
|
+
[[ -f "$fpath" ]] || continue
|
|
110
|
+
local pattern
|
|
111
|
+
pattern="$(required_headers_for "$fname")"
|
|
112
|
+
[[ -n "$pattern" ]] || continue
|
|
113
|
+
IFS='|' read -r -a heads <<< "$pattern"
|
|
114
|
+
for h in "${heads[@]}"; do
|
|
115
|
+
# Build a hyphen-or-space-tolerant regex: every literal space in
|
|
116
|
+
# the expected header may match `[ -]` in the actual header.
|
|
117
|
+
local h_flex="${h// /[ -]}"
|
|
118
|
+
# Case-insensitive (-i) substring match against an ATX header line.
|
|
119
|
+
if ! grep -Eiq "^#{1,6} +.*${h_flex}" "$fpath"; then
|
|
120
|
+
record_drift "$rel_track" "missing-header" "$fname:$h"
|
|
121
|
+
fi
|
|
122
|
+
done
|
|
123
|
+
done
|
|
124
|
+
|
|
125
|
+
while IFS= read -r f; do
|
|
126
|
+
local rel_file="${f#"$track_dir/"}"
|
|
127
|
+
if grep -nE "$REMOVED_FIELDS_RE" "$f" >/dev/null 2>&1; then
|
|
128
|
+
while IFS= read -r line; do
|
|
129
|
+
record_drift "$rel_track" "removed-field" "$rel_file:$line"
|
|
130
|
+
done < <(grep -nE "$REMOVED_FIELDS_RE" "$f" | head -5)
|
|
131
|
+
fi
|
|
132
|
+
done < <(find "$track_dir" -maxdepth 1 -type f -name '*.md')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Sanity-check templates themselves first.
|
|
136
|
+
for fname in "${REQUIRED_FILES[@]}"; do
|
|
137
|
+
[[ -f "$TEMPLATES_DIR/$fname" ]] || record_drift "core/templates" "missing-template" "$fname"
|
|
138
|
+
done
|
|
139
|
+
|
|
140
|
+
for t in "${TRACK_PATHS[@]}"; do
|
|
141
|
+
[[ -d "$t" ]] || { record_drift "$t" "not-a-directory" ""; continue; }
|
|
142
|
+
scan_track "$(cd "$t" && pwd)"
|
|
143
|
+
done
|
|
144
|
+
|
|
145
|
+
emit_records() {
|
|
146
|
+
if ((EMIT_JSON)); then
|
|
147
|
+
printf '{"drift_count": %d, "records": [\n' "$drift_count"
|
|
148
|
+
local first=1
|
|
149
|
+
for r in "${drift_records[@]}"; do
|
|
150
|
+
local track kind detail
|
|
151
|
+
IFS='|' read -r track kind detail <<< "$r"
|
|
152
|
+
if ((first)); then first=0; else printf ',\n'; fi
|
|
153
|
+
printf ' {"track": "%s", "kind": "%s", "detail": "%s"}' \
|
|
154
|
+
"$(json_escape "$track")" \
|
|
155
|
+
"$(json_escape "$kind")" \
|
|
156
|
+
"$(json_escape "$detail")"
|
|
157
|
+
done
|
|
158
|
+
printf '\n]}\n'
|
|
159
|
+
else
|
|
160
|
+
if ((drift_count == 0)); then
|
|
161
|
+
printf 'OK: no drift across %d track(s).\n' "${#TRACK_PATHS[@]}"
|
|
162
|
+
else
|
|
163
|
+
printf 'DRIFT: %d defect(s) across %d track(s).\n' \
|
|
164
|
+
"$drift_count" "${#TRACK_PATHS[@]}" >&2
|
|
165
|
+
local r track kind detail
|
|
166
|
+
for r in "${drift_records[@]}"; do
|
|
167
|
+
IFS='|' read -r track kind detail <<< "$r"
|
|
168
|
+
printf ' [%s] %s — %s\n' "$kind" "$track" "$detail" >&2
|
|
169
|
+
done
|
|
170
|
+
fi
|
|
171
|
+
fi
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
emit_records
|
|
175
|
+
|
|
176
|
+
((drift_count == 0))
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# emit-skill-metrics.sh — Append a NDJSON metrics record to ~/.draft/metrics.jsonl
|
|
3
|
+
#
|
|
4
|
+
# Usage: emit-skill-metrics.sh <json-payload>
|
|
5
|
+
# json-payload: a JSON object string (must be valid JSON, single line)
|
|
6
|
+
#
|
|
7
|
+
# Exit codes: always 0 (silent on all errors — never break the calling skill)
|
|
8
|
+
# Concurrency: uses flock on the metrics file to prevent interleaved writes
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
|
13
|
+
cat <<'EOF'
|
|
14
|
+
Usage: emit-skill-metrics.sh '<json-payload>'
|
|
15
|
+
|
|
16
|
+
Appends a single NDJSON record to ~/.draft/metrics.jsonl with an injected
|
|
17
|
+
"ts" field (ISO-8601 UTC). Concurrency-safe via flock. Silent on all errors —
|
|
18
|
+
never fails the calling skill. Rotates the metrics file to the last 1000 lines
|
|
19
|
+
when it exceeds 10MB.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
emit-skill-metrics.sh '{"skill":"review","verdict":"approve"}'
|
|
23
|
+
|
|
24
|
+
Resolution (when invoked by a skill):
|
|
25
|
+
1. $DRAFT_PLUGIN_ROOT/scripts/tools/emit-skill-metrics.sh
|
|
26
|
+
2. $HOME/.claude/plugins/draft/scripts/tools/emit-skill-metrics.sh
|
|
27
|
+
3. $PWD/scripts/tools/emit-skill-metrics.sh
|
|
28
|
+
|
|
29
|
+
Self-test: /draft:draft metrics-check
|
|
30
|
+
EOF
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
METRICS_DIR="${HOME}/.draft"
|
|
35
|
+
METRICS_FILE="${METRICS_DIR}/metrics.jsonl"
|
|
36
|
+
LOCK_FILE="${METRICS_DIR}/metrics.lock"
|
|
37
|
+
|
|
38
|
+
payload="${1:-}"
|
|
39
|
+
|
|
40
|
+
# Validate that a payload was provided
|
|
41
|
+
if [[ -z "${payload}" ]]; then
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Ensure the metrics directory exists (silent — never fail the caller)
|
|
46
|
+
mkdir -p "${METRICS_DIR}" 2>/dev/null || exit 0
|
|
47
|
+
|
|
48
|
+
# Append ISO timestamp to the payload and write under an exclusive lock
|
|
49
|
+
# flock -x -w 2: acquire exclusive lock, wait up to 2 seconds, then give up
|
|
50
|
+
(
|
|
51
|
+
flock -x -w 2 200 2>/dev/null || exit 0
|
|
52
|
+
|
|
53
|
+
# Inject timestamp into the payload using sed (avoids requiring jq)
|
|
54
|
+
# Assumes payload ends with '}' — insert timestamp field before closing brace
|
|
55
|
+
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "unknown")"
|
|
56
|
+
record="${payload%\}},\"ts\":\"${ts}\"}"
|
|
57
|
+
echo "${record}" >> "${METRICS_FILE}" 2>/dev/null || true
|
|
58
|
+
|
|
59
|
+
# Rotate when file exceeds 10MB: keep last 1000 lines.
|
|
60
|
+
# Cheap size check via `wc -c`; only invoke rotation when triggered.
|
|
61
|
+
if [[ -f "${METRICS_FILE}" ]]; then
|
|
62
|
+
size_bytes=$(wc -c < "${METRICS_FILE}" 2>/dev/null || echo 0)
|
|
63
|
+
if [[ "${size_bytes}" -gt 10485760 ]]; then
|
|
64
|
+
tail -n 1000 "${METRICS_FILE}" > "${METRICS_FILE}.tmp" 2>/dev/null \
|
|
65
|
+
&& mv -f "${METRICS_FILE}.tmp" "${METRICS_FILE}" 2>/dev/null \
|
|
66
|
+
|| rm -f "${METRICS_FILE}.tmp" 2>/dev/null
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
) 200>"${LOCK_FILE}" 2>/dev/null || true
|
|
71
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# fix-whitespace.sh — strip trailing whitespace and blank lines at EOF from
|
|
3
|
+
# AI-generated markdown files.
|
|
4
|
+
#
|
|
5
|
+
# GitHub (and git --check) rejects commits with trailing whitespace or a blank
|
|
6
|
+
# final line. This script normalises draft-generated markdown in-place before
|
|
7
|
+
# the files are committed, preventing upload failures.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# # Fix specific files:
|
|
11
|
+
# scripts/tools/fix-whitespace.sh <file> [<file> ...]
|
|
12
|
+
#
|
|
13
|
+
# # Fix all markdown files in a track:
|
|
14
|
+
# scripts/tools/fix-whitespace.sh --track <track_id>
|
|
15
|
+
#
|
|
16
|
+
# # Fix all draft-generated markdown in the repo (safe subset):
|
|
17
|
+
# scripts/tools/fix-whitespace.sh --draft [<repo_root>]
|
|
18
|
+
#
|
|
19
|
+
# Exit codes:
|
|
20
|
+
# 0 — success (all files normalised; prints list of changed files)
|
|
21
|
+
# 1 — invocation error
|
|
22
|
+
# 2 — one or more files could not be processed
|
|
23
|
+
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
|
|
27
|
+
echo "${0##*/} — Foundations quality tool (see core/ docs for full behavior)"
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
32
|
+
# shellcheck source=_lib.sh
|
|
33
|
+
source "$SCRIPT_DIR/_lib.sh"
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Internal helpers
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
usage() {
|
|
40
|
+
sed -n '2,/^[^#]/p' "$0" | grep '^#' | sed 's/^# \?//'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Fix a single file in-place.
|
|
44
|
+
# Returns: 0 if modified, 1 if already clean (exit code from the function,
|
|
45
|
+
# not the script — caller decides whether to count it).
|
|
46
|
+
fix_file() {
|
|
47
|
+
local file="$1"
|
|
48
|
+
if [[ ! -f "$file" ]]; then
|
|
49
|
+
echo " SKIP (not a file): $file" >&2
|
|
50
|
+
return 2
|
|
51
|
+
fi
|
|
52
|
+
if [[ ! -w "$file" ]]; then
|
|
53
|
+
echo " ERROR (not writable): $file" >&2
|
|
54
|
+
return 2
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
local original
|
|
58
|
+
original="$(cat "$file")"
|
|
59
|
+
|
|
60
|
+
# 1. Strip trailing whitespace on every line (spaces and tabs).
|
|
61
|
+
# 2. Strip trailing blank lines at EOF, then add exactly one final newline.
|
|
62
|
+
local fixed
|
|
63
|
+
fixed="$(
|
|
64
|
+
printf '%s' "$original" \
|
|
65
|
+
| sed 's/[[:space:]]*$//' \
|
|
66
|
+
| sed -e :a -e '/^\n*$/{$d;N;ba}'
|
|
67
|
+
)"$'\n'
|
|
68
|
+
|
|
69
|
+
if [[ "$fixed" == "$original" ]]; then
|
|
70
|
+
return 1 # already clean
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
local _tmp
|
|
74
|
+
_tmp="$(mktemp "${file}.XXXXXX")"
|
|
75
|
+
if printf '%s' "$fixed" > "$_tmp"; then
|
|
76
|
+
mv -f "$_tmp" "$file"
|
|
77
|
+
else
|
|
78
|
+
rm -f "$_tmp"
|
|
79
|
+
return 2
|
|
80
|
+
fi
|
|
81
|
+
return 0
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Collect target files from arguments
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
TARGETS=()
|
|
89
|
+
REPO_ROOT=""
|
|
90
|
+
|
|
91
|
+
if [[ $# -eq 0 ]]; then
|
|
92
|
+
usage
|
|
93
|
+
exit 0
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
case "$1" in
|
|
97
|
+
--track)
|
|
98
|
+
[[ $# -ge 2 ]] || { echo "ERROR: --track requires a track_id argument." >&2; exit 1; }
|
|
99
|
+
TRACK_ID="$2"
|
|
100
|
+
# Determine repo root: walk up from cwd until draft/ is found.
|
|
101
|
+
REPO_ROOT="$(pwd)"
|
|
102
|
+
while [[ ! -d "$REPO_ROOT/draft" && "$REPO_ROOT" != "/" ]]; do
|
|
103
|
+
REPO_ROOT="$(dirname "$REPO_ROOT")"
|
|
104
|
+
done
|
|
105
|
+
TRACK_DIR="$REPO_ROOT/draft/tracks/$TRACK_ID"
|
|
106
|
+
if [[ ! -d "$TRACK_DIR" ]]; then
|
|
107
|
+
echo "ERROR: track directory not found: $TRACK_DIR" >&2
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
while IFS= read -r -d '' f; do
|
|
111
|
+
TARGETS+=("$f")
|
|
112
|
+
done < <(find "$TRACK_DIR" -maxdepth 1 -name "*.md" -print0 | sort -z)
|
|
113
|
+
;;
|
|
114
|
+
--draft)
|
|
115
|
+
REPO_ROOT="${2:-$(pwd)}"
|
|
116
|
+
while [[ ! -d "$REPO_ROOT/draft" && "$REPO_ROOT" != "/" ]]; do
|
|
117
|
+
REPO_ROOT="$(dirname "$REPO_ROOT")"
|
|
118
|
+
done
|
|
119
|
+
if [[ ! -d "$REPO_ROOT/draft" ]]; then
|
|
120
|
+
echo "ERROR: could not locate draft/ directory from: ${2:-$(pwd)}" >&2
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
# Safe subset: only files produced by draft skills.
|
|
124
|
+
while IFS= read -r -d '' f; do
|
|
125
|
+
TARGETS+=("$f")
|
|
126
|
+
done < <(
|
|
127
|
+
find "$REPO_ROOT/draft" \
|
|
128
|
+
-name "architecture.md" \
|
|
129
|
+
-o -name ".ai-context.md" \
|
|
130
|
+
-o -name ".ai-profile.md" \
|
|
131
|
+
-o -name "hld.md" \
|
|
132
|
+
-o -name "lld.md" \
|
|
133
|
+
-o -name "spec.md" \
|
|
134
|
+
-o -name "plan.md" \
|
|
135
|
+
-o -name "rca.md" \
|
|
136
|
+
-o -name "guardrails.md" \
|
|
137
|
+
-o -name "product.md" \
|
|
138
|
+
-o -name "tech-stack.md" \
|
|
139
|
+
| sort \
|
|
140
|
+
| tr '\n' '\0'
|
|
141
|
+
)
|
|
142
|
+
;;
|
|
143
|
+
-h|--help)
|
|
144
|
+
usage
|
|
145
|
+
exit 0
|
|
146
|
+
;;
|
|
147
|
+
-*)
|
|
148
|
+
echo "ERROR: unknown option: $1" >&2
|
|
149
|
+
exit 1
|
|
150
|
+
;;
|
|
151
|
+
*)
|
|
152
|
+
TARGETS=("$@")
|
|
153
|
+
;;
|
|
154
|
+
esac
|
|
155
|
+
|
|
156
|
+
if [[ ${#TARGETS[@]} -eq 0 ]]; then
|
|
157
|
+
echo "fix-whitespace: no files to process."
|
|
158
|
+
exit 0
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Process files
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
CHANGED=()
|
|
166
|
+
ERRORS=()
|
|
167
|
+
|
|
168
|
+
for f in "${TARGETS[@]}"; do
|
|
169
|
+
rc=0
|
|
170
|
+
fix_file "$f" || rc=$?
|
|
171
|
+
case $rc in
|
|
172
|
+
0) CHANGED+=("$f") ;;
|
|
173
|
+
1) ;; # already clean — silent
|
|
174
|
+
*) ERRORS+=("$f") ;;
|
|
175
|
+
esac
|
|
176
|
+
done
|
|
177
|
+
|
|
178
|
+
if [[ ${#CHANGED[@]} -gt 0 ]]; then
|
|
179
|
+
echo "fix-whitespace: normalised ${#CHANGED[@]} file(s):"
|
|
180
|
+
for f in "${CHANGED[@]}"; do
|
|
181
|
+
echo " $f"
|
|
182
|
+
done
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
if [[ ${#ERRORS[@]} -gt 0 ]]; then
|
|
186
|
+
echo "fix-whitespace: ERROR — could not process ${#ERRORS[@]} file(s):" >&2
|
|
187
|
+
for f in "${ERRORS[@]}"; do
|
|
188
|
+
echo " $f" >&2
|
|
189
|
+
done
|
|
190
|
+
exit 2
|
|
191
|
+
fi
|
|
192
|
+
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# freshness-check.sh — verify recorded file hashes against current contents.
|
|
3
|
+
#
|
|
4
|
+
# Input state file format (draft/.state/freshness.json):
|
|
5
|
+
# {
|
|
6
|
+
# "generated_at": "2026-04-22T10:00:00Z",
|
|
7
|
+
# "files": [
|
|
8
|
+
# {"path": "draft/architecture.md", "sha256": "abc..."},
|
|
9
|
+
# ...
|
|
10
|
+
# ]
|
|
11
|
+
# }
|
|
12
|
+
#
|
|
13
|
+
# Emits:
|
|
14
|
+
# {
|
|
15
|
+
# "fresh": true|false,
|
|
16
|
+
# "stale_files": ["..."],
|
|
17
|
+
# "missing_files": ["..."],
|
|
18
|
+
# "reason": "..."
|
|
19
|
+
# }
|
|
20
|
+
#
|
|
21
|
+
# Usage:
|
|
22
|
+
# scripts/tools/freshness-check.sh [--state PATH] [--root DIR]
|
|
23
|
+
#
|
|
24
|
+
# Exit codes: 0 fresh, 1 invocation error, 2 stale (still emits JSON).
|
|
25
|
+
set -euo pipefail
|
|
26
|
+
|
|
27
|
+
STATE_FILE=""
|
|
28
|
+
ROOT="."
|
|
29
|
+
|
|
30
|
+
usage() {
|
|
31
|
+
cat <<'EOF'
|
|
32
|
+
freshness-check.sh — verify file hashes against a recorded state snapshot.
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
scripts/tools/freshness-check.sh [--state PATH] [--root DIR]
|
|
36
|
+
|
|
37
|
+
Flags:
|
|
38
|
+
--state PATH Path to freshness JSON (default: <root>/draft/.state/freshness.json).
|
|
39
|
+
--root DIR Repository root to resolve file paths against (default: cwd).
|
|
40
|
+
--help Show this help.
|
|
41
|
+
|
|
42
|
+
Output: JSON {fresh, stale_files, missing_files, reason}.
|
|
43
|
+
Exit 0 fresh, 2 stale (still emits JSON), 1 invocation error.
|
|
44
|
+
EOF
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
while [[ $# -gt 0 ]]; do
|
|
48
|
+
case "$1" in
|
|
49
|
+
--state) STATE_FILE="$2"; shift 2;;
|
|
50
|
+
--root) ROOT="$2"; shift 2;;
|
|
51
|
+
--help|-h) usage; exit 0;;
|
|
52
|
+
*) echo "Unknown flag: $1" >&2; usage >&2; exit 1;;
|
|
53
|
+
esac
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
if [[ ! -d "$ROOT" ]]; then
|
|
57
|
+
echo "ERROR: --root '$ROOT' is not a directory" >&2
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
ROOT_ABS="$(cd "$ROOT" && pwd)"
|
|
61
|
+
|
|
62
|
+
if [[ -z "$STATE_FILE" ]]; then
|
|
63
|
+
STATE_FILE="$ROOT_ABS/draft/.state/freshness.json"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
if [[ ! -f "$STATE_FILE" ]]; then
|
|
67
|
+
esc="${STATE_FILE//\\/\\\\}"; esc="${esc//\"/\\\"}"
|
|
68
|
+
printf '{"fresh": false, "stale_files": [], "missing_files": [], "reason": "no state file at %s"}\n' "$esc"
|
|
69
|
+
exit 2
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
73
|
+
echo "ERROR: jq is required" >&2
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# hash calc: prefer sha256sum; fallback shasum -a 256
|
|
78
|
+
sha256() {
|
|
79
|
+
local f="$1"
|
|
80
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
81
|
+
sha256sum "$f" | awk '{print $1}'
|
|
82
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
83
|
+
shasum -a 256 "$f" | awk '{print $1}'
|
|
84
|
+
else
|
|
85
|
+
return 1
|
|
86
|
+
fi
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
stale=()
|
|
90
|
+
missing=()
|
|
91
|
+
|
|
92
|
+
while IFS=$'\t' read -r path expected; do
|
|
93
|
+
[[ -z "$path" ]] && continue
|
|
94
|
+
full="$ROOT_ABS/$path"
|
|
95
|
+
if [[ ! -f "$full" ]]; then
|
|
96
|
+
missing+=("$path")
|
|
97
|
+
continue
|
|
98
|
+
fi
|
|
99
|
+
actual="$(sha256 "$full")"
|
|
100
|
+
if [[ "$actual" != "$expected" ]]; then
|
|
101
|
+
stale+=("$path")
|
|
102
|
+
fi
|
|
103
|
+
done < <(jq -r '.files[]? | [.path, .sha256] | @tsv' "$STATE_FILE")
|
|
104
|
+
|
|
105
|
+
fresh="true"
|
|
106
|
+
reason=""
|
|
107
|
+
if [[ ${#stale[@]} -gt 0 || ${#missing[@]} -gt 0 ]]; then
|
|
108
|
+
fresh="false"
|
|
109
|
+
reason="$([[ ${#stale[@]} -gt 0 ]] && echo "${#stale[@]} stale" || echo "")"
|
|
110
|
+
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
111
|
+
if [[ -n "$reason" ]]; then reason="$reason, "; fi
|
|
112
|
+
reason="${reason}${#missing[@]} missing"
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
json_array() {
|
|
117
|
+
local arr=("$@")
|
|
118
|
+
if [[ ${#arr[@]} -eq 0 ]]; then
|
|
119
|
+
printf '[]'
|
|
120
|
+
return
|
|
121
|
+
fi
|
|
122
|
+
printf '['
|
|
123
|
+
local first=true
|
|
124
|
+
for x in "${arr[@]}"; do
|
|
125
|
+
if $first; then first=false; else printf ','; fi
|
|
126
|
+
# Escape quotes.
|
|
127
|
+
escaped="${x//\\/\\\\}"
|
|
128
|
+
escaped="${escaped//\"/\\\"}"
|
|
129
|
+
printf '"%s"' "$escaped"
|
|
130
|
+
done
|
|
131
|
+
printf ']'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
printf '{"fresh":%s,"stale_files":%s,"missing_files":%s,"reason":"%s"}\n' \
|
|
135
|
+
"$fresh" \
|
|
136
|
+
"$(json_array "${stale[@]+"${stale[@]}"}")" \
|
|
137
|
+
"$(json_array "${missing[@]+"${missing[@]}"}")" \
|
|
138
|
+
"$reason"
|
|
139
|
+
|
|
140
|
+
if [[ "$fresh" == "false" ]]; then
|
|
141
|
+
exit 2
|
|
142
|
+
fi
|
|
143
|
+
exit 0
|