Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@
tests/test_validate_workflow_schedules.bats \
tests/caller_stub_freeze.bats \
tests/pr_review_canary.bats \
tests/dev-lead/unit/test_maintainer_review_thread_gate.bats
tests/dev-lead/unit/test_maintainer_review_thread_gate.bats \
tests/dev-lead/unit/test_conflict_integrity.bats

actionlint:
# General workflow linting (#1256, epic #1052 Part D): actionlint validates
Expand Down Expand Up @@ -205,7 +206,7 @@
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# NOTE: persist-credentials is intentionally left at the default (true) —

Check warning on line 209 in .github/workflows/lint.yml

View workflow job for this annotation

GitHub Actions / Lint

209:9 [comments-indentation] comment not indented like content
# vci_resolve_reusable needs `git fetch origin` to authenticate against this
# private repo when resolving same-repo channel tags. Setting
# persist-credentials: false would silently push every such caller into the
Expand Down
1,354 changes: 93 additions & 1,261 deletions scripts/dev-lead-fix-reviews.sh

Large diffs are not rendered by default.

115 changes: 115 additions & 0 deletions scripts/lib/conflict-integrity.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env bash
# conflict-integrity.sh — post-conflict-resolution integrity check (#1482).
#
# A narrow, mechanical detector for the corruption class from #1449: a botched
# automated conflict resolution that duplicates whole blocks of a shell script
# (grew scripts/engine.sh from 2688 to 4011 lines, doubling run_writer /
# parse_reset_time / extract_verdict_json). Such a resolution "completes" and
# reports success, so nothing on the conflict-resolution path checks it — it only
# surfaces hours later when an unrelated test trips over the corrupted function.
#
# These helpers are pure (no network/git side effects) so they are unit-testable
# in isolation; the git/gh plumbing that feeds them lives in the caller
# (dev-lead-fix-reviews.sh's rebase intent). The detector is deliberately
# mechanical — duplicate top-level declarations — not a semantic "is this diff
# correct" analysis, which is neither tractable nor in scope.

