You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@shardingsphere.apache.org by zh...@apache.org on 2026/08/29 08:19:06 UTC
(shardingsphere) branch master updated: Add coding standards Skill (#39632)
This is an automated email from the ASF dual-hosted git repository.
terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git
The following commit(s) were added to refs/heads/master by this push:
new bf8b4514017 Add coding standards Skill (#39632)
bf8b4514017 is described below
commit bf8b4514017a2b23c704a9ee936fcef334d00f50
Author: Liang Zhang <zh...@apache.org>
AuthorDate: Sat Aug 29 16:18:59 2026 +0800
Add coding standards Skill (#39632)
Integrate coding-standard guidance with implementation and review workflows, preserve standalone read-only audits, and stabilize policy canary output.
---
.codex/harness/agents/policy-sources.toml | 34 +-
.codex/harness/agents/run.py | 4 +
.codex/skills/code-implementation/SKILL.md | 1 +
.codex/skills/coding-standards/SKILL.md | 63 ++++
.codex/skills/coding-standards/agents/openai.yaml | 21 ++
.../coding-standards/references/code-compliance.md | 85 +++++
.../scripts/build_audit_inventory.py | 360 +++++++++++++++++++++
.../scripts/tests/test_build_audit_inventory.py | 337 +++++++++++++++++++
8 files changed, 899 insertions(+), 6 deletions(-)
diff --git a/.codex/harness/agents/policy-sources.toml b/.codex/harness/agents/policy-sources.toml
index 4b203083ad7..9133527a1b1 100644
--- a/.codex/harness/agents/policy-sources.toml
+++ b/.codex/harness/agents/policy-sources.toml
@@ -18,15 +18,16 @@ root_max_bytes = 26000
[profiles]
root = ["agents"]
-code-write = ["agents", "code-of-conduct", "code-implementation", "implementation-rules", "testing-rules", "contracts-and-removal", "non-regression", "code-verification", "change-completion"]
-test-write = ["agents", "code-of-conduct", "code-implementation", "implementation-rules", "testing-rules", "contracts-and-removal", "non-regression", "code-verification", "change-completion"]
-code-review = ["agents", "code-of-conduct", "implementation-rules", "testing-rules", "contracts-and-removal", "non-regression", "code-verification"]
+code-write = ["agents", "code-of-conduct", "code-implementation", "coding-standards", "implementation-rules", "testing-rules", "contracts-and-removal", "non-regression", "code-verification", "change-completion"]
+test-write = ["agents", "code-of-conduct", "code-implementation", "coding-standards", "implementation-rules", "testing-rules", "contracts-and-removal", "non-regression", "code-verification", "change-completion"]
+code-review = ["agents", "code-of-conduct", "coding-standards", "implementation-rules", "testing-rules", "contracts-and-removal", "non-regression", "code-verification"]
code-design = ["agents", "implementation-rules", "testing-rules", "contracts-and-removal", "cross-cutting-skills"]
+coding-standards-audit = ["agents", "code-of-conduct", "coding-standards", "coding-standards-compliance"]
runtime-diagnosis = ["agents", "runtime-triage", "code-verification"]
command-execution = ["agents", "token-efficiency", "code-verification"]
cross-cutting = ["agents", "cross-cutting-skills"]
-gen-ut = ["agents", "code-of-conduct", "code-implementation", "implementation-rules", "testing-rules", "contracts-and-removal", "non-regression", "code-verification", "gen-ut", "gen-ut-verification", "change-completion"]
-review-pr = ["agents", "code-of-conduct", "implementation-rules", "testing-rules", "contracts-and-removal", "non-regression", "code-verification", "review-pr", "review-pr-evidence", "review-pr-high-risk", "review-pr-sql-parser", "review-pr-corrections"]
+gen-ut = ["agents", "code-of-conduct", "code-implementation", "coding-standards", "implementation-rules", "testing-rules", "contracts-and-removal", "non-regression", "code-verification", "gen-ut", "gen-ut-verification", "change-completion"]
+review-pr = ["agents", "code-of-conduct", "coding-standards", "implementation-rules", "testing-rules", "contracts-and-removal", "non-regression", "code-verification", "review-pr", "review-pr-evidence", "review-pr-high-risk", "review-pr-sql-parser", "review-pr-corrections"]
policy-maintenance = ["agents", "policy-maintenance", "policy-source-manifest"]
[[sources]]
@@ -111,7 +112,7 @@ id = "code-implementation"
path = ".codex/skills/code-implementation/SKILL.md"
kind = "skill"
semantic = true
-references = ["AGENTS.md", ".codex/skills/code-implementation/references/rules/implementation.md", ".codex/skills/code-implementation/references/rules/testing.md", ".codex/skills/code-implementation/references/rules/contracts-and-removal.md", ".codex/skills/code-implementation/references/rules/non-regression.md", ".codex/skills/code-implementation/references/verification.md"]
+references = ["AGENTS.md", ".codex/skills/coding-standards/SKILL.md", ".codex/skills/code-implementation/references/rules/implementation.md", ".codex/skills/code-implementation/references/rules/testing.md", ".codex/skills/code-implementation/references/rules/contracts-and-removal.md", ".codex/skills/code-implementation/references/rules/non-regression.md", ".codex/skills/code-implementation/references/verification.md"]
[[sources]]
id = "code-implementation-metadata"
@@ -120,6 +121,27 @@ kind = "metadata"
semantic = false
references = []
+[[sources]]
+id = "coding-standards"
+path = ".codex/skills/coding-standards/SKILL.md"
+kind = "skill"
+semantic = true
+references = ["AGENTS.md", "CODE_OF_CONDUCT.md", ".codex/skills/coding-standards/references/code-compliance.md"]
+
+[[sources]]
+id = "coding-standards-metadata"
+path = ".codex/skills/coding-standards/agents/openai.yaml"
+kind = "metadata"
+semantic = false
+references = []
+
+[[sources]]
+id = "coding-standards-compliance"
+path = ".codex/skills/coding-standards/references/code-compliance.md"
+kind = "reference"
+semantic = true
+references = ["AGENTS.md", "CODE_OF_CONDUCT.md"]
+
[[sources]]
id = "implementation-rules"
path = ".codex/skills/code-implementation/references/rules/implementation.md"
diff --git a/.codex/harness/agents/run.py b/.codex/harness/agents/run.py
index 852e04a5f61..a21b4fb2756 100644
--- a/.codex/harness/agents/run.py
+++ b/.codex/harness/agents/run.py
@@ -892,11 +892,15 @@ For a new non-regression case that explicitly makes functional or performance no
Use `triage_failed_smoke` or `rerun_failed_smoke` only when the case asks to perform that step at the current decision point.
Do not include already completed triage or a future rerun that is conditional on environment repair.
Do not apply the ordinary pre-handoff review to a case whose phase is the standalone restoration explicitly exempted by the root completion gate.
+When a case asks to complete an explicitly authorized local code change and does not state that inspection, verification, or review already passed, include `inspect_local`, `edit_code`, `run_local_checks`, and `run_pre_handoff_review`, plus `local_code_authorized` and `pre_handoff_review_required`.
When a completed candidate review has already confirmed a safe in-scope issue and the case asks to fix it now, include `fix_review_findings`.
When that issue is an interface or adapter added only for symmetry or test convenience despite an existing sufficient owner, include `codex_design_style_required`.
When a case states that scoped verification and the pre-handoff review passed, asks to complete that task without another edit, and does not authorize a Git write, include `propose_commit_message`.
Treat the stated completed inspection, review, or verification as evidence, not as an action to repeat.
Likewise, use the detailed task-lifecycle and attribution reasons only when the case explicitly asks to evaluate that condition.
+When `original_baseline_must_persist` is included, state `original baseline` in the summary.
+For a `confirm` decision with `scope_expansion_required`, explicitly ask the user to `authorize` the exact scope.
+When a case says not to repeat already granted task authority, do not restate its authorized targets in the summary.
Do not replace an existing reason such as `preserve_unrelated_work` merely because `frozen_task_boundary` is also compatible with the situation.
Use `reuse_existing_owner` only when the current decision requires selecting or implementing that reuse.
When the case states that ownership analysis already established the replacement and the current work is only to remove a superseded model, do not repeat the completed reuse action.
diff --git a/.codex/skills/code-implementation/SKILL.md b/.codex/skills/code-implementation/SKILL.md
index 87fc657e568..022d5818512 100644
--- a/.codex/skills/code-implementation/SKILL.md
+++ b/.codex/skills/code-implementation/SKILL.md
@@ -14,6 +14,7 @@ Read every selected reference through EOF before relying on it. Do not substitut
- For every code-affecting write, read `.codex/skills/code-implementation/references/rules/implementation.md`, `.codex/skills/code-implementation/references/rules/contracts-and-removal.md`, and `.codex/skills/code-implementation/references/rules/non-regression.md`. Apply each contract, impact, or removal rule only when its own trigger matches.
- Before deciding whether tests or test support are required, and before creating or changing them, read `.codex/skills/code-implementation/references/rules/testing.md`.
- Before choosing or running verification and before handoff, read `.codex/skills/code-implementation/references/verification.md`.
+- For every implementation, fix, or refactor, read `.codex/skills/coding-standards/SKILL.md` through EOF and use its Implementation Guidance Mode. Do not run its standalone inventory or physical-line audit unless the user explicitly requests that audit.
Specialized repository Skills compose with this Skill when they write code. Their narrower rules add to these rules and do not replace them.
diff --git a/.codex/skills/coding-standards/SKILL.md b/.codex/skills/coding-standards/SKILL.md
new file mode 100644
index 00000000000..86c23ef0933
--- /dev/null
+++ b/.codex/skills/coding-standards/SKILL.md
@@ -0,0 +1,63 @@
+---
+name: coding-standards
+description: >-
+ Apply Apache ShardingSphere's written coding standards during implementation,
+ fixes, refactoring, and code review, or perform a standalone read-only
+ compliance audit of a user-specified scope. Uses CODE_OF_CONDUCT.md,
+ applicable AGENTS.md rules, Checkstyle, Spotless, and other repository-defined
+ standards. Does not judge architecture, runtime ownership, caches, lifecycle,
+ or other semantic design rules.
+---
+
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+# Coding Standards
+
+Use this Skill in exactly one of the following modes. Both modes derive compliance only from written standards that actually apply to the target files.
+
+## Implementation Guidance Mode
+
+Use this mode when implementing, fixing, refactoring, or reviewing code unless the user explicitly requests a standalone coding-standards compliance audit.
+
+- Read the portions of `CODE_OF_CONDUCT.md`, repository-level and path-level `AGENTS.md`, Checkstyle, Spotless, and other written repository standards that actually apply to every affected file.
+- Determine whether each rule governs all existing code or only code that is new, modified, touched, submitted, or generated by Codex.
+- Apply every applicable rule to each modified file within the task's authorized scope.
+- Do not create rules from personal preference, general clean-code principles, or nearby code.
+- Do not scan for or report pre-existing violations outside the current task scope.
+- Run the applicable formatting and standards checks required by the repository workflow.
+- Do not run `apply`, `format`, or another file-modifying command unless the current task authority and repository rules authorize every possible write.
+- Do not build a complete file inventory or perform physical-line accounting in this mode.
+
+This mode constrains the current code task and must not expand it into a repository-wide audit.
+
+## Standalone Compliance Audit Mode
+
+Use this mode only when the user explicitly asks to audit, check, or report coding-standards compliance.
+
+- Require an exact file, package, Maven module, directory, or whole-repository scope before starting.
+- Audit the current working tree rather than a pull request or diff.
+- Remain read-only throughout the audit. Do not edit code, generate patches, run formatting, or change Git or external state.
+- Read the [standalone compliance audit rules](references/code-compliance.md) through EOF.
+- Before auditing, run `scripts/build_audit_inventory.py` with the repository root and every user-specified repository-relative scope.
+- Check every constrained file and every physical line in scope without sampling.
+- Record applicable rules, checked files, checked physical lines, excluded files, and blocked checks.
+- Use exactly one of these conclusions: `Strictly compliant`, `Non-compliant`, `Partial audit`, or `Not applicable`.
+- Report only confirmed violations of applicable written standards.
+- Do not assign severity labels or report architecture, caching, lifecycle, ownership, or other semantic issues.
+
+This Skill does not judge semantic design rules in either mode.
diff --git a/.codex/skills/coding-standards/agents/openai.yaml b/.codex/skills/coding-standards/agents/openai.yaml
new file mode 100644
index 00000000000..5c5ba29e97f
--- /dev/null
+++ b/.codex/skills/coding-standards/agents/openai.yaml
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+interface:
+ display_name: "Coding Standards"
+ short_description: "Apply repository coding standards or audit compliance"
+ default_prompt: "Use $coding-standards to apply the repository coding standards to this task or audit the following scope:"
diff --git a/.codex/skills/coding-standards/references/code-compliance.md b/.codex/skills/coding-standards/references/code-compliance.md
new file mode 100644
index 00000000000..69931b4f1ea
--- /dev/null
+++ b/.codex/skills/coding-standards/references/code-compliance.md
@@ -0,0 +1,85 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+# Strict Coding Standards Compliance
+
+## Audit Targets
+
+Audit every production, test, grammar, configuration, workflow, and other file in scope that a written coding standard explicitly covers. Do not assume that only Java files are subject to the audit.
+
+Determine the files to check from the objects, paths, and purposes explicitly governed by each rule. Applicable standards may cover Java, G4, GitHub Workflow files, configuration files, or any other file category named by a written repository rule.
+
+Discover files from their actual paths and purposes rather than only from the extensions expected for compliant files. For example, a rule requiring GitHub Workflow files to use `.yml` still requires enumerating files with `.yaml` or other extensions in the workflow directory.
+
+Exclude a file only when ignore rules, repository instructions, or generation configuration proves that it is a developer-local file, generated code, a build artifact, or externally copied content. Mark an in-scope file with no applicable written coding standard as `Not constrained by written standards`; do not count it as compliant and do not invent a rule for it.
+
+## Enumerate Applicable Rules
+
+Before checking physical lines, read `CODE_OF_CONDUCT.md`, every `AGENTS.md` that applies to the target path, Checkstyle, Spotless, and any other standard explicitly referenced for the target category.
+
+For each rule, record its source, exact requirement, constrained files, exceptions, and inspection method. Classify the rule as mandatory, prohibited, conditional, or advisory.
+
+## Determine Applicability
+
+Complete the following checks before classifying existing code as non-compliant:
+
+1. Read the exact requirement, including required actions, prohibited actions, allowed forms, exceptions, and verification methods. Do not add prohibitions based on personal preference.
+2. Determine whether the rule governs all existing code or only code that is new, modified, touched, submitted, or generated by Codex.
+3. Confirm that the current file and its state are within the rule's scope. A rule that only guides future changes cannot prove that untouched existing code is non-compliant.
+4. Check whether a repository-level or parent rule explicitly permits the current form or another compliant form. Do not use a narrower preference to override an existing permission.
+5. Do not report a compliance violation when the rule does not apply to the current file.
+
+`AGENTS.md` determines how to conduct the audit. Use an `AGENTS.md` rule as evidence against existing source only when the rule explicitly constrains existing source rather than only code created, modified, or delivered by Codex.
+
+Mandatory and prohibited rules directly determine compliance. A conditional rule establishes a violation only when its condition is satisfied and no written exception permits the current form. Development philosophy and general clean-code principles are not independently enforceable compliance rules unless another written standard states an exact requirement.
+
+When material rules conflict or their applicability cannot be determined, do not choose the stricter interpretation. Mark the affected files or checks as `Blocked`.
+
+## Inspect Every File and Physical Line
+
+Run `scripts/build_audit_inventory.py` to create the initial inventory.
+
+1. Associate every in-scope file with its applicable rules or mark it as `Not constrained by written standards`.
+2. Check every physical line of each constrained text file, including licenses, blank lines, comments, declarations, and bodies. Also check filenames, directories, member order, cross-file consistency, and other requirements that cannot be expressed by a single line.
+3. For Java, include production and test source, top-level and nested classes, interfaces, enums, records, annotation types, `package-info.java`, and `module-info.java`.
+4. Record each file as `Pending`, `Checked`, `Blocked`, or `Not constrained by written standards`, together with its checked line count and rules. Do not sample files, infer one file from another, or substitute representative files for complete coverage.
+5. Rebuild the affected inventory when target content changes during the audit. Do not use a stale inventory for the compliance conclusion.
+
+## Use Checkers and Manual Inspection
+
+Run Checkstyle, Spotless, and other repository checkers only in check mode. Do not run `apply`, `format`, or another correction mode.
+
+When a checker may modify the working tree, run it only in an isolated copy outside the working tree. The isolated copy must contain the current audited files and rule configuration, not only `HEAD` content or formatted content, and the check must not install dependencies or access the network.
+
+If a required checker cannot be run safely and no other method can determine the result, mark the affected rules as `Blocked`.
+
+A passing checker proves only the rules that the checker actually covers. Continue physical-line inspection for naming, ordering, conditional, semantic, and file-category rules that the tool does not cover. Do not replace a required checker with manual inspection when the checker determines strict formatting compliance.
+
+## Violations and Conclusions
+
+An existing-code violation of an applicable written standard is sufficient to report non-compliance. Do not require an additional semantic consequence and do not assign a severity label.
+
+For every violation, report the rule source, exact requirement, applicability conditions, exceptions, all violating locations, the way each location violates the rule, and the minimum correction. Violations with the same rule and correction may be grouped only when every violating location is listed rather than sampled.
+
+Use exactly one compliance conclusion:
+
+- `Strictly compliant`: At least one constrained file exists, every constrained file and physical line was checked, every applicable rule was verified, and no violation was found.
+- `Non-compliant`: At least one explicit violation of an applicable written standard was confirmed.
+- `Partial audit`: At least one file, physical line, applicable rule, or required verification was unchecked or blocked. Confirmed violations may also be reported, but the remainder of the scope must not be described as compliant.
+- `Not applicable`: No file in scope is constrained by an applicable written coding standard. Do not describe this result as `Strictly compliant`.
+
+Report the audit scope, conclusion, checked and total files, checked physical lines, checked and total applicable rules, files not constrained by written standards, excluded files, and blocked checks. Append the uncovered scope and missing information for a `Partial audit` result.
diff --git a/.codex/skills/coding-standards/scripts/build_audit_inventory.py b/.codex/skills/coding-standards/scripts/build_audit_inventory.py
new file mode 100644
index 00000000000..251d5da4f3f
--- /dev/null
+++ b/.codex/skills/coding-standards/scripts/build_audit_inventory.py
@@ -0,0 +1,360 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Build a deterministic, read-only scope inventory for the coding-standards skill."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import subprocess
+import sys
+import xml.etree.ElementTree as ET
+from collections import Counter
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Sequence
+
+
+IGNORED_POM_DIRECTORIES = {".agents", ".codex", ".git", "target"}
+
+
+@dataclass(frozen=True)
+class ModuleDeclaration:
+
+ path: str
+ source: str
+
+
+@dataclass(frozen=True)
+class PomData:
+
+ artifact_id: str
+ packaging: str
+ declarations: tuple[ModuleDeclaration, ...]
+ parse_error: str | None = None
+
+
+def local_name(tag: str) -> str:
+ return tag.rsplit("}", 1)[-1]
+
+
+def direct_child(element: ET.Element, name: str) -> ET.Element | None:
+ return next((each for each in element if local_name(each.tag) == name), None)
+
+
+def direct_children(element: ET.Element | None, name: str) -> list[ET.Element]:
+ return [] if element is None else [each for each in element if local_name(each.tag) == name]
+
+
+def direct_text(element: ET.Element, name: str, default: str = "") -> str:
+ child = direct_child(element, name)
+ return default if child is None or child.text is None else child.text.strip()
+
+
+def module_declarations(element: ET.Element, source: str) -> list[ModuleDeclaration]:
+ modules = direct_child(element, "modules")
+ result = []
+ for each in direct_children(modules, "module"):
+ if each.text and each.text.strip():
+ result.append(ModuleDeclaration(each.text.strip(), source))
+ return result
+
+
+def parse_pom(pom_path: Path) -> PomData:
+ try:
+ root = ET.parse(pom_path).getroot()
+ except (ET.ParseError, OSError) as ex:
+ return PomData("", "", (), str(ex))
+ declarations = module_declarations(root, "default")
+ profiles = direct_child(root, "profiles")
+ for profile in direct_children(profiles, "profile"):
+ profile_id = direct_text(profile, "id", "<unnamed>")
+ declarations.extend(module_declarations(profile, f"profile:{profile_id}"))
+ return PomData(
+ direct_text(root, "artifactId"),
+ direct_text(root, "packaging", "jar"),
+ tuple(declarations),
+ )
+
+
+def discover_poms(repo_root: Path) -> list[Path]:
+ result = []
+ for current_root, directory_names, file_names in os.walk(repo_root):
+ directory_names[:] = sorted(each for each in directory_names if each not in IGNORED_POM_DIRECTORIES)
+ if "pom.xml" in file_names:
+ result.append(Path(current_root) / "pom.xml")
+ return sorted(result, key=lambda each: relative_path(repo_root, each))
+
+
+def discover_repository_files(repo_root: Path) -> list[Path]:
+ try:
+ completed = subprocess.run(
+ ["git", "-C", str(repo_root), "ls-files", "-z", "--cached", "--others", "--exclude-standard"],
+ check=False, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
+ )
+ except OSError as ex:
+ raise ValueError("repository files cannot be listed by git") from ex
+ if 0 != completed.returncode:
+ raise ValueError("repository files cannot be listed by git")
+ paths = [repo_root / Path(os.fsdecode(each)) for each in completed.stdout.split(b"\0") if each]
+ return sorted((each for each in paths if each.is_file() or each.is_symlink()), key=lambda each: relative_path(repo_root, each))
+
+
+def discover_scope_files(target: Path, repository_files: list[Path]) -> list[Path]:
+ if target.is_file():
+ return [target]
+ return [each for each in repository_files if is_within(each, target)]
+
+
+def relative_path(repo_root: Path, path: Path) -> str:
+ relative = path.relative_to(repo_root)
+ return "." if not relative.parts else relative.as_posix()
+
+
+def module_path(repo_root: Path, pom_path: Path) -> str:
+ return relative_path(repo_root, pom_path.parent)
+
+
+def is_within(path: Path, parent: Path) -> bool:
+ try:
+ path.relative_to(parent)
+ return True
+ except ValueError:
+ return False
+
+
+def resolve_declared_pom(repo_root: Path, owner_pom: Path, declaration: ModuleDeclaration) -> tuple[Path | None, str | None]:
+ if "${" in declaration.path:
+ return None, "unresolved_module_path"
+ candidate = (owner_pom.parent / declaration.path).resolve()
+ if not is_within(candidate, repo_root):
+ return None, "outside_repository_module"
+ candidate_pom = candidate if "pom.xml" == candidate.name else candidate / "pom.xml"
+ return (candidate_pom, None) if candidate_pom.is_file() else (None, "missing_module_pom")
+
+
+def service_files(repo_root: Path, module_dir: Path, source_set: str) -> list[str]:
+ service_dir = module_dir / "src" / source_set / "resources" / "META-INF" / "services"
+ if not service_dir.is_dir():
+ return []
+ return sorted(relative_path(repo_root, each) for each in service_dir.rglob("*") if each.is_file())
+
+
+def source_set(relative: str) -> str:
+ parts = Path(relative).parts
+ if 2 <= len(parts) and parts[:2] == (".github", "workflows"):
+ return "workflow"
+ for index in range(len(parts) - 1):
+ if "src" == parts[index] and "main" == parts[index + 1]:
+ return "production"
+ if "src" == parts[index] and "test" == parts[index + 1]:
+ return "test"
+ return "repository"
+
+
+def inspect_file_content(path: Path) -> tuple[int | None, str]:
+ if path.is_symlink():
+ return None, "symlink"
+ try:
+ text = path.read_text(encoding="utf-8")
+ except UnicodeDecodeError:
+ return None, "non_utf8_or_binary"
+ except OSError:
+ return None, "unreadable"
+ return len(text.splitlines()), "utf8_text"
+
+
+def file_record(repo_root: Path, path: Path) -> dict[str, object]:
+ relative = relative_path(repo_root, path)
+ line_count, content_kind = inspect_file_content(path)
+ try:
+ size_bytes = path.lstat().st_size
+ except OSError:
+ size_bytes = None
+ return {
+ "path": relative,
+ "extension": path.suffix.lower() or None,
+ "source_set": source_set(relative),
+ "size_bytes": size_bytes,
+ "content_kind": content_kind,
+ "line_count": line_count,
+ }
+
+
+def build_graph(repo_root: Path, pom_paths: list[Path]) -> tuple[dict[str, PomData], dict[str, list[dict[str, str]]], dict[str, list[str]], list[dict[str, str]]]:
+ pom_by_path = {module_path(repo_root, each): each for each in pom_paths}
+ data_by_path = {path: parse_pom(pom) for path, pom in pom_by_path.items()}
+ children_by_path = {path: [] for path in pom_by_path}
+ parents_by_path = {path: [] for path in pom_by_path}
+ issues = []
+ for owner_path, data in data_by_path.items():
+ if data.parse_error:
+ issues.append({"type": "invalid_pom", "pom": relative_path(repo_root, pom_by_path[owner_path]), "detail": data.parse_error})
+ continue
+ for declaration in data.declarations:
+ child_pom, issue_type = resolve_declared_pom(repo_root, pom_by_path[owner_path], declaration)
+ if issue_type:
+ issues.append({"type": issue_type, "pom": relative_path(repo_root, pom_by_path[owner_path]),
+ "module": declaration.path, "source": declaration.source})
+ continue
+ child_path = module_path(repo_root, child_pom)
+ if child_path not in parents_by_path:
+ issues.append({"type": "undiscovered_module_pom", "pom": relative_path(repo_root, pom_by_path[owner_path]),
+ "module": declaration.path, "source": declaration.source})
+ continue
+ children_by_path[owner_path].append({"path": child_path, "source": declaration.source})
+ parents_by_path[child_path].append(owner_path)
+ for children in children_by_path.values():
+ children.sort(key=lambda each: (each["path"], each["source"]))
+ for parents in parents_by_path.values():
+ parents[:] = sorted(set(parents))
+ issues.sort(key=lambda each: tuple(str(each.get(key, "")) for key in ("pom", "type", "module", "source", "detail")))
+ return data_by_path, children_by_path, parents_by_path, issues
+
+
+def find_owner_module(repo_root: Path, target: Path, module_paths: set[str]) -> str:
+ current = target.parent if target.is_file() else target
+ while is_within(current, repo_root):
+ candidate = relative_path(repo_root, current)
+ if candidate in module_paths:
+ return candidate
+ if current == repo_root:
+ break
+ current = current.parent
+ raise ValueError(f"scope has no owning Maven module: {relative_path(repo_root, target)}")
+
+
+def descendants(module: str, children_by_path: dict[str, list[dict[str, str]]]) -> set[str]:
+ result = {module}
+ pending = [module]
+ while pending:
+ current = pending.pop()
+ for child in children_by_path.get(current, []):
+ child_path = child["path"]
+ if child_path not in result:
+ result.add(child_path)
+ pending.append(child_path)
+ return result
+
+
+def resolve_scope(repo_root: Path, raw_scope: str, module_paths: set[str], children_by_path: dict[str, list[dict[str, str]]]) -> tuple[dict[str, object], set[str]]:
+ scope_path = Path(raw_scope)
+ if scope_path.is_absolute() or ".." in scope_path.parts:
+ raise ValueError(f"scope must be a repository-relative path: {raw_scope}")
+ target = repo_root / scope_path
+ if not is_within(target.resolve(), repo_root):
+ raise ValueError(f"scope escapes repository: {raw_scope}")
+ if not target.exists() and not target.is_symlink():
+ raise ValueError(f"scope does not exist: {raw_scope}")
+ owner = find_owner_module(repo_root, target, module_paths)
+ owner_dir = repo_root if "." == owner else repo_root / owner
+ is_repository = target == repo_root
+ is_module = target == owner_dir
+ selected = set(module_paths) if is_repository else descendants(owner, children_by_path) if is_module else {owner}
+ kind = "repository" if is_repository else "module" if is_module else "file" if target.is_file() else "path"
+ return {
+ "path": relative_path(repo_root, target),
+ "kind": kind,
+ "owner_module": owner,
+ "selected_modules": sorted(selected),
+ }, selected
+
+
+def module_record(repo_root: Path, path: str, data: PomData, children: list[dict[str, str]], parents: list[str]) -> dict[str, object]:
+ module_dir = repo_root if "." == path else repo_root / path
+ return {
+ "path": path,
+ "pom": relative_path(repo_root, module_dir / "pom.xml"),
+ "artifact_id": data.artifact_id,
+ "packaging": data.packaging,
+ "parents": parents,
+ "children": children,
+ "is_leaf": not children,
+ "has_main_java": (module_dir / "src" / "main" / "java").is_dir(),
+ "has_test_java": (module_dir / "src" / "test" / "java").is_dir(),
+ "main_service_files": service_files(repo_root, module_dir, "main"),
+ "test_service_files": service_files(repo_root, module_dir, "test"),
+ "parse_error": data.parse_error,
+ }
+
+
+def build_inventory(repo_root: Path, raw_scopes: Sequence[str]) -> dict[str, object]:
+ resolved_root = repo_root.resolve()
+ if not (resolved_root / "pom.xml").is_file():
+ raise ValueError(f"repository root has no pom.xml: {repo_root}")
+ if not raw_scopes:
+ raise ValueError("at least one --scope is required")
+ pom_paths = discover_poms(resolved_root)
+ repository_files = discover_repository_files(resolved_root)
+ data_by_path, children_by_path, parents_by_path, issues = build_graph(resolved_root, pom_paths)
+ scope_records = []
+ selected_modules = set()
+ selected_files = set()
+ for raw_scope in raw_scopes:
+ scope_record, selected = resolve_scope(resolved_root, raw_scope, set(data_by_path), children_by_path)
+ scope_target = resolved_root if "." == scope_record["path"] else resolved_root / str(scope_record["path"])
+ scope_files = discover_scope_files(scope_target, repository_files)
+ scope_record["file_count"] = len(scope_files)
+ scope_records.append(scope_record)
+ selected_modules.update(selected)
+ selected_files.update(scope_files)
+ selected_issues = [each for each in issues if module_path(resolved_root, resolved_root / each["pom"]) in selected_modules]
+ modules = [module_record(resolved_root, each, data_by_path[each], children_by_path[each], parents_by_path[each])
+ for each in sorted(selected_modules)]
+ files = [file_record(resolved_root, each) for each in sorted(selected_files, key=lambda each: relative_path(resolved_root, each))]
+ extension_counts = Counter(each["extension"] or "<none>" for each in files)
+ unreferenced_poms = [each for each in sorted(selected_modules) if "." != each and not parents_by_path[each]]
+ return {
+ "version": 2,
+ "scopes": scope_records,
+ "discovered_module_count": len(data_by_path),
+ "selected_module_count": len(modules),
+ "leaf_module_count": sum(1 for each in modules if each["is_leaf"]),
+ "file_count": len(files),
+ "text_file_count": sum(1 for each in files if each["line_count"] is not None),
+ "text_line_count": sum(int(each["line_count"]) for each in files if each["line_count"] is not None),
+ "file_type_counts": dict(sorted(extension_counts.items())),
+ "unreferenced_poms": unreferenced_poms,
+ "issues": selected_issues,
+ "modules": modules,
+ "files": files,
+ }
+
+
+def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description="Build a deterministic, read-only coding-standards scope inventory.")
+ parser.add_argument("--repo-root", default=".", help="Repository root containing pom.xml")
+ parser.add_argument("--scope", action="append", required=True, help="Repository-relative file, package directory, or Maven module path")
+ return parser.parse_args(argv)
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+ args = parse_args(argv)
+ try:
+ inventory = build_inventory(Path(args.repo_root), args.scope)
+ except (OSError, ValueError) as ex:
+ print(f"error: {ex}", file=sys.stderr)
+ return 1
+ print(json.dumps(inventory, ensure_ascii=False, indent=2, sort_keys=True))
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/.codex/skills/coding-standards/scripts/tests/test_build_audit_inventory.py b/.codex/skills/coding-standards/scripts/tests/test_build_audit_inventory.py
new file mode 100644
index 00000000000..a373aaf3835
--- /dev/null
+++ b/.codex/skills/coding-standards/scripts/tests/test_build_audit_inventory.py
@@ -0,0 +1,337 @@
+#!/usr/bin/env python3
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import json
+import subprocess
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+from unittest.mock import Mock, patch
+
+
+SCRIPT_DIR = Path(__file__).resolve().parents[1]
+if str(SCRIPT_DIR) not in sys.path:
+ sys.path.insert(0, str(SCRIPT_DIR))
+
+import build_audit_inventory as inventory_builder # noqa: E402
+
+
+class BuildAuditInventoryTest(unittest.TestCase):
+
+ def test_repository_file_discovery_uses_git_inventory_when_available(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.write_text(repo / "tracked.java", "class Tracked {}\n")
+ self.write_text(repo / "ignored.java", "class Ignored {}\n")
+ completed = Mock(returncode=0, stdout=b"tracked.java\0")
+
+ with patch.object(inventory_builder.subprocess, "run", return_value=completed):
+ actual = inventory_builder.discover_repository_files(repo)
+
+ self.assertEqual([repo / "tracked.java"], actual)
+
+ def test_repository_file_discovery_fails_when_git_command_fails(self) -> None:
+ completed = Mock(returncode=128, stdout=b"")
+
+ with patch.object(inventory_builder.subprocess, "run", return_value=completed):
+ with self.assertRaisesRegex(ValueError, "cannot be listed by git"):
+ inventory_builder.discover_repository_files(Path("repo"))
+
+ def test_repository_file_discovery_fails_when_git_is_unavailable(self) -> None:
+ with patch.object(inventory_builder.subprocess, "run", side_effect=OSError):
+ with self.assertRaisesRegex(ValueError, "cannot be listed by git"):
+ inventory_builder.discover_repository_files(Path("repo"))
+
+ def test_build_repository_inventory(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root", modules=("module-a",), profile_modules={"extra": ("module-b",)})
+ self.write_pom(repo / "module-a", "module-a")
+ self.write_pom(repo / "module-b", "module-b")
+ self.write_pom(repo / "orphan", "orphan")
+ service_file = repo / "module-a" / "src" / "main" / "resources" / "META-INF" / "services" / "example.Service"
+ service_file.parent.mkdir(parents=True)
+ service_file.write_text("example.Implementation\n", encoding="utf-8")
+ (repo / "module-b" / "src" / "test" / "java").mkdir(parents=True)
+
+ actual = inventory_builder.build_inventory(repo, (".",))
+
+ self.assertEqual(4, actual["discovered_module_count"])
+ self.assertEqual(4, actual["selected_module_count"])
+ self.assertEqual(3, actual["leaf_module_count"])
+ self.assertEqual(["orphan"], actual["unreferenced_poms"])
+ modules = {each["path"]: each for each in actual["modules"]}
+ self.assertEqual(
+ [{"path": "module-a", "source": "default"}, {"path": "module-b", "source": "profile:extra"}],
+ modules["."]["children"],
+ )
+ self.assertEqual(
+ ["module-a/src/main/resources/META-INF/services/example.Service"],
+ modules["module-a"]["main_service_files"],
+ )
+ self.assertTrue(modules["module-b"]["has_test_java"])
+
+ def test_list_all_file_types_and_source_sets(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root", modules=("module-a",))
+ self.write_pom(repo / "module-a", "module-a")
+ self.write_text(repo / "module-a" / "src" / "main" / "java" / "Foo.java", "class Foo {\n}\n")
+ self.write_text(repo / "module-a" / "src" / "test" / "java" / "FooTest.java", "class FooTest {}\n")
+ self.write_text(repo / "module-a" / "src" / "main" / "antlr4" / "Foo.g4", "grammar Foo;\n")
+ self.write_text(repo / ".github" / "workflows" / "check.yaml", "name: Check\n")
+ self.write_text(repo / "config" / "example_config.xml", "<config/>\n")
+ binary_file = repo / "module-a" / "src" / "main" / "resources" / "image.bin"
+ binary_file.parent.mkdir(parents=True)
+ binary_file.write_bytes(b"\xff\x00")
+ symlink = repo / "module-a" / "src" / "main" / "resources" / "image-link.bin"
+ symlink.symlink_to(binary_file)
+ self.write_text(repo / ".gitignore", "target/\n")
+ self.write_text(repo / "module-a" / "target" / "Generated.java", "class Generated {}\n")
+
+ actual = inventory_builder.build_inventory(repo, (".",))
+
+ files = {each["path"]: each for each in actual["files"]}
+ self.assertEqual(2, actual["version"])
+ self.assertEqual("production", files["module-a/src/main/java/Foo.java"]["source_set"])
+ self.assertEqual(2, files["module-a/src/main/java/Foo.java"]["line_count"])
+ self.assertEqual("test", files["module-a/src/test/java/FooTest.java"]["source_set"])
+ self.assertEqual("production", files["module-a/src/main/antlr4/Foo.g4"]["source_set"])
+ self.assertEqual("workflow", files[".github/workflows/check.yaml"]["source_set"])
+ self.assertEqual(".yaml", files[".github/workflows/check.yaml"]["extension"])
+ self.assertEqual("repository", files["config/example_config.xml"]["source_set"])
+ self.assertIsNone(files["module-a/src/main/resources/image.bin"]["line_count"])
+ self.assertEqual("non_utf8_or_binary", files["module-a/src/main/resources/image.bin"]["content_kind"])
+ self.assertIsNone(files["module-a/src/main/resources/image-link.bin"]["line_count"])
+ self.assertEqual("symlink", files["module-a/src/main/resources/image-link.bin"]["content_kind"])
+ self.assertNotIn("module-a/target/Generated.java", files)
+ self.assertEqual(len(files), actual["file_count"])
+ self.assertEqual(len(files) - 2, actual["text_file_count"])
+
+ def test_file_scope_keeps_exact_owner_module(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root", modules=("module-a",))
+ self.write_pom(repo / "module-a", "module-a", modules=("nested",))
+ self.write_pom(repo / "module-a" / "nested", "nested")
+ source_file = repo / "module-a" / "src" / "main" / "java" / "example" / "Foo.java"
+ source_file.parent.mkdir(parents=True)
+ source_file.write_text("class Foo {}\n", encoding="utf-8")
+
+ actual = inventory_builder.build_inventory(repo, ("module-a/src/main/java/example/Foo.java",))
+
+ self.assertEqual(1, actual["selected_module_count"])
+ self.assertEqual(["module-a"], [each["path"] for each in actual["modules"]])
+ self.assertEqual("file", actual["scopes"][0]["kind"])
+ self.assertEqual("module-a", actual["scopes"][0]["owner_module"])
+ self.assertEqual(["module-a/src/main/java/example/Foo.java"], [each["path"] for each in actual["files"]])
+
+ def test_symlink_file_scope_keeps_exact_path_and_owner_module(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root", modules=("module-a", "module-b"))
+ self.write_pom(repo / "module-a", "module-a")
+ self.write_pom(repo / "module-b", "module-b")
+ target = repo / "module-b" / "actual.txt"
+ self.write_text(target, "actual\n")
+ symlink = repo / "module-a" / "alias.txt"
+ symlink.symlink_to(target)
+
+ actual = inventory_builder.build_inventory(repo, ("module-a/alias.txt",))
+
+ self.assertEqual("module-a/alias.txt", actual["scopes"][0]["path"])
+ self.assertEqual("module-a", actual["scopes"][0]["owner_module"])
+ self.assertEqual(["module-a"], [each["path"] for each in actual["modules"]])
+ self.assertEqual("module-a/alias.txt", actual["files"][0]["path"])
+ self.assertEqual("symlink", actual["files"][0]["content_kind"])
+ self.assertIsNone(actual["files"][0]["line_count"])
+
+ def test_reject_symlink_scope_outside_repository(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ root = Path(temp_dir)
+ repo = root / "repo"
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root")
+ target = root / "outside.txt"
+ self.write_text(target, "outside\n")
+ (repo / "alias.txt").symlink_to(target)
+
+ with self.assertRaisesRegex(ValueError, "scope escapes repository"):
+ inventory_builder.build_inventory(repo, ("alias.txt",))
+
+ def test_path_scope_lists_only_files_below_exact_target(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root", modules=("module-a",))
+ self.write_pom(repo / "module-a", "module-a")
+ self.write_text(repo / "module-a" / "src" / "main" / "java" / "selected" / "Foo.java", "class Foo {}\n")
+ self.write_text(repo / "module-a" / "src" / "main" / "java" / "sibling" / "Bar.java", "class Bar {}\n")
+
+ actual = inventory_builder.build_inventory(repo, ("module-a/src/main/java/selected",))
+
+ self.assertEqual("path", actual["scopes"][0]["kind"])
+ self.assertEqual(["module-a/src/main/java/selected/Foo.java"], [each["path"] for each in actual["files"]])
+
+ def test_pom_file_scope_keeps_exact_owner_module(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root", modules=("module-a",))
+ self.write_pom(repo / "module-a", "module-a", modules=("nested",))
+ self.write_pom(repo / "module-a" / "nested", "nested")
+
+ actual = inventory_builder.build_inventory(repo, ("module-a/pom.xml",))
+
+ self.assertEqual(1, actual["selected_module_count"])
+ self.assertEqual(["module-a"], [each["path"] for each in actual["modules"]])
+ self.assertEqual("file", actual["scopes"][0]["kind"])
+ self.assertEqual("module-a", actual["scopes"][0]["owner_module"])
+
+ def test_module_scope_includes_declared_descendants(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root", modules=("module-a",))
+ self.write_pom(repo / "module-a", "module-a", modules=("nested",))
+ self.write_pom(repo / "module-a" / "nested", "nested")
+
+ actual = inventory_builder.build_inventory(repo, ("module-a",))
+
+ self.assertEqual(["module-a", "module-a/nested"], [each["path"] for each in actual["modules"]])
+ self.assertEqual("module", actual["scopes"][0]["kind"])
+
+ def test_multiple_scopes_select_module_union(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root", modules=("module-a", "module-b"))
+ self.write_pom(repo / "module-a", "module-a")
+ self.write_pom(repo / "module-b", "module-b")
+
+ actual = inventory_builder.build_inventory(repo, ("module-b", "module-a"))
+
+ self.assertEqual(["module-a", "module-b"], [each["path"] for each in actual["modules"]])
+ self.assertEqual(2, len(actual["scopes"]))
+
+ def test_report_invalid_module_declarations(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root", modules=("${module.name}", "missing", "../outside"))
+
+ actual = inventory_builder.build_inventory(repo, (".",))
+
+ self.assertEqual(
+ ["missing_module_pom", "outside_repository_module", "unresolved_module_path"],
+ sorted(each["type"] for each in actual["issues"]),
+ )
+
+ def test_report_invalid_pom(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root", modules=("broken",))
+ broken_pom = repo / "broken" / "pom.xml"
+ broken_pom.parent.mkdir(parents=True)
+ broken_pom.write_text("<project>", encoding="utf-8")
+
+ actual = inventory_builder.build_inventory(repo, (".",))
+
+ self.assertEqual("invalid_pom", actual["issues"][0]["type"])
+ modules = {each["path"]: each for each in actual["modules"]}
+ self.assertIsNotNone(modules["broken"]["parse_error"])
+
+ def test_reject_invalid_scope(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root")
+
+ with self.assertRaisesRegex(ValueError, "repository-relative"):
+ inventory_builder.build_inventory(repo, (str(repo),))
+ with self.assertRaisesRegex(ValueError, "repository-relative"):
+ inventory_builder.build_inventory(repo, ("../outside",))
+ with self.assertRaisesRegex(ValueError, "scope does not exist"):
+ inventory_builder.build_inventory(repo, ("missing",))
+
+ def test_inventory_is_deterministic(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root", modules=("b", "a"))
+ self.write_pom(repo / "a", "a")
+ self.write_pom(repo / "b", "b")
+
+ first = inventory_builder.build_inventory(repo, (".",))
+ second = inventory_builder.build_inventory(repo, (".",))
+
+ self.assertEqual(json.dumps(first, sort_keys=True), json.dumps(second, sort_keys=True))
+ self.assertEqual([".", "a", "b"], [each["path"] for each in first["modules"]])
+
+ def test_ignore_agent_codex_and_target_poms_but_list_their_files(self) -> None:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ repo = Path(temp_dir)
+ self.initialize_repository(repo)
+ self.write_pom(repo, "root")
+ self.write_pom(repo / ".agents" / "helper", "helper")
+ self.write_pom(repo / ".codex" / "helper", "helper")
+ self.write_text(repo / ".gitignore", "target/\n")
+ self.write_pom(repo / "module" / "target" / "fixture", "fixture")
+
+ actual = inventory_builder.build_inventory(repo, (".",))
+
+ self.assertEqual(["."], [each["path"] for each in actual["modules"]])
+ self.assertEqual(
+ [".agents/helper/pom.xml", ".codex/helper/pom.xml", ".gitignore", "pom.xml"],
+ [each["path"] for each in actual["files"]],
+ )
+
+ @staticmethod
+ def initialize_repository(directory: Path) -> None:
+ subprocess.run(["git", "init", "--quiet", str(directory)], check=True)
+
+ @staticmethod
+ def write_text(path: Path, content: str) -> None:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(content, encoding="utf-8")
+
+ @staticmethod
+ def write_pom(directory: Path, artifact_id: str, modules: tuple[str, ...] = (),
+ profile_modules: dict[str, tuple[str, ...]] | None = None) -> None:
+ directory.mkdir(parents=True, exist_ok=True)
+ module_xml = "".join(f"<module>{each}</module>" for each in modules)
+ default_modules = f"<modules>{module_xml}</modules>" if modules else ""
+ profile_xml = ""
+ for profile_id, paths in (profile_modules or {}).items():
+ declared_modules = "".join(f"<module>{each}</module>" for each in paths)
+ profile_xml += f"<profile><id>{profile_id}</id><modules>{declared_modules}</modules></profile>"
+ profiles = f"<profiles>{profile_xml}</profiles>" if profile_xml else ""
+ (directory / "pom.xml").write_text(
+ f"<project><modelVersion>4.0.0</modelVersion><artifactId>{artifact_id}</artifactId>{default_modules}{profiles}</project>",
+ encoding="utf-8",
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()