Skip to content

Commit c9a59ae

Browse files
Refine agent governance global dependency downgrade (#7591)
* feat: enforce global dependency budget * docs: document global dependency budget * ci: surface governance warnings on PRs --------- Co-authored-by: QuantumMisaka <QuantumMisaka@users.noreply.github.com>
1 parent 36fa6a1 commit c9a59ae

8 files changed

Lines changed: 164 additions & 37 deletions

File tree

.coderabbit.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ reviews:
1515
Focus on newly introduced GlobalV/GlobalC/PARAM dependencies, default
1616
parameters in headers, module placement, CMakeLists.txt linkage, C++11
1717
compatibility, and focused tests for behavior changes.
18+
During the legacy global-state migration period, treat a net increase in
19+
GlobalV/GlobalC/PARAM code references as blocking, and treat
20+
migration-neutral added usage as reviewer-visible warnings requiring
21+
reason, scope, risk, and cleanup rationale.
1822
- path: "source/source_io/module_parameter/**"
1923
instructions: |
2024
Treat INPUT parameter metadata, parsing, defaults, descriptions, and

.github/instructions/abacus-governance.instructions.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ Apply these instructions when reviewing or changing ABACUS code:
1212
newly introduced symbols, and changed text files for line-ending checks.
1313
- Do not treat untouched historical debt as a default blocker. Mention it only
1414
when it affects the changed area, and label it as advisory.
15-
- Flag newly introduced `GlobalV`, `GlobalC`, or `PARAM` cross-layer control.
15+
- Flag PRs that increase `GlobalV`, `GlobalC`, or `PARAM` code references as
16+
blocker-level governance issues. Flag migration-neutral added usage as a
17+
warning that requires reason, scope, risk, and cleanup/follow-up rationale.
1618
Prefer explicit dependencies or narrow local interfaces.
1719
- Flag new default arguments in existing header interfaces. Prefer explicit
1820
call-site updates, overloads, or a clearer configuration object.

.github/pull_request_template.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Fix #...
2222
- Example: My changes might affect the performance of the application under certain conditions, and I have tested the impact on various scenarios...
2323

2424
### Governance Checklist
25-
- Global dependencies: no new `GlobalV`, `GlobalC`, or `PARAM` cross-layer control, or exception requested below.
25+
- Global dependencies: no net increase in `GlobalV`, `GlobalC`, or `PARAM` code references, or exception requested below with reason, scope, risk, and cleanup plan.
2626
- Default parameters: no new default arguments added to existing interfaces, or exception requested below.
2727
- Headers: no unnecessary header dependencies or `.hpp` propagation, or rationale provided below.
2828
- Line endings: text files use LF; only `.bat` and `.cmd` use CRLF.

.github/workflows/agent_governance.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
permissions:
88
contents: read
99
pull-requests: read
10+
issues: write
1011

1112
jobs:
1213
governance:
@@ -53,3 +54,40 @@ jobs:
5354
if: always()
5455
run: |
5556
cat agent_governance_summary.md >> "$GITHUB_STEP_SUMMARY"
57+
58+
- name: Comment governance warnings
59+
if: >-
60+
always() &&
61+
github.event.pull_request.head.repo.full_name == github.repository
62+
env:
63+
GH_TOKEN: ${{ github.token }}
64+
PR_NUMBER: ${{ github.event.pull_request.number }}
65+
run: |
66+
if ! grep -qi '^| warning |' agent_governance_summary.md; then
67+
exit 0
68+
fi
69+
70+
marker='<!-- agent-governance-warning -->'
71+
body_file="$(mktemp)"
72+
json_file="$(mktemp)"
73+
{
74+
echo "$marker"
75+
echo
76+
cat agent_governance_summary.md
77+
} > "$body_file"
78+
jq -Rs '{body: .}' < "$body_file" > "$json_file"
79+
80+
existing_comment_id="$(gh api --paginate \
81+
"repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
82+
--jq ".[] | select(.body | contains(\"${marker}\")) | .id" \
83+
| head -n 1)"
84+
85+
if [ -n "$existing_comment_id" ]; then
86+
gh api --method PATCH \
87+
"repos/${GITHUB_REPOSITORY}/issues/comments/${existing_comment_id}" \
88+
--input "$json_file"
89+
else
90+
gh api --method POST \
91+
"repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
92+
--input "$json_file"
93+
fi

