start-vibing 4.2.0 → 4.3.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.
@@ -1,30 +1,122 @@
1
1
  #!/usr/bin/env bash
2
2
  # Usage: detect-changes.sh <last_sha> [<last_iso>]
3
3
  # Emits JSON: { mode, range_start, commits, files, classified }
4
+ #
5
+ # Implements the fallback ladder from artifact §6 + §14:
6
+ # 1. Anchor exists → diff with rename detection (-M90%).
7
+ # 2. Anchor missing + shallow repo → `git fetch --unshallow --no-tags`.
8
+ # 3. Still missing → fetch super-design git notes, retry.
9
+ # 4. Still missing → `--since=<iso>` time-based fallback.
10
+ # 5. Empty repo / no last_sha at all → diff against empty-tree SHA
11
+ # 4b825dc642cb6eb9a060e54bf8d69288fbee4904 (artifact line 900).
12
+ # Also uses --first-parent + --cherry-pick --right-only when walking
13
+ # history (artifact §2.5 line 167-172).
4
14
  set -euo pipefail
5
15
 
6
16
  LAST_SHA="${1:-}"
7
17
  LAST_ISO="${2:-}"
18
+ EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
19
+
20
+ log() { printf '[detect-changes] %s\n' "$*" >&2; }
8
21
 
9
- if [[ -z "$LAST_SHA" ]]; then echo '{"error":"missing last_sha"}'; exit 2; fi
10
22
  if ! git rev-parse --git-dir >/dev/null 2>&1; then echo '{"error":"not-a-git-repo"}'; exit 3; fi
11
23
 
12
- if git rev-parse --verify --quiet "${LAST_SHA}^{commit}" >/dev/null; then
13
- if git merge-base --is-ancestor "$LAST_SHA" HEAD; then RANGE_START="$LAST_SHA"
14
- else RANGE_START="$(git merge-base HEAD "$LAST_SHA" 2>/dev/null || echo "")"; fi
15
- else RANGE_START=""; fi
16
-
17
- if [[ -z "$RANGE_START" ]]; then
18
- if [[ -n "$LAST_ISO" ]]; then
19
- FILES="$(git log --since="$LAST_ISO" --name-only --pretty=format: 2>/dev/null | sort -u | sed '/^$/d' || true)"
20
- COMMITS="$(git log --since="$LAST_ISO" --pretty=format:'%H|%s|%an|%aI' 2>/dev/null || true)"
21
- MODE="since-time"
22
- else echo '{"error":"lost-anchor-no-fallback-time"}'; exit 4; fi
24
+ # Determine HEAD availability empty repos have no commits at all.
25
+ if ! git rev-parse --verify --quiet HEAD >/dev/null; then
26
+ log "no HEAD commit; using empty-tree SHA fallback"
27
+ LAST_SHA="$EMPTY_TREE"
28
+ fi
29
+
30
+ # If caller didn't pass a last_sha, treat it as empty-tree (first audit).
31
+ if [[ -z "$LAST_SHA" ]]; then
32
+ log "missing last_sha; defaulting to empty-tree SHA"
33
+ LAST_SHA="$EMPTY_TREE"
34
+ fi
35
+
36
+ resolve_anchor() {
37
+ # Sets RANGE_START (may be empty if anchor unrecoverable).
38
+ if [[ "$LAST_SHA" == "$EMPTY_TREE" ]]; then
39
+ RANGE_START="$EMPTY_TREE"; return 0
40
+ fi
41
+ if git rev-parse --verify --quiet "${LAST_SHA}^{commit}" >/dev/null; then
42
+ if git merge-base --is-ancestor "$LAST_SHA" HEAD 2>/dev/null; then
43
+ RANGE_START="$LAST_SHA"
44
+ else
45
+ RANGE_START="$(git merge-base HEAD "$LAST_SHA" 2>/dev/null || echo "")"
46
+ fi
47
+ return 0
48
+ fi
49
+ RANGE_START=""
50
+ return 1
51
+ }
52
+
53
+ if ! resolve_anchor; then
54
+ # Ladder step (a): unshallow if possible.
55
+ if [[ "$(git rev-parse --is-shallow-repository 2>/dev/null || echo false)" == "true" ]]; then
56
+ log "anchor missing and repo is shallow; attempting git fetch --unshallow --no-tags"
57
+ git fetch --unshallow --no-tags 2>/dev/null || log "unshallow fetch failed (continuing)"
58
+ resolve_anchor || true
59
+ fi
60
+ fi
61
+ if [[ -z "${RANGE_START:-}" ]]; then
62
+ # Ladder step (b): try to fetch super-design notes; they may pin a commit
63
+ # we don't have locally.
64
+ log "anchor still missing; fetching refs/notes/super-design"
65
+ git fetch origin '+refs/notes/super-design:refs/notes/super-design' 2>/dev/null \
66
+ || log "notes fetch failed (continuing)"
67
+ resolve_anchor || true
68
+ fi
69
+
70
+ # Ladder step (c): time-based fallback.
71
+ if [[ -z "${RANGE_START:-}" && -n "$LAST_ISO" ]]; then
72
+ log "using --since=$LAST_ISO time-based fallback"
73
+ FILES="$(git log --since="$LAST_ISO" --name-only --pretty=format: 2>/dev/null | sort -u | sed '/^$/d' || true)"
74
+ COMMITS="$(git log --first-parent --since="$LAST_ISO" --pretty=format:'%H|%s|%an|%aI' 2>/dev/null || true)"
75
+ MODE="since-time"
76
+ RANGE_START=""
77
+ elif [[ -z "${RANGE_START:-}" ]]; then
78
+ echo '{"error":"lost-anchor-no-fallback-time"}'; exit 4
23
79
  else
