@windyroad/architect 0.21.3-preview.1085 → 0.21.4-preview.1104

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-preview.1085",
3
+ "version": "0.21.4-preview.1104",
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
 
@@ -157,7 +174,8 @@ get_bullets() {
157
174
  # Joins with "; ". Strips markdown emphasis to keep the line scannable.
158
175
  compact_join_bullets() {
159
176
  local per_item="${1:-120}"
160
- awk -v n="$per_item" '
177
+ local compacted
178
+ if ! compacted=$(awk -v n="$per_item" '
161
179
  {
162
180
  # Strip leading checkbox markers `[ ]` / `[x]` (from Confirmation).
163
181
  sub(/^\[[ x]\] */, "")
@@ -172,7 +190,11 @@ compact_join_bullets() {
172
190
  else out = out "; " line
173
191
  }
174
192
  END { print out }
175
- '
193
+ ' | iconv -f UTF-8 -t UTF-8 -c); then
194
+ echo "generate-decisions-compendium: failed to truncate confirmation text safely" >&2
195
+ return 1
196
+ fi
197
+ printf '%s\n' "$compacted"
176
198
  }
177
199
 
178
200
  # Extract ADR-NNN references from Related bullets. Compact "ADR-NNN" listing
@@ -242,12 +264,19 @@ emit_entry() {
242
264
 
243
265
  # Chosen-option line — truncate to a comfortable summary length.
244
266
  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 }')
267
+ if ! chosen=$(printf '%s' "$chosen" \
268
+ | awk -v n=240 '{ if (length($0) > n) print substr($0,1,n) "..."; else print }' \
269
+ | iconv -f UTF-8 -t UTF-8 -c); then
270
+ echo "generate-decisions-compendium: failed to truncate chosen text safely for $file" >&2
271
+ return 1
272
+ fi
246
273
 
247
274
  # Confirmation: cap 5 bullets, ≤ 110 chars each, joined with "; " on one line.
248
275
  # This is the routine-compliance scannable view; the full Confirmation list
249
276
  # remains in the per-ADR body for deep-dive surfaces.
250
- confirmation=$(get_bullets "$file" "Confirmation" 5 | strip_links | compact_join_bullets 110)
277
+ if ! confirmation=$(get_bullets "$file" "Confirmation" 5 | strip_links | compact_join_bullets 110); then
278
+ return 1
279
+ fi
251
280
 
252
281
  # Related: extract ADR-NNN graph references only. Full relationship prose
253
282
  # (amends / extends / relates / composes) stays in the per-ADR body.
@@ -311,6 +340,13 @@ done < <(find "$DECISIONS_DIR" -maxdepth 1 -type f -name '*.md' \
311
340
  ! -name '*-summary.md' \
312
341
  2>/dev/null | sort)
313
342
 
343
+ for f in "${all_files[@]}"; do
344
+ if ! iconv -f UTF-8 -t UTF-8 "$f" >/dev/null 2>&1; then
345
+ echo "generate-decisions-compendium: invalid UTF-8 in authoritative ADR: $f" >&2
346
+ exit 1
347
+ fi
348
+ done
349
+
314
350
  in_force_files=()
315
351
  historical_files=()
316
352
  for f in "${all_files[@]}"; do
@@ -364,7 +400,9 @@ total=$((in_force_total + historical_total))
364
400
  echo ""
365
401
  echo "_${in_force_total} ADRs. These are the current rules. The architect agent reads this section first for routine compliance review._"
366
402
  for f in "${in_force_files[@]}"; do
367
- emit_entry "$f"
403
+ if ! emit_entry "$f"; then
404
+ exit 1
405
+ fi
368
406
  done
369
407
  if [ "$historical_total" -gt 0 ]; then
370
408
  echo ""
@@ -374,11 +412,40 @@ total=$((in_force_total + historical_total))
374
412
  echo ""
375
413
  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
414
  for f in "${historical_files[@]}"; do
377
- emit_entry "$f"
415
+ if ! emit_entry "$f"; then
416
+ exit 1
417
+ fi
378
418
  done
379
419
  fi
380
420
  } > "$COMPENDIUM"
381
421
 
422
+ validate_compendium() {
423
+ local file="$1" in_force_count historical_count expected_historical=0
424
+ if ! iconv -f UTF-8 -t UTF-8 "$file" >/dev/null 2>&1; then
425
+ echo "generate-decisions-compendium: generated output is not valid UTF-8" >&2
426
+ return 1
427
+ fi
428
+ if ! in_force_count=$(awk '$0 == "## In-force decisions" { n++ } END { print n + 0 }' "$file"); then
429
+ echo "generate-decisions-compendium: could not validate the in-force section" >&2
430
+ return 1
431
+ fi
432
+ if ! historical_count=$(awk '$0 == "## Historical decisions" { n++ } END { print n + 0 }' "$file"); then
433
+ echo "generate-decisions-compendium: could not validate the historical section" >&2
434
+ return 1
435
+ fi
436
+ if [ "$historical_total" -gt 0 ]; then
437
+ expected_historical=1
438
+ fi
439
+ if [ "$in_force_count" -ne 1 ] || [ "$historical_count" -ne "$expected_historical" ]; then
440
+ echo "generate-decisions-compendium: generated output has invalid section structure (in-force=$in_force_count, historical=$historical_count, expected historical=$expected_historical)" >&2
441
+ return 1
442
+ fi
443
+ }
444
+
445
+ if ! validate_compendium "$COMPENDIUM"; then
446
+ exit 1
447
+ fi
448
+
382
449
  if [ "$CHECK_MODE" = "1" ]; then
383
450
  # Check mode: diff temp against target. Idempotency contract holds only
384
451
  # when both files exist; treat absence as "stale" (drift detected).
@@ -403,4 +470,14 @@ if [ "$CHECK_MODE" = "1" ]; then
403
470
  exit 1
404
471
  fi
405
472
 
406
- echo "generate-decisions-compendium: wrote $COMPENDIUM (${total} ADRs total — ${in_force_total} in-force, ${historical_total} historical)" >&2
473
+ if ! chmod 0644 "$COMPENDIUM"; then
474
+ echo "generate-decisions-compendium: could not set compendium mode to 0644" >&2
475
+ exit 1
476
+ fi
477
+ if ! mv -f "$COMPENDIUM" "$TARGET_COMPENDIUM"; then
478
+ echo "generate-decisions-compendium: could not replace $TARGET_COMPENDIUM" >&2
479
+ exit 1
480
+ fi
481
+ COMPENDIUM=""
482
+
483
+ echo "generate-decisions-compendium: wrote $TARGET_COMPENDIUM (${total} ADRs total — ${in_force_total} in-force, ${historical_total} historical)" >&2