@windyroad/architect 0.17.2 → 0.17.3

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.17.2"
126
+ "version": "0.17.3"
127
127
  }
@@ -169,10 +169,22 @@ if [ -z "$new_entry" ] || ! printf '%s' "$new_entry" | grep -qE '^### ADR-[0-9]+
169
169
  exit 0
170
170
  fi
171
171
 
172
+ # --- Capture pre-modification invariants for the fail-closed guard (P367) ---
173
+ # A single-entry re-author must change ONLY the edited ADR's entry. Snapshot the
174
+ # set of all ADR ids and the count of `## ` section headers, plus a backup of
175
+ # the whole file, so the post-condition guard below can detect (and reject) any
176
+ # silent tail truncation or spurious-id/section injection from the subprocess.
177
+ before_ids=$(grep -oE '^### ADR-[0-9]+' "$readme" | grep -oE '[0-9]+' | sed 's/^0*//' | sort -n -u)
178
+ before_sections=$(grep -cE '^## ' "$readme")
179
+ entry_existed=0
180
+ [ -n "$current_entry" ] && entry_existed=1
181
+
172
182
  # --- Apply the entry: delete any existing block, then insert sorted ---------
173
183
  tmp_entry=$(mktemp -t architect-entry.XXXXXX)
174
184
  tmp_readme=$(mktemp -t architect-readme.XXXXXX)
175
- trap 'rm -f "$tmp_entry" "$tmp_readme"' EXIT
185
+ backup_readme=$(mktemp -t architect-readme-orig.XXXXXX)
186
+ trap 'rm -f "$tmp_entry" "$tmp_readme" "$backup_readme"' EXIT
187
+ cp "$readme" "$backup_readme"
176
188
  printf '%s\n' "$new_entry" > "$tmp_entry"
177
189
 
178
190
  # Pass 1 — remove any existing block for this ADR-ID (and the single blank line
@@ -218,6 +230,26 @@ awk -v id="$adr_id" -v section="$target_section" -v entryfile="$tmp_entry" '
218
230
  END { if (!done) { print ""; print entry } }
219
231
  ' "$tmp_readme" > "$readme"
220
232
 
233
+ # --- Fail-closed post-condition guard (P367, ADR-078 criterion l) -----------
234
+ # The rewrite must preserve every OTHER ADR's entry and the section structure;
235
+ # only the edited ADR's entry may change (it may be newly added). If the result
236
+ # dropped a pre-existing entry (silent tail truncation) or injected spurious ids
237
+ # or sections (malformed subprocess emit), restore the original and degrade —
238
+ # never stage a corrupted compendium. Same contract as the subprocess-failure
239
+ # path: exit 0, do not block the body edit; Story B's pairing check surfaces it.
240
+ after_ids=$(grep -oE '^### ADR-[0-9]+' "$readme" | grep -oE '[0-9]+' | sed 's/^0*//' | sort -n -u)
241
+ after_sections=$(grep -cE '^## ' "$readme")
242
+ expected_ids="$before_ids"
243
+ if [ "$entry_existed" -eq 0 ]; then
244
+ expected_ids=$(printf '%s\n%s\n' "$before_ids" "$adr_id" | sed '/^$/d' | sort -n -u)
245
+ fi
246
+ edited_count=$(grep -oE '^### ADR-[0-9]+' "$readme" | grep -oE '[0-9]+' | sed 's/^0*//' | grep -cxF "$adr_id")
247
+ if [ "$after_ids" != "$expected_ids" ] || [ "$after_sections" != "$before_sections" ] || [ "$edited_count" -ne 1 ]; then
248
+ cp "$backup_readme" "$readme"
249
+ echo "architect-compendium-update-entry: post-condition guard tripped for ADR-${adr_id} (compendium entry-set or section drift — possible truncation or spurious injection); restored README unchanged (degraded mode), not staged. Recover with wr-architect-generate-decisions-compendium && git add docs/decisions/README.md" >&2
250
+ exit 0
251
+ fi
252
+
221
253
  # Stage the compendium so it lands in the same commit as the ADR body change.
222
254
  ( cd "$project_dir" && git add docs/decisions/README.md 2>/dev/null ) || \