24
- FILES="$(git diff --name-only "${RANGE_START}..HEAD" \
25
- -- ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml' ':!yarn.lock' \
26
- ':!.github/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/*.stories.*' | sort -u)"
27
- COMMITS="$(git log --no-merges --pretty=format:'%H|%s|%an|%aI' "${RANGE_START}..HEAD" 2>/dev/null || true)"
80
+ # SHA range available. Use --name-status -M90% -z to catch renames AND
81
+ # filenames with spaces (NUL-terminated output).
82
+ RANGE="${RANGE_START}..HEAD"
83
+ if [[ "$RANGE_START" == "$EMPTY_TREE" ]]; then
84
+ # Empty-tree baseline: everything in HEAD is "new".
85
+ RANGE="${EMPTY_TREE} HEAD"
86
+ fi
87
+
88
+ # Parse NUL-terminated name-status output.
89
+ # Format: <STATUS>\0<path>[\0<new_path>] where STATUS can be A/M/D/Rnn/Cnn.
90
+ # We collapse to the post-rename path so downstream classification matches
91
+ # the current file layout.
92
+ FILES="$(
93
+ git diff --name-status -M90% -z \
94
+ -- . ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml' ':!yarn.lock' \
95
+ ':!.github/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/*.stories.*' \
96
+ $RANGE 2>/dev/null |
97
+ awk -v RS='\0' '
98
+ BEGIN { status = "" }
99
+ {
100
+ if (status == "") { status = $0; next }
101
+ # Rename/copy has two path fields; we want the second (post-rename).
102
+ if (status ~ /^R/ || status ~ /^C/) {
103
+ if (wanted == "") { wanted = 1; next }
104
+ print; status = ""; wanted = ""
105
+ } else {
106
+ print; status = ""
107
+ }
108
+ }
109
+ ' | sort -u
110
+ )"
111
+
112
+ # --first-parent keeps one entry per merged PR (trunk-based). Combine
113
+ # with --cherry-pick --right-only to dedupe back-ports / rebased copies.
114
+ if [[ "$RANGE_START" == "$EMPTY_TREE" ]]; then
115
+ COMMITS="$(git log --first-parent --pretty=format:'%H|%s|%an|%aI' HEAD 2>/dev/null || true)"
116
+ else
117
+ COMMITS="$(git log --first-parent --cherry-pick --right-only --no-merges \
118
+ --pretty=format:'%H|%s|%an|%aI' "${RANGE_START}...HEAD" 2>/dev/null || true)"
119
+ fi
28
120
  MODE="sha-range"