# extract_top_level_symbols <file>
# Emit one line per top-level declaration in file order, tagged by kind:
# fn:NAME — a function declaration (`NAME() {` / `NAME()` / `function NAME`)
# var:NAME — a top-level variable assignment (optionally export/readonly/declare)
# "Top-level" means the declaration begins at column 0 (no leading whitespace),
# so nested functions and in-function `local` assignments are ignored. A symbol
# declared N times appears N times.
extract_top_level_symbols() {
local file="$1"
[ -f "$file" ] || return 0
awk '
# NAME() { / NAME() (POSIX + ksh function forms), column 0 only.
/^[A-Za-z_][A-Za-z0-9_]*[[:space:]]*\(\)[[:space:]]*\{/ {
name = $0
sub(/[[:space:]]*\(\).*$/, "", name)
print "fn:" name
next
}
# function NAME (with or without trailing parens).
/^function[[:space:]]+[A-Za-z_][A-Za-z0-9_]*/ {
name = $2
sub(/\(.*$/, "", name)
print "fn:" name
next
}
# [export|readonly|declare -x] NAME=... top-level assignment, column 0.
/^(export[[:space:]]+|readonly[[:space:]]+|declare[[:space:]]+(-[A-Za-z]+[[:space:]]+)*)?[A-Za-z_][A-Za-z0-9_]*=/ {
line = $0
sub(/^(export[[:space:]]+|readonly[[:space:]]+|declare[[:space:]]+(-[A-Za-z]+[[:space:]]+)*)/, "", line)
name = line
sub(/=.*$/, "", name)
print "var:" name
next
}
' "$file"
}

# symbol_counts <file>
# Emit "SYMBOL<TAB>COUNT" for every top-level symbol, sorted.
symbol_counts() {
extract_top_level_symbols "$1" | LC_ALL=C sort | uniq -c \
| awk '{ print $2 "\t" $1 }'
}

# _count_of <counts-block> <symbol>
# Look up a symbol's count in a "SYMBOL<TAB>COUNT" block; 0 if absent.
_count_of() {
local counts="$1" sym="$2" c
c="$(printf '%s\n' "$counts" | awk -F'\t' -v s="$sym" '$1 == s { print $2; exit }')"
printf '%s' "${c:-0}"
}

# new_duplicate_symbols <resolved> <parent_a> <parent_b>
# Emit "SYMBOL<TAB>COUNT" for each top-level symbol whose declaration count in
# the resolved file EXCEEDS its count in *both* parents — i.e., the resolution
# introduced extra copies. A symbol that already appeared N times in a parent and
# still appears N times is NOT flagged, so a legitimate large upstream merge (or
# an intentional pre-existing repetition) does not trip the check (AC #1, #6).
# A missing parent file counts as zero declarations (a brand-new file that itself
# ships duplicate declarations is still flagged).
new_duplicate_symbols() {
local resolved="$1" parent_a="$2" parent_b="$3"
{
extract_top_level_symbols "$parent_a" | awk '{print "a\t" $0}'
extract_top_level_symbols "$parent_b" | awk '{print "b\t" $0}'
extract_top_level_symbols "$resolved" | awk '{print "r\t" $0}'
} | awk -F'\t' '
$1 == "a" { count_a[$2]++ }
$1 == "b" { count_b[$2]++ }
$1 == "r" { count_r[$2]++ }
END {
for (sym in count_r) {
rcount = count_r[sym]
if (rcount > 1) {
acount = count_a[sym] + 0
bcount = count_b[sym] + 0
maxp = (acount > bcount) ? acount : bcount
if (rcount > maxp) {
print sym "\t" rcount
}
}
}
}
' | LC_ALL=C sort
}
Comment thread
don-petry marked this conversation as resolved.

# format_integrity_findings <file> <findings>
# Render a Markdown bullet naming the file and each duplicated symbol, where
# <findings> is the "SYMBOL<TAB>COUNT" output of new_duplicate_symbols. Emits
# nothing when there are no findings.
format_integrity_findings() {
local file="$1" findings="$2"
[ -n "$findings" ] || return 0
printf -- '- `%s` — duplicated top-level declarations after resolution:\n' "$file"
printf '%s\n' "$findings" | while IFS="$(printf '\t')" read -r sym count; do
[ -n "$sym" ] || continue
printf -- ' - `%s` (declared %s times)\n' "$sym" "$count"
done
}
25 changes: 25 additions & 0 deletions tests/dev-lead/fixtures/conflict-integrity/parent_base.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Fixture: the BASE (main) side of the #1449 conflict — each top-level symbol
# declared exactly once. Represents scripts/engine.sh on `main`.
set -euo pipefail

MAX_RETRIES=3

run_writer() {
local prompt="$1"
echo "writing $prompt"
}

extract_verdict_json() {
local log="$1"
grep -oE '\{.*\}' "$log"
}

parse_reset_time() {
local header="$1"
date -d "$header" +%s
}

build_prompt() {
echo "prompt"
}
31 changes: 31 additions & 0 deletions tests/dev-lead/fixtures/conflict-integrity/parent_branch.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Fixture: the BRANCH (PR head) side of the #1449 conflict — each top-level
# symbol declared exactly once, with the branch's own edits (run_writer gains a
# model arg, plus a branch-only run_reviewer).
set -euo pipefail

MAX_RETRIES=3

run_writer() {
local prompt="$1"
local model="$2"
echo "writing $prompt with $model"
}

extract_verdict_json() {
local log="$1"
grep -oE '\{.*\}' "$log"
}

parse_reset_time() {
local header="$1"
date -d "$header" +%s
}

build_prompt() {
echo "prompt"
}

run_reviewer() {
echo "review"
}
17 changes: 17 additions & 0 deletions tests/dev-lead/fixtures/conflict-integrity/parent_predup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Fixture: a parent that ALREADY declares `legacy_shim` twice (an intentional,
# pre-existing repetition upstream). The detector must not flag a symbol that was
# already duplicated in a parent — only duplication *introduced* by the merge.
set -euo pipefail

legacy_shim() {
echo "shim v1"
}

legacy_shim() {
echo "shim v2"
}

core_a() {
echo a
}
30 changes: 30 additions & 0 deletions tests/dev-lead/fixtures/conflict-integrity/resolved_clean.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Fixture: a CORRECT resolution of the #1449 conflict — the union of both sides,
# each top-level symbol declared exactly once. Must produce NO integrity finding.
set -euo pipefail

MAX_RETRIES=3

run_writer() {
local prompt="$1"
local model="$2"
echo "writing $prompt with $model"
}

extract_verdict_json() {
local log="$1"
grep -oE '\{.*\}' "$log"
}

parse_reset_time() {
local header="$1"
date -d "$header" +%s
}

build_prompt() {
echo "prompt"
}

run_reviewer() {
echo "review"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# Fixture: the BOTCHED resolution of the #1449 conflict — the merge duplicated
# whole blocks, so run_writer / extract_verdict_json / parse_reset_time (and the
# MAX_RETRIES constant) each appear TWICE, roughly doubling the file. This is the
# exact corruption signature that grew scripts/engine.sh from 2688 to 4011 lines.
set -euo pipefail

MAX_RETRIES=3
MAX_RETRIES=3

run_writer() {
local prompt="$1"
echo "writing $prompt"
}

extract_verdict_json() {
local log="$1"
grep -oE '\{.*\}' "$log"
}

parse_reset_time() {
local header="$1"
date -d "$header" +%s
}

build_prompt() {
echo "prompt"
}

run_reviewer() {
echo "review"
}

run_writer() {
local prompt="$1"
local model="$2"
echo "writing $prompt with $model"
}

extract_verdict_json() {
local log="$1"
grep -oE '\{.*\}' "$log"
}

parse_reset_time() {
local header="$1"
date -d "$header" +%s
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Fixture: a LEGITIMATELY large resolution — it carries the pre-existing
# `legacy_shim` double declaration through unchanged (count still 2, same as the
# parent) and adds many brand-new single-declaration functions (a real upstream
# feature merge). Must produce NO integrity finding: the raw size grew, but no
# symbol's declaration count exceeds both parents. (AC #6)
set -euo pipefail

legacy_shim() {
echo "shim v1"
}

legacy_shim() {
echo "shim v2"
}

core_a() {
echo a
}

feature_b() {
echo b
}

feature_c() {
echo c
}

feature_d() {
echo d
}

feature_e() {
echo e
}

feature_f() {
echo f
}
Loading
Loading