@windyroad/style-guide 0.4.3 → 0.4.4-preview.581

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.
@@ -79,5 +79,5 @@
79
79
  }
80
80
  },
81
81
  "name": "wr-style-guide",
82
- "version": "0.4.3"
82
+ "version": "0.4.4"
83
83
  }
package/README.md CHANGED
@@ -45,22 +45,6 @@ This examines your existing CSS, components, and design patterns, then asks abou
45
45
 
46
46
  The `wr-style-guide:agent` reads your `docs/STYLE-GUIDE.md` and reviews proposed changes against your documented design system.
47
47
 
48
- ## Jobs to be Done
49
-
50
- This plugin serves the [Jobs to be Done](../../docs/jtbd/) below. Per [ADR-051](../../docs/decisions/051-jtbd-anchored-readme-with-drift-advisory.proposed.md), the persona-grouped JTBD anchor is the canonical source of truth for the README's value framing.
51
-
52
- ### Solo developer
53
-
54
- - **[JTBD-001 Enforce Governance Without Slowing Down](../../docs/jtbd/solo-developer/JTBD-001-enforce-governance.proposed.md)** — style-guide review fires automatically on every CSS or component edit; the project's own design system is the policy source.
55
-
56
- ### Tech lead / consultant
57
-
58
- - **[JTBD-202 Run Pre-Flight Governance Checks Before Release or Handover](../../docs/jtbd/tech-lead/JTBD-202-pre-flight-governance-check.proposed.md)** — style-guide alignment is reviewable on demand before a release or client handover.
59
-
60
- ### Plugin user
61
-
62
- - **[JTBD-302 Trust That the README Describes the Plugin I Just Installed](../../docs/jtbd/plugin-user/JTBD-302-trust-readme-describes-installed-behaviour.proposed.md)** — this README is anchored on current JTBD job IDs; drift between prose and shipped behaviour is detectable at retro time per ADR-051.
63
-
64
48
  ## Updating and Uninstalling
65
49
 