29
121
  fi
30
122
 
@@ -32,7 +124,7 @@ declare -A CLASSIFIED
32
124
  while IFS= read -r p; do
33
125
  [[ -z "$p" ]] && continue
34
126
  case "$p" in
35
- tailwind.config.*|*.tokens.json|styles/tokens.css|styles/theme.css) CLASSIFIED[tokens]+="$p,";;
127
+ tailwind.config.*|*.tokens.json|*.tokens|styles/tokens.css|styles/theme.css) CLASSIFIED[tokens]+="$p,";;
36
128
  components/*|src/components/*|app/_components/*) CLASSIFIED[components]+="$p,";;
37
129
  app/*/page.*|app/page.*|app/*/route.*|app/route.*|pages/*|src/pages/*|app/routes/*|src/routes/*) CLASSIFIED[routes]+="$p,";;
38
130
  public/*|src/assets/*|assets/*) CLASSIFIED[imagery]+="$p,";;
@@ -43,7 +135,7 @@ while IFS= read -r p; do
43
135
  esac
44
136
  done <<< "$FILES"
45
137
 
46
- jq -Rn --arg mode "$MODE" --arg range_start "$RANGE_START" --arg last_iso "$LAST_ISO" \
138
+ jq -Rn --arg mode "$MODE" --arg range_start "${RANGE_START:-}" --arg last_iso "$LAST_ISO" \
47
139
  --arg tokens "${CLASSIFIED[tokens]:-}" --arg components "${CLASSIFIED[components]:-}" \
48
140
  --arg routes "${CLASSIFIED[routes]:-}" --arg imagery "${CLASSIFIED[imagery]:-}" \
49
141
  --arg deps "${CLASSIFIED[deps]:-}" --arg theory "${CLASSIFIED[theory]:-}" \
@@ -59,3 +151,6 @@ jq -Rn --arg mode "$MODE" --arg range_start "$RANGE_START" --arg last_iso "$LAST
59
151
  deps: tolist($deps), theory: tolist($theory), content: tolist($content)
60
152
  }
61
153
  }'
154
+
155
+ # TODO(sd-audit-state §11/artifact line 902): monorepo per-app state
156
+ # (apps/*/docs/super-design/.audit-state.json) not yet supported.
@@ -1,4 +1,11 @@
1
1
  #!/usr/bin/env bash
2
+ # TODO(sd-audit-state artifact §14): dynamic routes like `/posts/[slug]`
3
+ # should be expanded to `/posts/@fixture-<id>` using a fixtures manifest
4
+ # (discovered from tests or a user-configured JSON) before being passed
5
+ # to hash-pages/sd-audit. Current impl emits the raw pattern.
6
+ # TODO(sd-audit-state artifact §8): madge-based import-graph builder
7
+ # (`madge --json --ts-config tsconfig.json src`) to compute N-hop
8
+ # component → page blast radius. Expected alongside this script.
2
9
  set -euo pipefail
3
10
 
4
11
  detect_framework() {
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ // Extract + canonicalize + hash design tokens from Tailwind configs and
3
+ // CSS custom properties. Output is deterministic regardless of insertion
4
+ // order (artifact §9.2 line 722: "canonical = JSON.stringify(theme,
5
+ // Object.keys(theme).sort())").
2
6
  import fs from "node:fs";
3
7
  import path from "node:path";
8
+ import { createHash } from "node:crypto";
4
9
  import { pathToFileURL } from "node:url";
5
10
 
6
11
  const out = {};
@@ -28,12 +33,24 @@ try {
28
33
  }
29
34
  } catch (e) { out._postcss_error = String(e.message || e); }