AGENTS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ rules. Read the complete governance document before making or reviewing changes:
99
## Required Baseline
1010

1111
- Follow the seven ABACUS coding rules summarized from the project governance:
12-
1. Do not introduce new cross-layer control through `GlobalV`, `GlobalC`, or
13-
`PARAM`; pass dependencies explicitly.
12+
1. Do not increase cross-layer control through `GlobalV`, `GlobalC`, or
13+
`PARAM`; pass dependencies explicitly where practical. Migration-neutral
14+
moves must keep the PR-level global dependency budget non-increasing and
15+
explain the remaining global usage.
1416
2. Do not hide workflow switches in mutable member variables that can be
1517
changed from multiple places.
1618
3. Keep header dependencies minimal.

docs/developers_guide/agent_governance.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ decisions.
6969
| --- | --- | --- | --- | --- | --- | --- | --- |
7070
| Basic text format | LF line endings | phase-one mechanical | hook + CI | medium | block | full changed text file | `.bat` and `.cmd` keep CRLF |
7171
| Language baseline | C++11 compatibility | build/toolchain | CI | high | block | build/static tooling | Actual compiler/toolchain result wins |
72-
| New global dependency | Added `GlobalV`/`GlobalC`/`PARAM` as cross-layer control | phase-one mechanical + AI review | CI + AI review | high | block | added code lines | Historical untouched usage and documentation mentions are not blocked |
72+
| Global dependency budget | Net increase of `GlobalV`/`GlobalC`/`PARAM` references in code diff | phase-one mechanical + AI review | CI + AI review | high | block on net increase, warn on non-increasing added usage | added and removed code lines | Historical untouched usage and documentation mentions are not blocked; migration-neutral moves require reviewer rationale |
7373
| New default parameter | Header declaration adds a default argument | phase-one mechanical + AI review | CI + AI review | high | block | header diff | High misuse risk |
7474
| `.hpp` propagation | New `.hpp` or header includes `.hpp` | phase-one mechanical warning | CI + AI review | medium | warn | new files and added includes | Exception can be recorded in PR |
7575
| Header dependency growth | Header diff adds includes | phase-one mechanical warning + AI review | CI + AI review | medium | warn | added header includes | Necessity is semantic and not mechanically decided |
@@ -97,6 +97,13 @@ explicit governance change. For header include warnings, the rationale should
9797
state whether the header needs a complete type, for example because it owns a
9898
value member rather than a pointer or reference.
9999

100+
For global dependencies, the mechanical checker uses a PR-level budget during
101+
the legacy migration period. A PR blocks only when the number of code references
102+
to `GlobalV`, `GlobalC`, or `PARAM` increases after accounting for deleted
103+
references. If a PR adds global references while deleting at least as many
104+
elsewhere, the checker warns instead of blocking; reviewers should confirm the
105+
change is a migration-neutral move or part of a cleanup path.
106+
100107
## Automation Responsibilities
101108

102109
Local hooks:

tools/03_code_analysis/agent_governance_check.py

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -120,42 +120,52 @@ def changed_paths(root: Path, args: argparse.Namespace) -> Tuple[Dict[str, str],
120120
return parse_name_status(output)
121121

122122

123-
def parse_added_lines(diff_text: str) -> List[DiffLine]:
124-
lines: List[DiffLine] = []
125-
path = ""
123+
def parse_changed_lines(diff_text: str) -> Tuple[List[DiffLine], List[DiffLine]]:
124+
added: List[DiffLine] = []
125+
removed: List[DiffLine] = []
126+
old_path = ""
127+
new_path = ""
128+
old_line: Optional[int] = None
126129
new_line: Optional[int] = None
127-
hunk_re = re.compile(r"@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
130+
hunk_re = re.compile(r"@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@")
128131
for raw in diff_text.splitlines():
132+
if raw.startswith("--- a/"):
133+
old_path = raw[6:]
134+
continue
129135
if raw.startswith("+++ b/"):
130-
path = raw[6:]
136+
new_path = raw[6:]
131137
continue
132-
if raw.startswith("+++ "):
133-
path = raw[4:]
138+
if raw.startswith("--- ") or raw.startswith("+++ "):
134139
continue
135140
match = hunk_re.match(raw)
136141
if match:
137-
new_line = int(match.group(1))
142+
old_line = int(match.group(1))
143+
new_line = int(match.group(2))
144+
continue
145+
if old_line is None or new_line is None:
138146
continue
139-
if new_line is None:
147+
if raw.startswith("\\"):
140148
continue
141149
if raw.startswith("+") and not raw.startswith("+++"):
142-
lines.append(DiffLine(path, new_line, raw[1:]))
150+
added.append(DiffLine(new_path, new_line, raw[1:]))
143151
new_line += 1
144152
elif raw.startswith("-") and not raw.startswith("---"):
145-
continue
153+
removed.append(DiffLine(old_path, old_line, raw[1:]))
154+
old_line += 1
146155
else:
156+
old_line += 1
147157
new_line += 1
148-
return lines
158+
return added, removed
149159

150160

151-
def added_lines(root: Path, args: argparse.Namespace) -> List[DiffLine]:
161+
def changed_lines(root: Path, args: argparse.Namespace) -> Tuple[List[DiffLine], List[DiffLine]]:
152162
if args.staged:
153163
output = git(["diff", "--cached", "--ignore-cr-at-eol", "-U0"], root).stdout
154164
elif args.base and args.head:
155165
output = git(["diff", "--ignore-cr-at-eol", "-U0", args.base, args.head], root).stdout
156166
else:
157167
output = ""
158-
return parse_added_lines(output)
168+
return parse_changed_lines(output)
159169

160170

161171
def read_changed_file_bytes(root: Path, path: str, args: argparse.Namespace) -> bytes:
@@ -232,22 +242,57 @@ def check_line_endings(
232242
)
233243

234244

235-
def check_global_dependencies(findings: List[Finding], lines: Iterable[DiffLine]) -> None:
236-
pattern = re.compile(r"\b(GlobalV::|GlobalC::|PARAM(?:\.|->|::|\b))")
245+
GLOBAL_DEPENDENCY_RE = re.compile(r"\b(GlobalV::|GlobalC::|PARAM(?:\.|->|::|\b))")
246+
247+
248+
def is_global_dependency_check_path(path: str) -> bool:
249+
if path.startswith("tools/03_code_analysis/"):
250+
return False
251+
return Path(path).suffix.lower() in CODE_EXTENSIONS
252+
253+
254+
def global_dependency_hits(lines: Iterable[DiffLine]) -> List[Tuple[DiffLine, int]]:
255+
hits: List[Tuple[DiffLine, int]] = []
237256
for line in lines:
238-
if line.path.startswith("tools/03_code_analysis/"):
257+
if not is_global_dependency_check_path(line.path):
239258
continue
240-
if Path(line.path).suffix.lower() not in CODE_EXTENSIONS:
241-
continue
242-
if pattern.search(line.content):
243-
add_finding(
259+
count = len(GLOBAL_DEPENDENCY_RE.findall(line.content))
260+
if count:
261+
hits.append((line, count))
262+
return hits
263+
264+
265+
def check_global_dependencies(
266+
findings: List[Finding],
267+
added_lines: Iterable[DiffLine],
268+
removed_lines: Iterable[DiffLine],
269+
) -> None:
270+
added_hits = global_dependency_hits(added_lines)
271+
removed_hits = global_dependency_hits(removed_lines)
272+
added_count = sum(count for _, count in added_hits)
273+
removed_count = sum(count for _, count in removed_hits)
274+
delta = added_count - removed_count
275+
if added_count == 0:
276+
return
277+
278+
severity = BLOCK if delta > 0 else WARN
279+
action = (
280+
"Reduce or explicitly pass dependencies so this PR does not increase global dependency usage."
281+
if delta > 0
282+
else "Confirm this is a migration-neutral move or partial cleanup, and explain the remaining global dependency rationale."
283+
)
284+
for line, count in added_hits:
285+
add_finding(
244286
findings,
245-
"No new cross-layer globals",
246-
WARN,
287+
"Global dependency budget",
288+
severity,
247289
line.path,
248290
line.line,
249-
"Added line introduces GlobalV, GlobalC, or PARAM as a dependency.",
250-
"Prefer explicit parameters or a narrow local interface. Document any required exception in the PR.",
291+
(
292+
f"Added line introduces {count} GlobalV/GlobalC/PARAM reference(s); "
293+
f"PR total added={added_count}, removed={removed_count}, net_delta={delta}."
294+
),
295+
action,
251296
)
252297

253298

@@ -518,7 +563,7 @@ def check_pr_metadata(findings: List[Finding], body: Optional[str]) -> None:
518563
add_finding(
519564
findings,
520565
"PR metadata completeness",
521-
WARN,
566+
BLOCK,
522567
"pull_request.body",
523568
None,
524569
"; ".join(reason_parts),
@@ -621,12 +666,12 @@ def check_documentation_warning(findings: List[Finding], changed: Sequence[str],
621666
def collect_findings(root: Path, args: argparse.Namespace) -> List[Finding]:
622667
findings: List[Finding] = []
623668
statuses, changed = changed_paths(root, args)
624-
lines = added_lines(root, args)
669+
lines, removed_lines = changed_lines(root, args)
625670
body = read_pr_body(args.event_path)
626671
body_text = body or ""
627672

628673
check_line_endings(findings, root, changed, statuses, args)
629-
check_global_dependencies(findings, lines)
674+
check_global_dependencies(findings, lines, removed_lines)
630675
check_default_parameters(findings, lines)
631676
check_hpp_warnings(findings, statuses, lines)
632677
check_header_include_warnings(findings, lines)

tools/03_code_analysis/test_agent_governance_check.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,43 @@ def test_allows_crlf_in_windows_scripts(self):
9494

9595
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
9696

97-
def test_blocks_new_global_dependencies_on_added_lines(self):
97+
def test_blocks_when_global_dependency_budget_increases(self):
9898
self.write("source/source_base/global.cpp", "int n = GlobalV::NPROC + PARAM.inp.nbands;\n")
9999
self.write("source/source_base/CMakeLists.txt", "add_library(global global.cpp)\n")
100100
head = self.commit_change()
101101

102102
result = self.run_checker("--base", self.base, "--head", head)
103103

104-
self.assert_blocked_by(result, "No new cross-layer globals")
104+
self.assert_blocked_by(result, "Global dependency budget")
105+
self.assertIn("net_delta=2", result.stdout)
106+
107+
def test_warns_when_global_dependency_usage_is_rebalanced(self):
108+
self.write("source/source_base/global.cpp", "int old_n = PARAM.inp.nbands;\n")
109+
self.write("source/source_base/CMakeLists.txt", "add_library(global global.cpp)\n")
110+
self.git("add", ".")
111+
self.git("commit", "-m", "add baseline global usage")
112+
base = self.git("rev-parse", "HEAD").stdout.strip()
113+
self.write("source/source_base/global.cpp", "int moved_n = GlobalV::NPROC;\n")
114+
head = self.commit_change()
115+
116+
result = self.run_checker("--base", base, "--head", head)
117+
118+
self.assert_warns_with_success(result, "Global dependency budget")
119+
self.assertIn("net_delta=0", result.stdout)
120+
121+
def test_allows_global_dependency_budget_reduction(self):
122+
self.write("source/source_base/global.cpp", "int old_n = PARAM.inp.nbands;\n")
123+
self.write("source/source_base/CMakeLists.txt", "add_library(global global.cpp)\n")
124+
self.git("add", ".")
125+
self.git("commit", "-m", "add baseline global usage")
126+
base = self.git("rev-parse", "HEAD").stdout.strip()
127+
self.write("source/source_base/global.cpp", "int old_n = 0;\n")
128+
head = self.commit_change()
129+
130+
result = self.run_checker("--base", base, "--head", head)
131+
132+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
133+
self.assertNotIn("Global dependency budget", result.stdout)
105134

106135
def test_allows_global_names_in_documentation(self):
107136
self.write("docs/governance-notes.md", "Mention GlobalV::NPROC and PARAM.inp in documentation.\n")
@@ -575,14 +604,14 @@ def test_warns_for_heterogeneous_file_without_test_evidence(self):
575604

576605
self.assert_warns_with_success(result, "Heterogeneous test evidence review")
577606

578-
def test_staged_mode_checks_index_content(self):
607+
def test_staged_mode_blocks_global_dependency_budget_increase(self):
579608
self.write("source/source_base/staged.cpp", "int n = GlobalC::ucell.nat;\n")
580609
self.write("source/source_base/CMakeLists.txt", "add_library(staged staged.cpp)\n")
581610
self.git("add", ".")
582611

583612
result = self.run_checker("--staged")
584613

585-
self.assert_blocked_by(result, "No new cross-layer globals")
614+
self.assert_blocked_by(result, "Global dependency budget")
586615

587616
def test_rejects_staged_with_base_head(self):
588617
result = self.run_checker("--staged", "--base", self.base, "--head", self.base)

0 commit comments

Comments
 (0)