223
255
  echo "architect-compendium-update-entry: git add docs/decisions/README.md failed (not a git repo or staging error) — stage it manually before commit" >&2
@@ -234,6 +234,64 @@ run_hook() {
234
234
  [ "$status" -eq 0 ]
235
235
  }
236
236
 
237
+ @test "fail-closed guard: rejects a subprocess entry that injects spurious ADR ids/sections — restores README, degraded, unstaged (P367)" {
238
+ mk_readme
239
+ ( cd "$PROJ" && git add -A && git commit -q -m init )
240
+ before=$(cat "$PROJ/docs/decisions/README.md")
241
+ fp=$(mk_adr "049" "accepted" "FortyNine")
242
+ # Malformed shim: valid header for the edited id, but ALSO injects an
243
+ # unrelated ADR-999 header and a spurious '## ' section — the additive
244
+ # corruption shape empirically reproduced for P367.
245
+ BADDIR="$(mktemp -d)"
246
+ cat > "$BADDIR/claude" <<'SHIM'
247
+ #!/usr/bin/env bash
248
+ cat >/dev/null
249
+ entry="### ADR-049 — Hijacked
250
+ **Status:** accepted | **Oversight:** confirmed
251
+ **Decides:** body.
252
+
253
+ ## Injected section
254
+
255
+ ### ADR-999 — Sneaky"
256
+ jq -cn --arg r "$entry" '{result:$r}'
257
+ SHIM
258
+ chmod +x "$BADDIR/claude"
259
+ export PATH="$BADDIR:$ORIG_PATH"
260
+ run run_hook "$fp"
261
+ [ "$status" -eq 0 ] # never blocks the body edit
262
+ [ "$before" = "$(cat "$PROJ/docs/decisions/README.md")" ] # restored unchanged
263
+ ! grep -q 'ADR-999' "$PROJ/docs/decisions/README.md" # no injected id survives
264
+ [[ "$output" == *"guard"* ]] # observable degraded signal
265
+ # README left in its committed state — no corrupted blob staged.
266
+ ( cd "$PROJ" && git diff --cached --quiet -- docs/decisions/README.md )
267
+ rm -rf "$BADDIR"
268
+ }
269
+
270
+ @test "fail-closed guard: rejects an emit for the wrong ADR id (edited id absent) — restores, degraded (P367)" {
271
+ mk_readme
272
+ before=$(cat "$PROJ/docs/decisions/README.md")
273
+ fp=$(mk_adr "050" "proposed" "Fifty") # NEW adr — not yet in the compendium
274
+ # Shim emits an entry for the WRONG id (049, which already exists) instead of
275
+ # the edited 050: the edited id never lands and 049 is duplicated.
276
+ WRONGDIR="$(mktemp -d)"
277
+ cat > "$WRONGDIR/claude" <<'SHIM'
278
+ #!/usr/bin/env bash
279
+ cat >/dev/null
280
+ entry="### ADR-049 — WrongId
281
+ **Status:** proposed | **Oversight:** confirmed
282
+ **Decides:** body."
283
+ jq -cn --arg r "$entry" '{result:$r}'
284
+ SHIM
285
+ chmod +x "$WRONGDIR/claude"
286
+ export PATH="$WRONGDIR:$ORIG_PATH"
287
+ run run_hook "$fp"
288
+ [ "$status" -eq 0 ]
289
+ [ "$before" = "$(cat "$PROJ/docs/decisions/README.md")" ] # restored unchanged
290
+ ! grep -q '^### ADR-050' "$PROJ/docs/decisions/README.md" # edited id never landed
291
+ [[ "$output" == *"guard"* ]]
292
+ rm -rf "$WRONGDIR"
293
+ }
294
+
237
295
  @test "registered in hooks.json on PostToolUse Edit|Write (criterion 9)" {
238
296
  HOOKS_JSON="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/hooks.json"
239
297
  run jq -e '.hooks.PostToolUse[] | select(.matcher | test("Edit")) | .hooks[] | select(.command | test("architect-compendium-update-entry"))' "$HOOKS_JSON"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/architect",
3
- "version": "0.17.2",
3
+ "version": "0.17.3",
4
4
  "description": "Architecture decision enforcement for AI coding agents",
5
5
  "bin": {
6
6
  "windyroad-architect": "./bin/install.mjs"