30
35
 
31
- console.log(JSON.stringify(out, null, 2));
36
+ // TODO(sd-audit-state §9.1 artifact line 707-709): DTCG *.tokens.json and
37
+ // Tokens Studio support — parse JSON, resolve `{alias}` refs per §9.2 line
38
+ // 747 before hashing, then merge into `out` with prefix `dtcg:`.
39
+
40
+ // Deterministic canonical form: keys sorted top-to-bottom.
41
+ const sorted = Object.keys(out).sort().reduce((acc, k) => { acc[k] = out[k]; return acc; }, {});
42
+ const canonical = JSON.stringify(sorted);
43
+ const tokens_hash = "sha256:" + createHash("sha256").update(canonical).digest("hex");
44
+
45
+ console.log(JSON.stringify({ tokens: sorted, tokens_hash }, null, 2));
32
46
 
33
47
  function flatten(obj, prefix, acc) {
34
48
  if (obj == null) return;
35
49
  if (typeof obj !== "object") { acc[prefix] = String(obj); return; }
36
- for (const [k, v] of Object.entries(obj)) {
50
+ // Sort keys so hash is stable across JS engines / config formatters.
51
+ // Artifact §9.2 line 722 calls this out explicitly.
52
+ for (const k of Object.keys(obj).sort()) {
53
+ const v = obj[k];
37
54
  const key = `${prefix}.${k}`;
38
55
  if (v && typeof v === "object" && !Array.isArray(v)) flatten(v, key, acc);
39
56
  else acc[key] = Array.isArray(v) ? v.join(",") : String(v);
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env bash
2
2
  # Usage: hash-pages.sh <urls_file>
3
+ #
4
+ # TODO(sd-audit-state artifact §10 line 492, §16 line 1367-1384):
5
+ # Per-viewport hashes (mobile_375 / tablet_768 / desktop_1280) + pHash
6
+ # for perceptual similarity, plus mask_selectors passed to
7
+ # page.screenshot({ mask: [...] }) for deterministic diffs. Current impl
8
+ # hashes a single desktop viewport only.
3
9
  set -euo pipefail
4
10
  URLS="${1:?usage: hash-pages.sh <urls_file>}"
5
11
  OUT_DIR="${OUT_DIR:-docs/super-design/.cache/hashes}"
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: setup-git-notes.sh
3
+ #
4
+ # One-shot setup so `git notes --ref=super-design` round-trips across
5
+ # clones. Without the remote refspec, git fetch ignores notes by default
6
+ # (artifact §7 line 570-573).
7
+ set -euo pipefail
8
+
9
+ if ! git rev-parse --git-dir >/dev/null 2>&1; then
10
+ echo '{"error":"not-a-git-repo"}' >&2; exit 3
11
+ fi
12
+
13
+ # Idempotent: only add if absent.
14
+ if git config --get-all remote.origin.fetch 2>/dev/null |
15
+ grep -q 'refs/notes/super-design'; then
16
+ echo '{"status":"already-configured"}'
17
+ else
18
+ git config --add remote.origin.fetch \
19
+ '+refs/notes/super-design:refs/notes/super-design'
20
+ echo '{"status":"added","ref":"refs/notes/super-design"}'
21
+ fi
@@ -1,14 +1,46 @@
1
1
  #!/usr/bin/env bash
2
+ # Usage: validate-state.sh [<state_path>]
3
+ #
4
+ # Validates the super-design audit state file. On schema/parse errors,
5
+ # moves the broken file aside (artifact §3 "Graceful corruption handling"
6
+ # line 74) and emits a JSON verdict. Also enforces schema_version major
7
+ # compatibility (artifact §12 line 934).
2
8
  set -euo pipefail
3
9
  STATE="${1:-docs/super-design/.audit-state.json}"
10
+
11
+ # Current schema major is either read from a sibling .schema-version file
12
+ # (so the number can be bumped without editing shell) or falls back to 1.
13
+ SCHEMA_VERSION_FILE="$(dirname "$0")/../.schema-version"
14
+ if [[ -f "$SCHEMA_VERSION_FILE" ]]; then
15
+ CURRENT_SCHEMA_MAJOR="$(cut -d. -f1 <"$SCHEMA_VERSION_FILE" | tr -d '[:space:]')"
16
+ else
17
+ CURRENT_SCHEMA_MAJOR=1
18
+ fi
19
+
4
20
  if [[ ! -f "$STATE" ]]; then echo '{"status":"missing"}'; exit 2; fi
5
- jq -e '
21
+
22
+ # Parse + shape check. On failure, rename so the user can inspect and we
23
+ # fall through to first-audit (SKILL.md Step 1 treats "corrupt" that way).
24
+ if ! jq -e '
6
25
  (.schema_version | type == "string") and
7
26
  (.last_audit_at | fromdateiso8601 | . > 0) and
8
27
  (.git_sha_at_audit | test("^[0-9a-f]{7,64}$")) and
9
28
  (.skill_version | type == "string") and
10
29
  (.tools | type == "object")
11
- ' "$STATE" >/dev/null 2>&1 || { echo '{"status":"corrupt"}'; exit 2; }
30
+ ' "$STATE" >/dev/null 2>&1; then
31
+ mv "$STATE" "$STATE.corrupt-$(date +%s)" 2>/dev/null || true
32
+ echo '{"status":"corrupt"}'; exit 2
33
+ fi
34
+
35
+ # schema_version major-bump check — if state was written by a newer OR
36
+ # incompatible-older skill, force a full re-audit rather than silently
37
+ # trusting the shape.
38
+ STATE_MAJOR="$(jq -r '.schema_version' "$STATE" | cut -d. -f1)"
39
+ if [[ -z "$STATE_MAJOR" || "$STATE_MAJOR" != "$CURRENT_SCHEMA_MAJOR" ]]; then
40
+ echo "{\"status\":\"schema-incompatible\",\"action\":\"force-full\",\"state_major\":\"${STATE_MAJOR:-unknown}\",\"current_major\":\"${CURRENT_SCHEMA_MAJOR}\"}"
41
+ exit 1
42
+ fi
43
+
12
44
  AGE_DAYS=$(( ( $(date -u +%s) - $(jq -r '.last_audit_at | fromdateiso8601' "$STATE") ) / 86400 ))
13
45
  if (( AGE_DAYS > 180 )); then echo "{\"status\":\"stale-force-full\",\"age_days\":$AGE_DAYS}"; exit 1
14
46
  elif (( AGE_DAYS > 90 )); then echo "{\"status\":\"stale-refresh-research\",\"age_days\":$AGE_DAYS}"; exit 1
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: write-state.sh [<target>] (reads JSON body from stdin)
3
+ #
4
+ # Atomic state writer: accepts JSON on stdin, writes to <target>.tmp,
5
+ # validates with jq, then renames in place. Referenced from
6
+ # docs/compass_artifact §11 ("Write-then-rename (atomic)") and SKILL.md
7
+ # Step 4 ("Atomic write .audit-state.json").
8
+ set -euo pipefail
9
+
10
+ TARGET="${1:-docs/super-design/.audit-state.json}"
11
+ TMP="${TARGET}.tmp"
12
+
13
+ mkdir -p "$(dirname "$TARGET")"
14
+
15
+ # Drain stdin into the tmp file.
16
+ cat >"$TMP"
17
+
18
+ # Validate it is parseable JSON before swapping.
19
+ if ! jq -e 'type == "object"' "$TMP" >/dev/null 2>&1; then
20
+ rm -f "$TMP"
21
+ echo '{"error":"invalid-json-on-stdin"}' >&2
22
+ exit 2
23
+ fi
24
+
25
+ mv -f "$TMP" "$TARGET"
26
+ echo "{\"status\":\"written\",\"path\":\"$TARGET\"}"