@windyroad/architect 0.21.3 → 0.21.4-preview.1105

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.
@@ -123,5 +123,5 @@
123
123
  }
124
124
  },
125
125
  "name": "wr-architect",
126
- "version": "0.21.3"
126
+ "version": "0.21.4"
127
127
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wr-architect",
3
- "version": "0.21.3",
3
+ "version": "0.21.4",
4
4
  "description": "Architecture decision enforcement for AI coding agents",
5
5
  "author": {
6
6
  "name": "Windy Road Technology",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/architect",
3
- "version": "0.21.3",
3
+ "version": "0.21.4-preview.1105",
4
4
  "description": "Architecture decision enforcement for AI coding agents",
5
5
  "bin": {
6
6
  "windyroad-architect": "./bin/install.mjs"
@@ -84,14 +84,31 @@ if [ ! -d "$DECISIONS_DIR" ]; then
84
84
  exit 2
85
85
  fi
86
86
 
87
- # In check mode, redirect generation to a temp file so the on-disk
88
- # compendium is never mutated.
87
+ if ! command -v iconv >/dev/null 2>&1; then
88
+ echo "generate-decisions-compendium: iconv is required for UTF-8 validation" >&2
89
+ exit 2
90
+ fi
91
+
92
+ # Generate into a temporary file so invalid or incomplete output never
93
+ # replaces the last known-good compendium. Write mode uses the target
94
+ # directory so the final rename is atomic.
89
95
  if [ "$CHECK_MODE" = "1" ]; then
90
- COMPENDIUM=$(mktemp -t architect-compendium-check.XXXXXX)
91
- trap 'rm -f "$COMPENDIUM"' EXIT
96
+ if ! COMPENDIUM=$(mktemp -t architect-compendium-check.XXXXXX); then
97
+ echo "generate-decisions-compendium: could not create check-mode temporary file" >&2
98
+ exit 2
99
+ fi
92
100
  else
93
- COMPENDIUM="$TARGET_COMPENDIUM"
101
+ if ! COMPENDIUM=$(mktemp "${TARGET_COMPENDIUM}.tmp.XXXXXX"); then
102
+ echo "generate-decisions-compendium: could not create temporary compendium beside $TARGET_COMPENDIUM" >&2
103
+ exit 2
104
+ fi
94
105
  fi
106
+ cleanup_compendium() {
107
+ if [ -n "${COMPENDIUM:-}" ] && [ -f "$COMPENDIUM" ]; then
108
+ rm -f "$COMPENDIUM"
109
+ fi
110
+ }
111
+ trap cleanup_compendium EXIT
95
112
 
96
113
  # --- Field extractors ------------------------------------------------------
97
114
 
@@ -130,15 +147,18 @@ get_section() {
130
147
  ' "$file"
131
148
  }
132
149
 
133
- # Extract the "Chosen option:" line from the Decision Outcome section.
150
+ # Extract the first "Chosen option:" paragraph from the Decision Outcome section.
134
151
  # Matches the common MADR shapes:
135
152
  # Chosen option: **"X"**, because Y.
136
153
  # Chosen option: X, because Y.
137
154
  # Chosen: X.
138
155
  get_chosen() {
139
156
  get_section "$1" "Decision Outcome" \
140
- | awk '/^Chosen/ { print; exit }' \
141
- | head -1
157
+ | awk '
158
+ /^Chosen/ { in_chosen = 1 }
159
+ in_chosen && NF == 0 { exit }
160
+ in_chosen { print }
161
+ '
142
162
  }
143
163
 
144
164
  # Extract top-level bullet lines (`- ...`) from a section. Skips nested
@@ -157,7 +177,8 @@ get_bullets() {
157
177
  # Joins with "; ". Strips markdown emphasis to keep the line scannable.
158
178
  compact_join_bullets() {
159
179
  local per_item="${1:-120}"
160
- awk -v n="$per_item" '
180
+ local compacted
181
+ if ! compacted=$(awk -v n="$per_item" '
161
182
  {
162
183
  # Strip leading checkbox markers `[ ]` / `[x]` (from Confirmation).
163
184
  sub(/^\[[ x]\] */, "")
@@ -172,7 +193,11 @@ compact_join_bullets() {
172
193
  else out = out "; " line
173
194
  }
174
195
  END { print out }
175
- '
196
+ ' | iconv -f UTF-8 -t UTF-8 -c); then
197
+ echo "generate-decisions-compendium: failed to truncate confirmation text safely" >&2
198
+ return 1
199
+ fi
200
+ printf '%s\n' "$compacted"
176
201
  }
177
202
 
178
203
  # Extract ADR-NNN references from Related bullets. Compact "ADR-NNN" listing
@@ -207,16 +232,27 @@ oneline() {
207
232
  tr '\n\r' ' ' | tr -s ' ' | sed 's/^ *//; s/ *$//'
208
233
  }
209
234
 
210
- # Truncate a string to N chars + ellipsis if longer. Avoids slicing inside
211
- # a markdown emphasis pair (e.g. `**text**`) if the truncation would land
212
- # inside `**...**`, round back to before the opening pair.
235
+ # Truncate a string to N chars + ellipsis if longer. Avoids slicing a `**`
236
+ # marker in half and closes emphasis when truncation lands inside it.
213
237
  truncate_with_ellipsis() {
214
- local s="$1" n="$2"
238
+ local s="$1" n="$2" truncated remainder pairs=0
215
239
  if [ "${#s}" -le "$n" ]; then
216
240
  printf '%s' "$s"
217
241
  return
218
242
  fi
219
- printf '%s' "${s:0:n}..."
243
+ truncated="${s:0:n}"
244
+ if [[ "$truncated" == *\* && "${s:n:1}" == "*" ]]; then
245
+ truncated="${truncated%?}"
246
+ fi
247
+ remainder="$truncated"
248
+ while [[ "$remainder" == *"**"* ]]; do
249
+ remainder="${remainder#*"**"}"
250
+ pairs=$((pairs + 1))
251
+ done
252
+ if [ $((pairs % 2)) -ne 0 ]; then
253
+ truncated="${truncated}**"
254
+ fi
255
+ printf '%s' "${truncated}..."
220
256
  }
221
257
 
222
258
  # --- Per-ADR entry emitter -------------------------------------------------
@@ -242,12 +278,18 @@ emit_entry() {
242
278
 
243
279
  # Chosen-option line — truncate to a comfortable summary length.
244
280
  chosen=$(get_chosen "$file" | strip_links | oneline)
245
- chosen=$(printf '%s' "$chosen" | awk -v n=240 '{ if (length($0) > n) print substr($0,1,n) "..."; else print }')
281
+ if ! chosen=$(truncate_with_ellipsis "$chosen" 240 \
282
+ | iconv -f UTF-8 -t UTF-8 -c); then
283
+ echo "generate-decisions-compendium: failed to truncate chosen text safely for $file" >&2
284
+ return 1
285
+ fi
246
286
 
247
287
  # Confirmation: cap 5 bullets, ≤ 110 chars each, joined with "; " on one line.
248
288
  # This is the routine-compliance scannable view; the full Confirmation list
249
289
  # remains in the per-ADR body for deep-dive surfaces.
250
- confirmation=$(get_bullets "$file" "Confirmation" 5 | strip_links | compact_join_bullets 110)
290
+ if ! confirmation=$(get_bullets "$file" "Confirmation" 5 | strip_links | compact_join_bullets 110); then
291
+ return 1
292
+ fi
251
293
 
252
294
  # Related: extract ADR-NNN graph references only. Full relationship prose
253
295
  # (amends / extends / relates / composes) stays in the per-ADR body.
@@ -311,6 +353,13 @@ done < <(find "$DECISIONS_DIR" -maxdepth 1 -type f -name '*.md' \
311
353
  ! -name '*-summary.md' \
312
354
  2>/dev/null | sort)
313
355
 
356
+ for f in "${all_files[@]}"; do
357
+ if ! iconv -f UTF-8 -t UTF-8 "$f" >/dev/null 2>&1; then
358
+ echo "generate-decisions-compendium: invalid UTF-8 in authoritative ADR: $f" >&2
359
+ exit 1
360
+ fi
361
+ done
362
+
314
363
  in_force_files=()
315
364
  historical_files=()
316
365
  for f in "${all_files[@]}"; do
@@ -364,7 +413,9 @@ total=$((in_force_total + historical_total))
364
413
  echo ""
365
414
  echo "_${in_force_total} ADRs. These are the current rules. The architect agent reads this section first for routine compliance review._"
366
415
  for f in "${in_force_files[@]}"; do
367
- emit_entry "$f"
416
+ if ! emit_entry "$f"; then
417
+ exit 1
418
+ fi
368
419
  done
369
420
  if [ "$historical_total" -gt 0 ]; then
370
421
  echo ""
@@ -374,11 +425,40 @@ total=$((in_force_total + historical_total))
374
425
  echo ""
375
426
  echo "_${historical_total} ADRs. These were tried and superseded, rejected, or deprecated. Read them as direction for what NOT to do, or to understand the lineage of an in-force decision. Do not enforce them as current rules._"
376
427
  for f in "${historical_files[@]}"; do
377
- emit_entry "$f"
428
+ if ! emit_entry "$f"; then
429
+ exit 1
430
+ fi
378
431
  done
379
432
  fi
380
433
  } > "$COMPENDIUM"
381
434
 
435
+ validate_compendium() {
436
+ local file="$1" in_force_count historical_count expected_historical=0
437
+ if ! iconv -f UTF-8 -t UTF-8 "$file" >/dev/null 2>&1; then
438
+ echo "generate-decisions-compendium: generated output is not valid UTF-8" >&2
439
+ return 1
440
+ fi
441
+ if ! in_force_count=$(awk '$0 == "## In-force decisions" { n++ } END { print n + 0 }' "$file"); then
442
+ echo "generate-decisions-compendium: could not validate the in-force section" >&2
443
+ return 1
444
+ fi
445
+ if ! historical_count=$(awk '$0 == "## Historical decisions" { n++ } END { print n + 0 }' "$file"); then
446
+ echo "generate-decisions-compendium: could not validate the historical section" >&2
447
+ return 1
448
+ fi
449
+ if [ "$historical_total" -gt 0 ]; then
450
+ expected_historical=1
451
+ fi
452
+ if [ "$in_force_count" -ne 1 ] || [ "$historical_count" -ne "$expected_historical" ]; then
453
+ echo "generate-decisions-compendium: generated output has invalid section structure (in-force=$in_force_count, historical=$historical_count, expected historical=$expected_historical)" >&2
454
+ return 1
455
+ fi
456
+ }
457
+
458
+ if ! validate_compendium "$COMPENDIUM"; then
459
+ exit 1
460
+ fi
461
+
382
462
  if [ "$CHECK_MODE" = "1" ]; then
383
463
  # Check mode: diff temp against target. Idempotency contract holds only
384
464
  # when both files exist; treat absence as "stale" (drift detected).
@@ -403,4 +483,14 @@ if [ "$CHECK_MODE" = "1" ]; then
403
483
  exit 1
404
484
  fi
405
485
 
406
- echo "generate-decisions-compendium: wrote $COMPENDIUM (${total} ADRs total — ${in_force_total} in-force, ${historical_total} historical)" >&2
486
+ if ! chmod 0644 "$COMPENDIUM"; then
487
+ echo "generate-decisions-compendium: could not set compendium mode to 0644" >&2
488
+ exit 1
489
+ fi
490
+ if ! mv -f "$COMPENDIUM" "$TARGET_COMPENDIUM"; then
491
+ echo "generate-decisions-compendium: could not replace $TARGET_COMPENDIUM" >&2
492
+ exit 1
493
+ fi
494
+ COMPENDIUM=""
495
+
496
+ echo "generate-decisions-compendium: wrote $TARGET_COMPENDIUM (${total} ADRs total — ${in_force_total} in-force, ${historical_total} historical)" >&2