66
50
  ```bash
@@ -13,6 +13,111 @@ _mtime() { stat -c%Y "$1" 2>/dev/null || /usr/bin/stat -f%m "$1" 2>/dev/null ||
13
13
  # Portable hash: tries md5sum, falls back to md5 -r, then shasum
14
14
  _hashcmd() { md5sum 2>/dev/null || md5 -r 2>/dev/null || shasum 2>/dev/null; }
15
15
 
16
+ # ---------------------------------------------------------------------------
17
+ # Substance-aware drift hash + atomic verdict-write (ADR-009 amendment
18
+ # 2026-06-06, P353 + P303 close).
19
+ #
20
+ # `_substance_hash_path` normalises trivial/no-op edits BEFORE hashing so a
21
+ # PASS marker survives whitespace / CRLF / trailing-newline edits while still
22
+ # detecting substantive policy changes. Conservative boundary: when in doubt
23
+ # whether an edit is trivial vs substantive, this helper treats it as
24
+ # substantive (re-review fires). Only whitespace + line-ending + trailing-
25
+ # newline are normalised in this iteration — single-numeral edits and
26
+ # frontmatter-key changes are intentionally NOT normalised. See ADR-009
27
+ # 2026-06-06 amendment for the ratified contract.
28
+ #
29
+ # `_atomic_mark_with_hash` writes the marker + hash file as an atomic pair
30
+ # (mktemp + mv) so a PASS NEVER silently fails to persist (the empirically-
31
+ # measured P353 failure mode that forced BYPASS_RISK_GATE=1 on every
32
+ # external-comms gate clearance). Either both files land, or neither does.
33
+ # Non-zero exit on failure so callers can emit a diagnostic.
34
+ # ---------------------------------------------------------------------------
35
+
36
+ # Substance-aware hash of a file or directory path.
37
+ # For directories: hashes the concatenated content of all *.md files
38
+ # (excluding README.md) in sorted order.
39
+ # For files: hashes the file content.
40
+ # Normalisation BEFORE hashing: CRLF → LF, strip trailing whitespace per
41
+ # line, normalise trailing whitespace to a single \n.
42
+ # Echoes "missing" for paths that do not exist (drop-in equivalence with the
43
+ # pre-amendment `cat | _hashcmd | cut -d' ' -f1` site behaviour).
44
+ # Echoes a hex sha256 of the normalised content on success.
45
+ _substance_hash_path() {
46
+ local path="$1"
47
+ if [ -z "$path" ]; then
48
+ echo "missing"
49
+ return 0
50
+ fi
51
+ if [ -f "$path" ]; then
52
+ cat "$path" 2>/dev/null | _substance_normalize_then_hash
53
+ elif [ -d "$path" ]; then
54
+ find "$path" -name '*.md' -not -name 'README.md' -print0 \
55
+ | sort -z \
56
+ | xargs -0 cat 2>/dev/null \
57
+ | _substance_normalize_then_hash
58
+ else
59
+ echo "missing"
60
+ fi
61
+ }
62
+
63
+ # Internal: reads from stdin, normalises whitespace + line endings, emits a
64
+ # hex sha256 of the normalised content. Conservative boundary documented in
65
+ # ADR-009 2026-06-06 amendment: ambiguous edits stay substantive.
66
+ _substance_normalize_then_hash() {
67
+ python3 -c "
68
+ import sys, hashlib
69
+ data = sys.stdin.buffer.read().decode('utf-8', errors='replace')
70
+ # CRLF / CR -> LF
71
+ data = data.replace('\r\n', '\n').replace('\r', '\n')
72
+ # Strip trailing whitespace per line.
73
+ lines = [line.rstrip() for line in data.split('\n')]
74
+ # Re-join and normalise trailing whitespace to a single \n.
75
+ normalised = '\n'.join(lines).rstrip() + '\n'
76
+ print(hashlib.sha256(normalised.encode('utf-8')).hexdigest())
77
+ " 2>/dev/null || echo "missing"
78
+ }
79
+
80
+ # Atomically write a presence marker + its paired hash file. Either both
81
+ # files land or neither does. Returns 0 on success, 1 on failure. On failure
82
+ # any partial state is rolled back.
83
+ # Usage: _atomic_mark_with_hash "/tmp/architect-reviewed-${SID}" "$HASH"
84
+ _atomic_mark_with_hash() {
85
+ local marker="$1"
86
+ local hash="$2"
87
+ local hash_file="${marker}.hash"
88
+
89
+ if [ -z "$marker" ]; then
90
+ return 1
91
+ fi
92
+
93
+ local htmp="${hash_file}.tmp.$$.${RANDOM:-0}"
94
+ local mtmp="${marker}.tmp.$$.${RANDOM:-0}"
95
+
96
+ # Write hash to tempfile.
97
+ if ! printf '%s\n' "$hash" > "$htmp" 2>/dev/null; then
98
+ rm -f "$htmp"
99
+ return 1
100
+ fi
101
+ # Write empty marker to tempfile.
102
+ if ! : > "$mtmp" 2>/dev/null; then
103
+ rm -f "$htmp" "$mtmp"
104
+ return 1
105
+ fi
106
+ # Atomic rename: hash file first.
107
+ if ! mv -f "$htmp" "$hash_file" 2>/dev/null; then
108
+ rm -f "$htmp" "$mtmp"
109
+ return 1
110
+ fi
111
+ # Atomic rename: marker second. If this fails, roll back the hash file
112
+ # so we never observe a hash-without-marker half-state.
113
+ if ! mv -f "$mtmp" "$marker" 2>/dev/null; then
114
+ rm -f "$mtmp"
115
+ rm -f "$hash_file"
116
+ return 1
117
+ fi
118
+ return 0
119
+ }
120
+
16
121
  # Paths excluded from pipeline state hashing and docs-only detection.
17
122
  _doc_exclusions() {
18
123
  echo ':!docs/' ':!.risk-reports/' ':!.changeset/' ':!governance/' ':!.claude/plans/' ':!CLAUDE.md' ':!AGENTS.md' ':!PRINCIPLES.md' ':!DECISION-MANAGEMENT.md' ':!AGENTIC_RISK_REGISTER.md' ':!PROBLEM-MANAGEMENT.md'
@@ -34,18 +34,15 @@ check_review_gate() {
34
34
  return 1
35
35
  fi
36
36
 
37
- # 3. Drift detection — policy file hash must match
37
+ # 3. Drift detection — substance-aware policy hash must match
38
+ # (ADR-009 amendment 2026-06-06: trivial whitespace / line-ending /
39
+ # trailing-newline edits do NOT trigger drift; substantive policy
40
+ # changes DO. Conservative boundary — ambiguous edits stay substantive.
41
+ # See gate-helpers.sh::_substance_hash_path.)
38
42
  if [ -f "$HASH_FILE" ] && [ -n "$POLICY_FILE" ]; then
39
43
  local STORED_HASH=$(cat "$HASH_FILE")
40
- local CURRENT_HASH=""
41
- if [ -f "$POLICY_FILE" ]; then
42
- CURRENT_HASH=$(cat "$POLICY_FILE" | _hashcmd | cut -d' ' -f1)
43
- elif [ -d "$POLICY_FILE" ]; then
44
- # Directory (e.g., docs/decisions/) — hash all .md files
45
- CURRENT_HASH=$(find "$POLICY_FILE" -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
46
- else
47
- CURRENT_HASH="missing"
48
- fi
44
+ local CURRENT_HASH
45
+ CURRENT_HASH=$(_substance_hash_path "$POLICY_FILE")
49
46
  if [ "$STORED_HASH" != "$CURRENT_HASH" ]; then
50
47
  rm -f "$MARKER" "$HASH_FILE"
51
48
  REVIEW_GATE_REASON="${SYSTEM} policy file changed since last review. Re-run the ${SYSTEM} agent."
@@ -59,24 +56,29 @@ check_review_gate() {
59
56
  }
60
57
 
61
58
  # Store policy file hash after a successful review.
59
+ # Routes the marker + hash write through `_atomic_mark_with_hash` so a PASS
60
+ # never silently fails to persist (ADR-009 amendment 2026-06-06: closes the
61
+ # "marker doesn't land after PASS" failure mode P353 measured as ~12
62
+ # subagent invocations + 3 BYPASS_RISK_GATE=1 uses per 3-filing session).
62
63
  # Usage: store_review_hash "$SESSION_ID" "style-guide" "docs/STYLE-GUIDE.md"
63
64
  store_review_hash() {
64
65
  local SESSION_ID="$1"
65
66
  local SYSTEM="$2"
66
67
  local POLICY_FILE="$3"
67
- local HASH_FILE="/tmp/${SYSTEM}-reviewed-${SESSION_ID}.hash"
68
+ local MARKER="/tmp/${SYSTEM}-reviewed-${SESSION_ID}"
68
69
 
69
70
  if [ -n "$POLICY_FILE" ]; then
70
- local HASH=""
71
- if [ -f "$POLICY_FILE" ]; then
72
- HASH=$(cat "$POLICY_FILE" | _hashcmd | cut -d' ' -f1)
73
- elif [ -d "$POLICY_FILE" ]; then
74
- HASH=$(find "$POLICY_FILE" -name '*.md' -not -name 'README.md' -print0 | sort -z | xargs -0 cat 2>/dev/null | _hashcmd | cut -d' ' -f1)
75
- else
76
- HASH="missing"
71
+ local HASH
72
+ HASH=$(_substance_hash_path "$POLICY_FILE")
73
+ # Atomic: marker + hash either both land or neither does.
74
+ if ! _atomic_mark_with_hash "$MARKER" "$HASH"; then
75
+ # Diagnostic on failure surface the silent-fail mode the
76
+ # pre-amendment `touch + echo > .hash` pair hid.
77
+ echo "WARN: ${SYSTEM}-mark-reviewed atomic marker-write failed for ${MARKER}" >&2
78
+ return 1
77
79
  fi
78
- echo "$HASH" > "$HASH_FILE"
79
80
  fi
81
+ return 0
80
82
  }
81
83
 
82
84
  # Emit fail-closed deny JSON for PreToolUse hooks.
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Behavioural tests for ADR-009 amendment 2026-06-06: substance-aware drift +
4
+ # atomic verdict-write — style-guide gate. Sibling-coverage to architect /
5
+ # jtbd / voice-tone bats files of the same name.
6
+
7
+ setup() {
8
+ SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
9
+ source "$SCRIPT_DIR/lib/review-gate.sh"
10
+
11
+ TEST_SESSION="bats-sg-substance-$$-${BATS_TEST_NUMBER}"
12
+ SYSTEM="style-guide"
13
+ TEST_DIR=$(mktemp -d -t substance-aware-drift-sg.XXXXXX)
14
+ POLICY_FILE="$TEST_DIR/STYLE-GUIDE.md"
15
+
16
+ MARKER="/tmp/${SYSTEM}-reviewed-${TEST_SESSION}"
17
+ HASH_FILE="${MARKER}.hash"
18
+ rm -f "$MARKER" "$HASH_FILE"
19
+ }
20
+
21
+ teardown() {
22
+ rm -f "$MARKER" "$HASH_FILE"
23
+ rm -rf "$TEST_DIR"
24
+ }
25
+
26
+ _install_and_mark() {
27
+ local body="$1"
28
+ printf '%s' "$body" > "$POLICY_FILE"
29
+ touch "$MARKER"
30
+ store_review_hash "$TEST_SESSION" "$SYSTEM" "$POLICY_FILE"
31
+ }
32
+
33
+ @test "style-guide substance-aware: trailing-whitespace edit does NOT re-trigger" {
34
+ _install_and_mark "# Style Guide
35
+
36
+ Use 2-space indentation.
37
+ "
38
+ printf '%s' "# Style Guide
39
+
40
+ Use 2-space indentation.
41
+ " > "$POLICY_FILE"
42
+
43
+ run check_review_gate "$TEST_SESSION" "$SYSTEM" "$POLICY_FILE"
44
+ [ "$status" -eq 0 ]
45
+ }
46
+
47
+ @test "style-guide substance-aware: new-rule edit DOES re-trigger" {
48
+ _install_and_mark "# Style Guide
49
+
50
+ Use 2-space indentation.
51
+ "
52
+ printf '%s' "# Style Guide
53
+
54
+ Use 2-space indentation.
55
+ Always trailing-comma.
56
+ " > "$POLICY_FILE"
57
+
58
+ run check_review_gate "$TEST_SESSION" "$SYSTEM" "$POLICY_FILE"
59
+ [ "$status" -ne 0 ]
60
+ }
61
+
62
+ @test "style-guide atomic-write: store_review_hash lands marker + hash together" {
63
+ printf '%s' "# Style Guide
64
+
65
+ Body.
66
+ " > "$POLICY_FILE"
67
+ touch "$MARKER"
68
+ run store_review_hash "$TEST_SESSION" "$SYSTEM" "$POLICY_FILE"
69
+ [ "$status" -eq 0 ]
70
+ [ -f "$MARKER" ]
71
+ [ -f "$HASH_FILE" ]
72
+ }
73
+
74
+ @test "style-guide conservative: single-numeral change DOES re-trigger" {
75
+ _install_and_mark "# Style Guide
76
+
77
+ Use 2-space indentation.
78
+ "
79
+ printf '%s' "# Style Guide
80
+
81
+ Use 4-space indentation.
82
+ " > "$POLICY_FILE"
83
+
84
+ run check_review_gate "$TEST_SESSION" "$SYSTEM" "$POLICY_FILE"
85
+ [ "$status" -ne 0 ]
86
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/style-guide",
3
- "version": "0.4.3",
3
+ "version": "0.4.4-preview.581",
4
4
  "description": "Style guide enforcement for CSS and UI components",
5
5
  "bin": {
6
6
  "windyroad-style-guide": "./bin/install.mjs"