Library of tooling for various purposes.
- Claude Code files Maven enforcer
- Code generation
- gRPC example
- Context engineering
- Data
- Claude Code adoption
- Architecture tests (ArchUnit)
- Integration tests
- Building
- Releasing
- License
The claude-code-enforcer module is a set of custom
maven-enforcer-plugin
rules that fail the build when the repository's Claude Code files are
missing or malformed, keeping CLAUDE.md, AGENTS.md, .claude/settings.json,
the sub-agents under .claude/agents, and the skills under .claude/skills
consistent and in their expected shape:
claudeMdFormat(ClaudeMdFormatRule) — checks thatCLAUDE.mdexists and is non-empty, starts with the# CLAUDE.mdtitle (a leading UTF-8 BOM is tolerated), referencesAGENTS.md, and contains every required section heading.agentsMdFormat(AgentsMdFormatRule) — applies the same structural checks toAGENTS.md: it must start with the# AGENTS.mdtitle and contain every required section heading.skillFilesExist(SkillFilesExistRule) — checks that every skill directory under.claude/skillscontains a non-emptySKILL.mdthat opens with a YAML front matter block declaring every required key (name,descriptionby default). Thenamemust follow the Claude Code naming convention (lower-case kebab-case, bounded length) and match the skill's directory name; thedescriptionmust be non-empty and withinmaxDescriptionLength. An optionalallowedFrontMatterKeyswhitelist catches typos such asdescripton.subAgentFormat(SubAgentFormatRule) — treats every*.mdfile in the configured agents directory as a sub-agent: it must be non-empty, open with a YAML front matter block declaring every required key, and carry anamethat follows the naming convention and matches its file name. An optionalallowedModelswhitelist rejects a mistypedmodelsuch asclaud-opus.commandFormat(CommandFormatRule) — treats every*.mdfile in the configured commands directory (e.g..claude/commands) as a custom slash command: it must be non-empty and carry a file name that follows the Claude Code naming convention, because the command's name comes from its file name. Front matter is optional, but when present adescriptionmust be non-empty, amodelmust be one ofallowedModelswhen that whitelist is configured, and an optionalallowedFrontMatterKeyswhitelist catches typos such asargument-hnt.settingsJsonValid(SettingsJsonValidRule) — checks that.claude/settings.jsonexists, is non-empty, and parses as JSON. It can also assert policy onpermissions.allow:requiredPermissionsmust all be present andforbiddenPermissionsmust all be absent, so a project can mandate a permission it relies on or ban an over-broad wildcard such asBash(*).hookCommandsValid(HookCommandsValidRule) — validates thehookssection of.claude/settings.json: every event must map to an array of groups, each group must carry ahooksarray, and every hook must declare a non-blanktype(acommandhook also a non-blankcommand). A command that points at a project-local script — through$CLAUDE_PROJECT_DIR, or as the plain repository-relative path Claude Code resolves the same way — is resolved againstprojectDirand must exist on disk, so a renamed or missing hook script is caught; only the program of each chained command is read as a relative script, so an argument that looks like a path is not required to exist. An optionalallowedEventswhitelist rejects a mistyped event such asSessionSart, andvalidateScriptReferencescan switch the script check off.mcpServersValid(McpServersValidRule) — validates the project's.mcp.json. A project-level MCP file is optional, so an absent file passes; when present it must be non-empty and parse as JSON, and every entry undermcpServersmust be a JSON object with a well-formed transport. Astdioserver (the default when notypeis declared) needs a non-blankcommand; ansseorhttpserver needs a non-blankurl. An explicittypeoutside theallowedTypeswhitelist (stdio,sse,httpby default) is reported, catching a mistypedhtttp.requiredServersmust all be present andforbiddenServersmust all be absent, so a project can mandate an MCP server it relies on or ban one it does not want committed.mcpConfigFormat(McpConfigFormatRule) — validates the details of each.mcp.jsonserver entry thatmcpServersValidleaves unchecked:argsmust be an array of strings,envandheadersmust be objects whose values are all strings, aurlmust be a syntactically validhttp/httpsURL (andhttpsonly whenrequireHttpsis set), and a server must not mix transports by declaring both acommandand aurl. LikemcpServersValidit treats an absent file as a pass.okfBundleFormat(OkfBundleFormatRule) — holds a bundle in Google's Open Knowledge Format atbundleDirto the specification's own conformance conditions: every non-reserved.mdfile carries a parseable YAML front matter block, every block declares a non-emptytype(the one mandatory field), and the reserved names keep their structure — anindex.mdcarries no front matter beyond anokf_versionat the bundle root, and alog.mdgroups its entries under ISO 8601YYYY-MM-DDheadings. Where the format defines a closed vocabulary the value is checked too: astatusmust bedraft,stableordeprecated, astale_aftermust be a real calendar date, and ageneratedmapping must name the actor that produced the concept. Where the format leaves things open, nothing is imposed —typevalues are not registered centrally, so an unknown one is not a violation. Beyond that,requiredKeysadds front matter keys every concept must declare,okfVersionpins the version the bundle root declares, andrequireIndexdemands a listing in every directory holding concepts (off by default, since a consumer must tolerate a missingindex.md). A bundle is optional, so an absentbundleDiris a pass and the rule starts enforcing the day a bundle is committed.hooksFormat(HooksFormatRule) — validates the hook scripts under a configuredhooksDir(e.g..claude/hooks): every regular file must be non-empty, start with a#!shebang (requireShebang), and carry the executable bit (requireExecutable), and an optionalallowedExtensionswhitelist rejects a stray file. WherehookCommandsValidvalidates the JSON shape of thehookssection, this rule validates the scripts themselves; when asettingsFileis configured it also cross-checks the wiring, so a command hook whose project-local path —$CLAUDE_PROJECT_DIR-rooted or plain repository-relative — lands in the hooks directory must point at a script that exists there, andreportUnreferencedScriptsflags a script no hook references. An absenthooksDiris a pass because hooks are optional.uniqueDescriptions(UniqueDescriptionsRule) — reads thedescriptionfrom the front matter of every sub-agent (*.md), command (*.md), and skill (SKILL.md) in the configuredcommandsDir,agentsDir, andskillsDir, and fails when one description is used by more than one definition, naming every file that uses it. Because Claude routes by matching intent against these descriptions, two identical descriptions are ambiguous and one shadows the other. Comparison ignores case and runs of whitespace; missing or blank descriptions are left to the format rules. As withuniqueNames, at least one directory must be configured and uniqueness is checked across all of them.uniqueNames(UniqueNamesRule) — gathers the names of every command, sub-agent, and skill from the configuredcommandsDir,agentsDir, andskillsDir(a command's and a sub-agent's name is its*.mdfile name, a skill's name is its directory name) and fails when one name is used more than once, naming every file or directory that uses it. At least one directory must be configured, and any directory that is configured must exist. Uniqueness is checked across all configured directories at once, so a command that clashes with a skill is caught just like two commands that clash.crossDocConsistency(CrossDocConsistencyRule) — keepsCLAUDE.mdandAGENTS.mdfrom contradicting each other. Each configuredconsistentPatternis a regular expression with one capturing group; the captured value must agree between the two files (or be absent from both). For exampleJava (\d+)fails the build if one file saysJava 25and the otherJava 24.readmeConsistency(ReadmeConsistencyRule) — keeps thisREADME.mdfrom drifting away from the agent docs (AGENTS.md, the single source of truth). Each configuredconsistentPattern(one capturing group) must capture the same value in both files, so a documented capability or version cannot silently disagree with the agent docs. UnlikecrossDocConsistency, a fact the README simply does not repeat is ignored — the README is a curated, example-heavy view and may document a subset — so only a value present in both files that disagrees fails the build.
The claudeMdFormat and agentsMdFormat rules share a MarkdownFormatRule
base class that performs the file-existence, BOM, title, and section checks. It
also exposes optional checks, each disabled by default: forbiddenTokens that
must not appear outside code fences, enforceSectionOrder to require the
sections in the configured order, a maxLineLength cap, and
validateFileReferences to confirm that Markdown links to local files resolve
to something on disk.
Every rule extends a common ClaudeCodeEnforcerRule base that reports all
violations together and honours a severity option: the default error fails
the build, while <severity>warn</severity> downgrades the same violations to a
logged warning so a team can adopt a rule gradually.
An optional <reportFile> writes the same outcome as a self-contained HTML
report — a single table pairing what failed and why (the header plus one entry
per violation) with the per-rule "How to fix" steps — so a build can surface the
violations in a browser or as a CI artifact. The page is inlined (styles
included, no external assets) so it opens anywhere, and it is written on pass and
fail alike, so a configured report file always reflects the latest run rather
than leaving a stale failure behind:
<reportFile>${project.build.directory}/claude-code-enforcer.html</reportFile>The front matter rules (skillFilesExist, subAgentFormat, commandFormat)
also accept an autoFix option. When it is enabled and a definition's front
matter is malformed in a way that is safe to repair — a delimiter written with
too many dashes such as ----, or an opening --- whose closing delimiter is
missing — the rule rewrites the file in place and continues against the
corrected content instead of failing the build. The repair is conservative: it
only acts when the document opens with a dashes line enclosing real
key: value entries, so a lone --- thematic break is never mistaken for front
matter. autoFix is off by default.
The rules are wired into the root pom.xml and run at the repository root only.
The check is opt-in via the enforceClaudeMd property, so ordinary builds
are unaffected:
mvn -pl claude-code-enforcer -am install # install the rule jar once
mvn package -DenforceClaudeMd # build with the checks enabled
Problem:
Generated builder java code for protobuffers detects missing required fields in runtime.
Solution:
Move detection to compile time (shift-left). For a visual walkthrough of how
the generated interface chain guarantees every required field is set before
build() can be called, see
docs/compile-time-safe-builders.md.
Example of the problem:
syntax = "proto2";
package example;
option java_multiple_files = true;
option java_package = "io.github.adamw7.tools.code.protos";
message Person {
optional string name = 1;
required int32 id = 2;
optional string email = 3;
required string department = 4;
}and the builder that allows building the object without setting the required field "Id":
Person.Builder personBuilder = Person.newBuilder();
personBuilder.setEmail("email@sth.com");
personBuilder.setName("Adam");
UninitializedMessageException thrown = assertThrows(UninitializedMessageException.class, personBuilder::build, "Expected build method to throw, but it didn't");
assertEquals("Message missing required fields: id, department", thrown.getMessage());Solution:
<plugin>
<groupId>io.github.adamw7</groupId>
<artifactId>protogen-maven-plugin</artifactId>
<!-- Use the latest release: https://github.com/adamw7/tools/releases/latest -->
<version>2.5.0</version>
<configuration>
<generatedSourcesDir>${project.basedir}/target/generated-sources/</generatedSourcesDir>
<pkgs>
<param>io.github.adamw7.tools.code.protos</param>
</pkgs>
<outputpackage>io.github.adamw7.tools.code.builders</outputpackage>
</configuration>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>code-generator</goal>
</goals>
</execution>
</executions>
</plugin>that generates builders detecting missing required fields in compile time (some methods are excluded for simplicity of the example):
interface OptionalIfc {
OptionalIfc setEmail(String email);
OptionalIfc setName(String name);
Person build();
}
interface DepartmentIfc {
OptionalIfc setDepartment(String department);
}
interface IdIfc {
DepartmentIfc setId(int id);
}
class OptionalImpl implements OptionalIfc {
private final Builder builder;
public OptionalImpl(Builder builder) {
this.builder = builder;
}
@Override
public OptionalIfc setEmail(String email) {
builder.setEmail(email);
return this;
}
@Override
public OptionalIfc setName(String name) {
builder.setName(name);
return this;
}
@Override
public Person build() {
return builder.build();
}
}
class DepartmentImpl implements DepartmentIfc {
private final Builder personOrBuilder;
public DepartmentImpl(Builder personOrBuilder) {
this.personOrBuilder = personOrBuilder;
}
@Override
public OptionalIfc setDepartment(String department) {
personOrBuilder.setDepartment(department);
return new OptionalImpl(personOrBuilder);
}
}
public class ExampleTest {
private static class PersonBuilderExample implements IdIfc {
private final Builder personBuilder = Person.newBuilder();
@Override
public DepartmentIfc setId(int id) {
personBuilder.setId(id);
return new DepartmentImpl(personBuilder);
}
}
@Test
public void happyPath() {
PersonBuilderExample builder = new PersonBuilderExample();
Person person = builder.setId(1).setDepartment("dep").setEmail("sth@sth.net").setName("Adam").build();
assertEquals(1, person.getId());
assertEquals("dep", person.getDepartment());
assertEquals("sth@sth.net", person.getEmail());
assertEquals("Adam", person.getName());
}
}Both proto2 and proto3 are supported. In proto2 the generated builder enforces
that every required field is set before build() can be called. proto3 has no
required fields, so there is nothing to enforce there; the builder simply
exposes all fields as optional. Presence-tracking is handled correctly for each
syntax: a hasXxx() accessor is generated only for fields that actually track
presence — every singular field in proto2, but in proto3 only message fields and
those declared with the explicit optional keyword (implicit-presence proto3
scalars, which have no hasXxx(), are left alone).
A oneof group additionally gets a getXxxCase() accessor returning protobuf's
generated XxxCase enum, so you can tell which member is set, plus a
clearXxx() that resets the whole group — both reachable through the fluent
builder chain. The synthetic oneofs that back proto3 optional fields are not
treated as groups, so no spurious case accessor is generated for them.
An end-to-end gRPC example combining standard protobuf/gRPC code generation with the compile-time-safe builder generation from this project.
Given greeter.proto:
syntax = "proto2";
package greeter;
option java_multiple_files = true;
option java_package = "io.github.adamw7.tools.grpc.proto";
message Address {
required string city = 1;
optional string country = 2;
}
message Person {
required string name = 1;
optional string title = 2;
optional Address address = 3;
}
message HelloRequest {
required Person person = 1;
}
message HelloReply {
required string message = 1;
}
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}Two generators run during the build:
protobuf-maven-plugincompiles the proto definitions into protobuf message classes and gRPC service stubs (GreeterGrpc).protogen-maven-plugin(this repo) generates compile-time-safe builders (AddressBuilder,PersonBuilder,HelloRequestBuilder,HelloReplyBuilder) that refuse to callbuild()until everyrequiredfield is set.
The messages are composed — a HelloRequest points to a Person, which points to
an Address — so the generated builders compose the same way, assembling a request
bottom-up through a three-level chain:
Address address = new AddressBuilder().setCity("London").setCountry("UK").build();
Person person = new PersonBuilder().setName("Smith").setTitle("Dr.").setAddress(address).build();
HelloRequest request = new HelloRequestBuilder().setPerson(person).build();The service implementation uses the generated builder:
HelloReply reply = new HelloReplyBuilder().setMessage(greetingFor(request)).build();Note: All example code (
GreeterServiceImpl,GreeterServer,GreeterClient) lives undersrc/test/javabecause the protogen-generated builders are written totarget/generated-test-sources. Run the example withmvn -pl grpc-example -am test.
See the grpc-example module for full details and how to run the example.
For gen ai agents that work with Java code the context usually starts with one class but may get wider and be extended to the classes used by it etc so on. In order to build this tree there is a very simple and fast regex based interface:
public interface Context {
Set<ClassContainer> find(ClassContainer root, int depth);
}where ClassContainer contains the path of the class and its sources.
The depth param tells the finder how deep we want to go in the tree of usages of the class.
Of course when the depth is growing the tree grows very fast.
The regex-based Finder is the default implementation. Pick the language with
the Language enum (Language.JAVA is the default):
Context context = new Finder(allContainers, Language.JAVA);
Set<ClassContainer> used = context.find(root, depth);Language.JAVA resolves .java files when mapping a referenced class name back
to its source file, so the usage-tree building works out of the box for Java
sources.
Kotlin is supported with exactly the same features as Java. The regex-based
Finder and the Context interface are language agnostic; the only difference
is the source-file extension used to resolve a referenced class back to a file.
Pick the language with the Language enum:
Context context = new Finder(allContainers, Language.KOTLIN);
Set<ClassContainer> used = context.find(root, depth);Language.JAVA (the default) resolves .java files and Language.KOTLIN
resolves .kt files, so the same usage-tree building works for Kotlin sources.
Scala is supported with exactly the same features as Java and Kotlin. The
regex-based Finder and the Context interface are language agnostic; the only
difference is the source-file extension used to resolve a referenced class back
to a file. Pick the language with the Language enum:
Context context = new Finder(allContainers, Language.SCALA);
Set<ClassContainer> used = context.find(root, depth);Language.SCALA resolves .scala files, so the same usage-tree building works
for Scala sources.
A single class is rarely enough context — an agent usually needs to see how a
whole project is laid out and how its files relate. ProjectTreeBuilder walks a
Java (or Kotlin or Scala) project directory and produces a tree of its folders, files
and dependencies in one structure:
ProjectTreeNode root = new ProjectTreeBuilder(/* depth */ 1).build(Path.of("my-project"));
System.out.println(new ProjectTreePrinter().print(root));For a Kotlin project just pass the language; everything else stays the same:
ProjectTreeNode root = new ProjectTreeBuilder(Language.KOTLIN, /* depth */ 1).build(Path.of("my-project"));
System.out.println(new ProjectTreePrinter().print(root));The building blocks are:
ProjectTreeNode— a node in the tree. Each node is either a directory (mirroring a project folder and holding child nodes) or a file. File nodes additionally carry the set of project classes they depend on, so folders, files and dependencies are all described by the same tree.ProjectTreeBuilder— scans the project directory recursively. Every folder becomes a directory node and every file a file node (directories are listed before files, then alphabetically). For each source file (.java,.ktor.scala, depending on the configuredLanguage) it resolves the project classes it uses with the same regex-basedContext, skipping the file's own class.depthcontrols how deep the usage search goes, exactly as in theContextinterface above.ContextFactory— decouples the builder from the concrete dependency finder (defaulting toFinder), so a different resolution strategy can be plugged in without changing the builder.ProjectTreePrinter— renders the tree as indented text, with each file's dependencies listed beneath it:
[dir] pkg
[file] A.java
[file] B.java
-> A.java
The result is a compact, human- and LLM-readable view of the project, ready to be handed to a gen-AI agent as context.
The tree can be rendered in several formats behind a single
ProjectTreeSerializer interface, so a consumer depends on the abstraction
rather than a concrete format and new formats can be added without touching the
tree:
ProjectTreeSerializer serializer = new ProjectTreeMarkdownSerializer(); // or JSON / printer
String rendered = serializer.serialize(root);ProjectTreePrinter— indented plain text (shown above).ProjectTreeMarkdownSerializer— a nested Markdown bullet list, with each file's dependencies as indented child bullets. Well suited to documents and chat-based agents.ProjectTreeJsonSerializer— structured JSON (name,type,dependencies,children) for programmatic consumers;serializePrettyproduces indented JSON.ProjectTreeDotSerializer— a Graphviz DOT digraph of the dependency edges (file → depended-on class); directories add structure but are not drawn. Render it with Graphviz tooling.ProjectTreeMermaidSerializer— the same dependency edges as a Mermaidflowchart, which renders inline on GitHub, in Markdown viewers and in many gen-AI agent surfaces without any external tooling.
The formats above render the tree as one document. Google's
Open Knowledge Format
(OKF) takes the other shape: a knowledge bundle is a directory of markdown
files with YAML frontmatter, so an agent can navigate it file by file instead
of swallowing the whole project at once, and no proprietary runtime is needed to
read it — if you can cat a file, you can read OKF.
OkfBundler maps a scanned tree onto that format. Every directory becomes the
reserved index.md listing what it holds, and every file becomes a concept
document:
ProjectTreeNode tree = new ProjectTreeBuilder(Language.JAVA, 1).build(root);
OkfBundle bundle = new OkfBundler(Language.JAVA).bundle(tree);
new OkfBundleWriter().write(bundle, Path.of("target/okf"));target/okf
├── index.md <- okf_version: "0.2", then the listing
└── pkg
├── index.md
├── A.java.md
└── B.java.md
A concept document names the file in frontmatter and links to what it depends on:
---
type: "Java Source File"
title: "B.java"
description: "Java source file with 1 project dependency."
resource: "pkg/B.java"
tags: ["source", "java"]
generated: { by: "tools.code.context/1", at: "2026-08-03T10:15:30Z" }
---
# Dependencies
* [`A.java`](/pkg/A.java.md)OkfBundler— maps aProjectTreeNodetree onto bundle paths and documents. A dependency is linked only when its file name identifies exactly one concept in the bundle; an unresolved or ambiguous name stays plain code rather than becoming a link to a guess. Links use the bundle-relative (/-prefixed) form the specification recommends, which survives a document being moved.OkfConcept— one concept's facts (type, title, description, resource, tags, dependencies) before they are rendered, so the same description serves both the concept's frontmatter and theindex.mdentry linking to it.OkfFrontmatter— renders the YAML block. OKF makes exactly one field mandatory,type; the rest are recommended, and v0.2 records production asgenerated: { by, at }with the<producer>/<version>actor convention.OkfBundleWriter— writes the bundle out as plain files, ready to be committed to a git repository or mounted into an agent's file system.
The okf_bundle MCP tool exposes the same mapping; it returns the bundle as
JSON rather than writing it, so the server stays read-only.
Conformance is checked by the build, on both sides of the format:
- The producer.
OkfBundleConformanceTestholds every bundleOkfBundleremits to the specification's conformance conditions, restated from the specification rather than read back out of the bundler — a test that asked the bundler what it meant to write would pass however far the output drifted. It runs in the ordinarytestphase, somvn test,mvn packageandmvn installall catch a drifting emitter without opting into anything. - A bundle on disk. The
okfBundleFormatenforcer rule checks a bundle a repository ships, atvalidateunder-DenforceClaudeMd— the check the pull-request workflow already runs.
A model's context window is finite, so BudgetedContext wraps any Context and
trims its result to fit a token budget. Because Finder returns dependencies in
breadth-first order (closest first), the decorator keeps that priority order and
accepts containers until the next one would exceed the budget:
TokenEstimator estimator = new HeuristicTokenEstimator(); // ~chars/4, no tokenizer dependency
Context budgeted = new BudgetedContext(new Finder(allContainers), estimator, /* token budget */ 8000);
Set<ClassContainer> used = budgeted.find(root, depth);TokenEstimator— abstracts how a piece of text is costed in tokens.HeuristicTokenEstimator— a fast, dependency-free estimate from character count (configurable characters-per-token, default4), rounded up so any non-empty text costs at least one token.BudgetedContext— aContextdecorator that returns the highest-priority prefix of the dependency graph that fits the budget.
It contains:
- data sources
- support relational data loading
- in memory and iterative loading
- CSV, JDBC support
- Parquet (
InMemoryParquetDataSource,IterableParquetDataSource) — read through an in-process DuckDB engine, exposing the file's columns and rows like any other JDBC-backed source - JSON (
InMemoryJSONDataSource,IterableJSONDataSource) — nested objects are flattened with dotted-path keys (e.g.people[0].address.city) - YAML (
InMemoryYAMLDataSource,IterableYAMLDataSource) — same flattening convention; no document-size limit - TOON (
InMemoryTOONDataSource,IterableTOONDataSource) — a compact, LLM-friendly format that minimises tokens; supports key-value pairs, primitive arrays, tabular arrays, and nested objects - All file-based sources accept either a file path or an
InputStream - GZIP decompression — any file-based source transparently decompresses
.gzfiles; no extra configuration needed
- uniqueness checks tool
- for a given set of data and subset of columns you can ask if these columns are unique (can be used as a key)
- the tool also tries to find a better (smaller) answer
- supports in memory and iterative processing
- data structures
- open addressing hashmap: a simpler alternative to HashMap based only on one array and double hashing, it implements java.util.Map<K, V>
- MCP server
- Model Context Protocol server exposing uniqueness checking as a tool for AI assistants
- Compatible with Claude Desktop, Cline, and other MCP clients
- Transports (select with
--transport.mode):stdio(default) — JSON-RPC over stdin/stdout (Spring Boot, no HTTP server started)streamable-http— the modern HTTP transport served at/mcpstateless-http— the same HTTP transport served at/mcp, but session-less: each JSON-RPC request is answered in isolation, which suits load-balanced or serverless deployments- any other value is refused at startup with a message naming the three
- Build:
mvn clean installproducesdata/target/tools.data-<version>.jar - Run:
java -jar data/target/tools.data-<version>.jar --transport.mode=stdio - See MCP Usage Documentation for client configuration (Claude Desktop, Cline) and usage examples
Examples:
in memory check:
AbstractUniqueness check = new InMemoryUniquenessCheck();
check.setDataSource(new InMemorySQLDataSource(connection, query));
Result result = check.exec("COLUMN1", "COLUMN2", "COLUMN3");
log.info(result.isUnique());
Set<Result> betterOptions = result.getBetterOptions();
for (Result betterOption : betterOptions) {
log.info(betterOption);
}In order to add a new data source for example for XML, JSON, etc you just need to implement this interface:
public interface IterableDataSource extends AutoCloseable, Closeable {
public String[] getColumnNames();
public void open();
public String[] nextRow();
public boolean hasMoreData();
public void reset();
// default method, loads up to batchSize rows in one operation
public List<String[]> nextRows(int batchSize);
}nextRow() answers null when a call produced no row, and hasMoreData() is what says which of the two reasons it was: the source is exhausted, or that particular line yielded nothing and a further call may still return a row (a CSV comment does this). null rather than an empty array because an empty array is a row these sources really produce — a blank CSV line splits to one empty column, and a query over no columns gives rows of exactly that shape.
nextRows(int batchSize) lets callers decide how much data is pulled from the source at once instead of reading row by row. It is a default method built on hasMoreData()/nextRow(), so every source gets it for free; an empty list signals the source is exhausted. The SQL source additionally applies batchSize as the JDBC fetch size so the rows are fetched in a single round-trip.
If you need an in memory source you need to implement one more method:
public interface InMemoryDataSource extends IterableDataSource {
public List<String[]> readAll();
}readAll() belongs to this interface alone, so a forward-only source never carries it: the file sources share the drain-the-whole-source machinery as a protected readAllRows() on their base class, and each in-memory source publishes it by writing readAll() itself.
Notes:
in memory checks are using in memory sources that load all the data once and run multiple recursive checks to find better options. Iterative (no memory) checks are keeping only one row at the time so they require very tiny heap size but for the recursive checks need to read the source many times.
A row holding fewer columns than the key reads — a CSV line with a delimiter missing — is reported as a fault in the data: RaggedRowException names the 1-based position of the row among the data rows (the header and any skipped comment line excluded), the arity the key needs and the arity the row has. Columns the key does not read are never touched, so a short row only fails a key that reaches past its end.
OpenAddressingMap<K, V> is a java.util.Map implementation that is simpler
than java.util.HashMap because it uses only one array: entries are stored
directly in a single array via open addressing, instead of HashMap's array of
buckets with linked (or tree-ified) nodes. That makes it an allocation-light
alternative when you want a plain map without the per-entry node objects of
separate chaining.
Map<String, Integer> map = new OpenAddressingMap<>(); // default capacity 64
map.put("a", 1);
map.put("b", 2);
map.get("a"); // 1
map.remove("b"); // 2
map.containsKey("b"); // falseHow it works:
- Double hashing resolves collisions: a key's probe sequence is
h1 + i * h2(modulo the array length), which spreads probes better than linear probing and avoids primary clustering.h1/h2are derived from the key'shashCode()and a prime chosen as the largest prime smaller than the array length. - Tombstones for removal:
removemarks a slot as removed rather than clearing it, so probe sequences that ran through that slot still find the entries placed after it.putreuses the first free slot and a never-used (null) slot terminates a lookup. - Automatic resizing: when the array is about to fill up, it grows by a
1.2factor and all live entries are re-hashed into the new array (tombstones are dropped in the process). The initial capacity can be set vianew OpenAddressingMap<>(size)(minimum effective size is 3); a non-positive size is rejected withIllegalArgumentException.
It extends java.util.AbstractMap, so equals, hashCode and toString are
the ones Map specifies over the entry set — a map holding the same entries as a
HashMap compares equal to it, and prints as {a=1, b=2}.
Caveats:
- Null keys are not supported —
putrejects one with aNullPointerException. As theMapcontract requires of a map that cannot hold such a key,containsKey/get/removereport it absent (false/null) rather than throwing. - Null values are stored faithfully and reported by
containsKey; onlygetcannot tell a storednullfrom a missing key. - The iterators of
keySet,valuesandentrySetare fail-fast: a structural modification made through anything but the iterator's ownremove()makes the nextnext()/remove()throwConcurrentModificationException. - It is not thread-safe; guard external synchronization if shared across threads.
OpenAddressingSet<E> is a java.util.Set backed by an OpenAddressingMap, in
the same way that java.util.HashSet is backed by a java.util.HashMap.
Elements are stored as keys of the underlying map against a shared sentinel
value, so all of the open-addressing behaviour (double hashing, tombstone
removal and automatic resizing) is reused rather than re-implemented.
Set<String> set = new OpenAddressingSet<>(); // default capacity 64
set.add("a"); // true (newly added)
set.add("a"); // false (already present)
set.contains("a"); // true
set.remove("a"); // trueIt inherits the map's caveats: null elements are not supported (add
rejects one with a NullPointerException, while contains/remove report it
absent), its iterator is fail-fast, and it is not thread-safe. The
initial capacity can be set via new OpenAddressingSet<>(size).
IntKeyOpenAddressingMap<V> is a primitive int-keyed sibling of
OpenAddressingMap. It uses the same double-hashing open-addressing strategy,
but stores keys in an int[] so that lookups and inserts never box the key.
That makes it an allocation-light choice for large, integer-keyed maps where the
autoboxing of a Map<Integer, V> would otherwise dominate.
IntKeyOpenAddressingMap<String> map = new IntKeyOpenAddressingMap<>();
map.put(1, "a");
map.get(1); // "a"
map.getOrDefault(2, ""); // "" (absent)
map.remove(1); // "a"
int[] keys = map.keys(); // live keys, unboxedIt deliberately does not implement java.util.Map, because that interface is
defined in terms of Object keys and would reintroduce the very boxing this
class exists to avoid; instead it mirrors the relevant map operations with
primitive int keys. Unlike OpenAddressingMap, null values are stored
faithfully and reported by containsKey(int) — only get(int) cannot tell a
stored null from an absent key. It is not thread-safe.
Switch turns all JVM outbound network access off for the lifetime of the
process. It is useful when you want to guarantee that a data-processing run stays
offline — e.g. no accidental calls out while loading and checking local data.
boolean changed = Switch.off(); // true the first time, false if already offSwitch.off() installs a default ProxySelector that refuses every proxy
selection by throwing UnsupportedOperationException("The network is off"), so
any subsequent attempt to open an outbound connection fails fast. The method is:
- One-way — there is no
on(); once off, the JVM stays offline. Apply it early, only when you really mean to seal the process. - Idempotent and thread-safe — the one-shot installation is serialized on a
private monitor and the state is guarded by a
volatileflag; calling it again is a no-op that logs a warning and returnsfalse. The monitor is deliberately notSwitch.class: that object is reachable from anything on the classpath, so astatic synchronizedmethod would let unrelated code contend with — and delay — the kill-switch.
The switch is wired into the data module's unit-test run for exactly this
reason: a NetworkOffExtension (a JUnit BeforeAllCallback discovered through
META-INF/services and JUnit's extension auto-detection) calls Switch.off()
before any unit test runs, so a unit test can never open an outbound connection.
Auto-detection and the tools.test.network.off guard property are set only on
surefire, so the failsafe integration tests (*IT), which need real network, keep
it. See Testing in AGENTS.md for the details.
A single test can also opt in explicitly with the @NetworkOff annotation, which
registers the same extension via @ExtendWith:
@NetworkOff
class MyDataSourceTest {
// every test here runs with the network off
}Unlike the module-wide auto-detection, an explicit @NetworkOff engages the
kill-switch unconditionally — regardless of the tools.test.network.off
property — so the network is off even when the test is run on its own from an IDE.
The adopt module (tools.adopt) is an ordered pipeline that adopts Claude
Code into a GitHub repository. Given a repository URL it clones the repo,
creates a feature branch, runs the Claude Code CLI (claude -p /init) to
generate a CLAUDE.md and commits it, then wires a CLAUDE.md guard into the
project's build and commits that too — so the freshly generated CLAUDE.md keeps
being validated on every build. The guard is build-tool aware: a Maven project
gets the full claude-code-enforcer rule in
its pom.xml, while a Gradle project (Groovy build.gradle or Kotlin
build.gradle.kts) gets a presence-and-non-empty guard task appended to the
build script — Gradle has no enforcer-rule equivalent. A repository with no
recognised build file falls back to a build-tool-agnostic GitHub Actions
workflow that runs a portable presence-and-non-empty check script on every push
and pull request, so even a build-less repository keeps the guard. Finally it
pushes the
branch and opens a pull request with the GitHub CLI, so the change is reviewed
rather than landing straight on the default branch, which is never written to.
Run it from the command line with a GitHub repository URL, an optional workspace
directory to clone into (a temporary directory is created when it is omitted),
and an optional feature-branch name (defaults to claude/adopt-claude-code).
Because it opens the pull request through the GitHub CLI, an authenticated gh
must be on the PATH alongside git and claude. Launch it through exec:java
so Maven puts the full runtime classpath (log4j2 and the rest) on the command — a
bare java -cp adopt/target/classes omits the dependency jars and fails at
start-up with a NoClassDefFoundError for the log4j LogManager:
mvn -pl adopt exec:java \
-Dexec.args="https://github.com/owner/repo.git [workspace-directory] [branch-name]"--help (or -h) prints the usage line and adopts nothing, so the arguments can
be asked for without naming a repository.
Before letting a run write to GitHub, --dry-run rehearses it: the repository is
cloned, branched, and committed on, and the guard is wired in and verified, but
the branch is never pushed and no pull request is opened. The pipeline is
assembled without those two steps rather than with steps that decide to do
nothing, so the report's completedSteps ends at verify and says what really
happened. The checkout is left in the workspace for the adoption's commits to be
read before any of it is published, and the report's checkout says where it is
— which is the only way to find the temporary workspace a run that named none was
given:
mvn -pl adopt exec:java \
-Dexec.args="https://github.com/owner/repo.git --workspace /tmp/adoptions --dry-run"Every external command is bounded by a timeout — 10 minutes by default —
overridable with --timeout <minutes>, for a repository whose claude init or
whose first Maven build against a cold ~/.m2 needs longer, or for a batch that
should fail fast instead.
A git or gh command the network refused is run again rather than failing
the repository: twice by default, after 2 seconds and 4 seconds, with
--retries <count> asking for more (up to 10) or --retries 0 for none. An
unattended batch otherwise lost a whole repository — claude init included, which
is the expensive part — to a single connection reset, and had to be started again
by an operator who first had to notice. Only a transport-level refusal is retried,
in the tools' own words: a host that would not resolve, a connection refused, reset
or timed out, a remote that hung up, a 5xx from GitHub. An authentication failure,
a 404, a rejected non-fast-forward push and a rate limit that wants minutes are all
reported the moment they happen, as is anything claude or the project's own build
tool says — re-running either costs the run minutes and their output is prose that
may merely mention a network error.
One run can adopt a list of repositories rather than a single one: repeat
--repo <url> for each, or point --repos <file> at a file naming one
repository per line (blank lines are skipped and a # line is a comment, so a
batch can be annotated and a repository commented out for a run). Duplicates are
adopted once. Every repository of the run shares the workspace and the branch
name — each clone lands in its own directory under the workspace, named after the
repository — so a batch driven entirely by the flags names them with
--workspace and --branch, the first positional argument always being a
repository URL; a first positional that names no repository owner while the flags
named repositories is rejected, since that is the workspace this reading invites.
Two repositories that would clone into the same directory — owner/tools and
other-owner/tools, or one repository named both with and without its .git
suffix — are refused rather than adopted on top of each other. The refusal belongs
to the repository that raised it: each one claims its checkout directory inside its
own adoption, so the second is recorded as its failure and the repositories
around it are still adopted, rather than the whole batch aborting before its first
clone:
mvn -pl adopt exec:java \
-Dexec.args="--repos repos.txt --workspace /tmp/adoptions --report report.json"A repository whose adoption fails does not strand the ones behind it: the batch
runs to the end, and the failures are raised together afterwards so the command
still exits non-zero. The --report file says which repositories landed — a run
over several repositories writes an overall succeeded (true only when every one
was adopted) and a repositories array of exactly the per-repository documents a
single-repository run writes unwrapped:
{
"succeeded" : false,
"repositories" : [ {
"repositoryUrl" : "https://github.com/owner/repo.git",
"branch" : "claude/adopt-claude-code",
"checkout" : "/tmp/claude-adopt-4711/repo",
"pullRequestUrl" : "https://github.com/owner/repo/pull/42",
"succeeded" : true,
"failure" : null,
"completedSteps" : [ "toolchain", "clone", "…", "pull-request" ]
}, {
"repositoryUrl" : "https://github.com/owner/other.git",
"branch" : "claude/adopt-claude-code",
"checkout" : "/tmp/claude-adopt-4711/other",
"pullRequestUrl" : null,
"succeeded" : false,
"failure" : "clone: repository not found",
"completedSteps" : [ "toolchain" ]
} ]
}--assets commits a set of starter Claude Code configuration files alongside the
generated CLAUDE.md: an AGENTS.md pointer, a .claude/settings.json that
denies reads of obvious secret files and wires a .claude/hooks/session-start.sh
stub, a starter .mcp.json, and a GitHub Actions workflow answering @claude
mentions. None of them ever overwrites a file the repository already carries, so
the flag is safe on a project that has configured some of them already.
The claude-code-enforcer version a Maven project's pom.xml is made to depend
on defaults to the version of the tools build running the adoption;
--rule-version <version> pins a different one. A -SNAPSHOT is refused either
way, because it resolves only from the adopting machine's own local repository:
wiring one in would open a pull request that builds for whoever ran the adoption
and fails for the adopted project's CI and every one of its contributors. A
snapshot build of tools therefore cannot adopt a Maven project until a release
of the rule is published.
The pipeline is a list of ordered, independent AdoptionSteps, each acting on a
shared immutable AdoptionContext (the repository URL, the workspace, the
derived checkout directory, and the feature-branch name):
AdoptionOptions options = AdoptionOptions.defaults();
CommandRunner runner = new RetryingCommandRunner(new ProcessCommandRunner(options.commandTimeout()),
options.retries());
GitHubRepoAdopter.withDefaultPipeline(runner, options)
.adopt(new AdoptionContext("https://github.com/owner/repo.git", workspace), new AdoptionReport());AdoptionOptions is how a run is configured — the pull request's metadata, the
starter assets, the rule version to pin, whether it is a dry run, how long one
command may take, and how many further attempts one the network refused earns. Both entry points build one, so the command line and the
MCP tool cannot drift apart on what an omitted option means, and the pipeline
factory does not grow a parameter per switch.
The report is a parameter rather than a return value alone, so a run that fails part-way still leaves the caller holding the steps that did complete and the reason it stopped.
BatchAdoption wraps that pipeline to work through a list of repository URLs,
handing each to a Checkouts that gives it its own directory under the run's
shared workspace, and answers with an AdoptionRun (the redacted URL, the branch,
and the report) per repository — a failing repository is recorded rather than
allowed to abandon the rest. The context is claimed inside each repository's own
adoption rather than for the whole run up front, so a URL that names no repository
is that repository's recorded failure too:
GitHubRepoAdopter adopter = GitHubRepoAdopter.withDefaultPipeline(runner, AdoptionOptions.defaults());
Checkouts checkouts = new Checkouts(workspace, AdoptionContext.DEFAULT_BRANCH);
List<AdoptionRun> runs = new BatchAdoption(adopter::adopt).adoptAll(repositoryUrls, checkouts);The default pipeline runs these steps in order:
ToolchainStep— probes the external tools the pipeline shells out to (git,claude,gh) with a--versioncheck before any real work, so a missing tool aborts the adoption immediately with a message naming every absent one instead of failing minutes later after a clone, aclaude init, and a Maven build have already run. Being installed is not enough forgh:gh --versionsucceeds for a CLI nobody is logged in to, so the login is probed too and an unauthenticatedghfails here rather than at the very last step. The probe reads the repository being adopted,gh api repos/<owner>/<repo>— the access the pull request will need — rather thangh auth status, which reports a rejectedGH_TOKENas invalid and still exits zero, and rather than the user-scopedgh api user, which a GitHub App installation token (a CI run'sGITHUB_TOKEN) is refused by even when it can open the pull request perfectly well. A URL naming no owner has no repository to ask about and falls back togh api user.CloneStep— clones the target repository into the workspace withgit clone, giving the remaining steps a working checkout. A freshly cloned checkout then has itsoriginrewritten to the same URL without its credentials:git clonerecords the URL it was handed verbatim, so a run driven by a CI token would otherwise leave that token in the workspace's.git/configin plaintext for as long as the workspace survives. A checkout the adoption is reusing is left as its owner configured it.BuildToolchainStep— probes the adopted project's build tool, now that the clone has revealed which one it is.ToolchainStepcannot: the checkout does not exist yet. Without this a machine without the project'smvnorgradleonly fails atVerifyStep, after a fullclaude init, a reshapedCLAUDE.md, and two commits have already been spent on a checkout that was never going to be verifiable. A build system needing no tool of its own — the fallback guard runs throughsh— is a no-op.BranchStep— creates and checks out the adoption feature branch withgit checkout -B, so every later commit lands on that branch instead of the default branch. A fresh clone whoseoriginalready publishes the branch — a re-adoption of a repository an earlier run pushed — starts it from that published tip, so the later push stays a fast-forward instead of being rejected; a branch that already exists locally is left alone, so unpushed work in a reused workspace is never reset onto the remote.TrustStep— marks the checkout trusted in~/.claude.jsonso the headlessclauderun is not blocked by the interactive folder-trust prompt.ClaudeInitStep— runs the Claude Code CLI in headless mode (claude -p /initby default; the invocation is configurable because the flags differ between environments) so it generates aCLAUDE.md, aborting if the file did not appear.ClaudeMdConformanceStep— reshapes that generated file so it satisfies theclaudeMdFormatrule the next step is about to wire in, and writes the companionAGENTS.mdthe rule's reference has to resolve to. Without it the adoption fails its ownVerifyStep: a genericclaude initwrites natural, project-specific headings and noAGENTS.mdreference, while the rule demands a fixed set of whole-line headings plus that reference. The reshape is deterministic and conservative — a near-miss heading is renamed in place so its body survives, only a genuinely absent section is appended, a required section left empty gets a stub body because the rule fails an empty section just as it fails a missing one, and fenced code and commented-out text are left alone, mirroring how the rule matches. Reshaping an already-conforming document is a no-op, so a re-adoption leaves the file untouched.CommitStep— commits the generatedCLAUDE.mdand its companion (Adopt Claude Code: add CLAUDE.md), reported ascommit:claude-md.EnforcerStep— detects the checkout's build system and wires theCLAUDE.mdguard into it. A Maven project has theclaude-code-enforceradded to its rootpom.xmlviaPomEnforcerInstaller(namespace-aware and idempotent); a Gradle project has aenforceClaudeMdguard task appended to itsbuild.gradle/build.gradle.ktsviaGradleGuardInstaller, wired intocheck. A repository with no recognised build file falls back to aFallbackBuildSystemthat installs a GitHub Actions workflow and the portable.github/claude-md-guard.shcheck it runs viaWorkflowGuardInstaller. All installs are idempotent and none of them overwrites a file the project already carries — a repository with its ownclaude-md-guard.ymlor its ownenforceClaudeMdregistration keeps it. Supporting a new build tool is a matter of adding aBuildSystemimplementation rather than branching inside the step. For Maven that already-declared check asks the POM's ownbuild/plugins— the one place a rule runs on every build, and the very place the installer would add one, so what it inspects and what it edits cannot disagree. A rule declared only inpluginManagement, only inside a profile, or only underreportingis not one an ordinary build runs, so the project is given an always-on declaration of its own and the one it had is left verbatim. Standing down for any of those left the build its contributors and its CI actually run with no guard at all, whichVerifyStepcannot catch — itsmvn -N validatepasses precisely because the rule never ran. The POM edit is spliced into the bytes the file already holds, at the source offsetsPomDocumentreads back from jsoup's XML parser, so the adoption commit shows the added block and nothing else — writing a parsed document out whole would normalise details a DOM does not record, collapsing a start tag spread over several lines and rewriting<rule />as<rule/>, turning a fourteen-line addition into a diff across the file.CommitStep— commits the build change (Add claude-code-enforcer to the build), reported ascommit:guard.AssetsStep→CommitStep— only on--assets(assetson the MCP tool): installs the starter configuration files described above and commits them (commit:assets). Each asset is installed independently and never overwrites an existing file, so the pair is idempotent.VerifyStep— runs the detected build system's verification (a non-recursivemvn -N validatefor Maven, theenforceClaudeMdtask for Gradle, the.github/claude-md-guard.shscript for the fallback) so the freshly wired guard actually executes against the generatedCLAUDE.md, failing the adoption locally if the file is missing or malformed rather than after the pull request lands.PushStep— pushes the feature branch to origin and sets its upstream (git push -u origin <branch>). The run's own credentials are supplied to that one command as a-c remote.origin.pushurloverride, sinceCloneStepdeliberately leaves none in the checkout; git applies the override to the single invocation and stores it nowhere. Left out of a--dry-runpipeline, together with the step below it.PullRequestStep— opens a pull request from the branch withgh pr create, targeting the repository's default branch as the base. The pull request metadata is supplied throughPullRequestOptions— title, body, and optional reviewers, labels, and assignees to request, plus whether to open the pull request as a--draft— so the defaults can be overridden per project. LikeCommitStepit stays idempotent: the branch's open pull requests are read first (gh pr list --state open) and creation is skipped when one is already open, and agh pr createthat fails only because a pull request for the branch already exists, or because there are no commits between base and head, is treated as a no-op rather than aborting the adoption. That tolerance is what makes re-running an adoption safe even where the pre-check cannot run:gh pr listneeds a query a restricted token or a proxied host may refuse, and a failed query is indistinguishable from "nothing is open" — so it is logged as a warning and the create that follows is allowed to report the duplicate itself.
External git/claude/gh invocations go through a CommandRunner abstraction,
so the steps are unit-tested without spawning real processes. The default
ProcessCommandRunner merges standard error into standard output for a single
ordered transcript, and bounds every command with a configurable timeout
(10 minutes by default, --timeout <minutes> on the command line and
timeout_minutes on the MCP tool) so a stalled clone or a stuck claude run
cannot hang the adoption — on expiry the child process is destroyed and the failure is
reported with whatever output was captured so far. A step whose command exits
non-zero aborts the pipeline with an AdoptionException carrying the command
transcript.
Every production module guards its own package structure with
ArchUnit rules that run as ordinary JUnit tests,
so an accidental dependency or a broken naming convention fails the build
instead of quietly eroding the design. Each module keeps its rules in a single
*ArchitectureTest under an architecture package, annotated with
@AnalyzeClasses(..., importOptions = ImportOption.DoNotIncludeTests.class) so
that only production classes are analysed. The ArchUnit dependency
(com.tngtech.archunit:archunit-junit5) is managed centrally in the root
pom.xml, and the tests run as part of the normal mvn install.
A set of conventions is shared across modules:
- No cycles between packages —
slices().matching(...).should().beFreeOfCycles()keeps the package graph acyclic in each module. - Loggers are constants — every
org.apache.logging.log4j.Loggerfield must beprivate static final(the context module relaxes this toprivate final), because a logger is a shared, immutable, class-scoped collaborator. *Exceptiontypes really are exceptions — any class whose simple name ends withExceptionmust be assignable tojava.lang.Exception.Abstract-prefixed names — a top-level abstract class must have a simple name starting withAbstract, so that a type meant to be extended is obvious at a glance.- Logging goes through log4j2, not the console or the JDK — ArchUnit's
GeneralCodingRulesforbid access toSystem.out/System.err, throwing generic exceptions, and usingjava.util.logging; libraries additionally must never callSystem.exit. Thedatamodule tightens this further, also rejecting the JDK's ownSystem.Loggerso all logging stays on log4j2.protogen-maven-pluginis the one exemption, and it has a rule of its own saying so: a Maven plugin reports throughAbstractMojo.getLog(), which is what honours-qand-Xand attributes a line to the plugin in the reactor output, sopluginLogsThroughTheMojoLogkeeps log4j2 out of its production code and out of the plugin jar. - No raw stack traces — no class may call any
Throwable.printStackTraceoverload, because a failure must be reported through log4j2 rather than dumped to the console. - Public fields are immutable — every
publicfield must befinal, so a field that is part of a type's API surface cannot be reassigned from outside. - No legacy date library —
GeneralCodingRulesforbid a dependency on Joda-Time, keeping date/time handling on thejava.timeAPI.
On top of that baseline, each module pins the boundaries specific to its own design:
data— data-source contracts (source.interfaces) must stay interfaces and must not know their concretesource.db/source.fileimplementations;structure.internalis accessible only fromstructure; the reusablestructurecollections must not couple to data sources; every concrete*DataSourcemust implementIterableDataSource; the uniqueness core must not depend on itsmcpadapter; and alayeredArchitecturepins the source layers so file and DB sources depend only downwards on their contracts (files may also use compression), never on each other. Two rules keep the open-addressing structures honest about their documented lack of thread-safety: no method instructuremay besynchronized, andstructuremay not depend onjava.util.concurrent, so neither can silently suggest the collections are safe to share. Three more pin thenetworkkill-switch to its documented shape: no method there may besynchronized, since astatic synchronizedmethod would lock the publicly reachableSwitch.class; theLOCKit synchronizes on instead must beprivate static final; and itsisOffflag must bevolatile.code/context— the finder/tree core must not depend on themcpdelivery package, and only thatmcppackage may build on the shared MCP scaffolding; every concrete*Serializermust honour theProjectTreeSerializercontract.code/protogen-maven-plugin— the reusableformatpackage must not depend on thegencode generator that builds on it, and every concrete*Mojomust implement the MavenMojocontract.mcp-common— theMcpToolSPI must stay an interface, every concrete*Toolmust implement it, and the shared scaffolding must never callSystem.exit.claude-code-enforcer— alayeredArchitecturepins the module's layers (textis the foundation,rulebuilds on it, and the feature packagesdefinition/doc/mcp/settingsbuild onrulewithout reaching sideways into one another), and every concrete*Rulemust extend the sharedClaudeCodeEnforcerRulebase.adopt— thecommandrunner layer must not depend on thesteppackage, so the reusable command abstraction stays unaware of the adoption steps that build on it; and every concrete*Stepinstepmust implement theAdoptionStepcontract. The pipeline also carries the shared baseline in full, including that it reports failure by throwingAdoptionExceptionand never callsSystem.exit.
Alongside the production rules, each module carries a companion
TestConventionsArchitectureTest that analyses only the test classes (via
ImportOption.OnlyIncludeTests) and pins conventions on the tests themselves:
every @Testable method must live in a *Test or *IT class so surefire or
failsafe actually runs it, no test is @Disabled, tests use JUnit Jupiter only (no
JUnit 4 org.junit API), and no test calls Thread.sleep or TimeUnit.sleep
(sleeping is slow and flaky — wait on a condition instead). A few rules guard
against tests that silently never run: a @Testable method must not be
private or static (Jupiter quietly ignores both), and a @BeforeAll/
@AfterAll method must be static (Jupiter requires it unless the class opts
into the PER_CLASS lifecycle).
Run them for a single module with, for example:
mvn -pl data -am test
(-am is required — a bare mvn -pl data test fails the root pom's
ReactorModuleConvergence enforcer rule.)
or across the whole repository as part of mvn install.
The unit tests run offline, without spawning a process and against directories
this repository laid out, so they are fast and say nothing about the seams where
the code meets something real. Those seams are covered by *IT classes gated
behind the integration-tests profile, declared per module in data,
code/context, adopt and claude-code-enforcer — a new module's *ITs stay
unrun until it gets its own copy of the profile:
mvn -P integration-tests verify
They are run daily by .github/workflows/integration-tests.yml rather than on a
pull request, because they clone from GitHub and run nested Maven builds. A green
PR build is therefore not proof that this suite passes.
All three servers — data, code/context and adopt — start as Spring Boot
applications on a random port and are talked to through a real MCP client, so the
tool listing, the argument schemas and the results are exercised as a client meets
them rather than through a direct method call. Each covers streamable HTTP and
stateless HTTP; only the transport differs between the subclasses, so the
fixture and the helpers live in an abstract parent and each subclass supplies the
transport and the calls it is there to assert.
adopt's pair is the one
whose tool does real work over the wire: adopt_repo is called with
verify_only, so the server clones octocat/Hello-World, finds it was never
adopted, and answers with the failed run's whole JSON report — the steps that
completed, the verdict that stopped it, and the checkout it really made under the
test's own workspace, which is then asserted on disk. A verification shells out to
git alone and writes nothing, to GitHub or to the checkout, so it is the
heaviest payload these transports can carry without a logged-in claude or gh;
a dry_run would reach claude init. The streamable test also pins the tool's
whole argument set as it arrives on the wire, an argument lost between the
tool definition and the client being invisible from inside the pipeline.
code/context adds an HTTPS run against a throwaway self-signed keystore,
which is where
McpStreamableHttpsIT
asserts the server's TLS hardening end to end: a tool call succeeds over the secure channel,
the negotiated protocol is TLS 1.3, a client offering only TLS 1.2 is refused,
and the pinned key-exchange groups really govern the handshake.
Everything between a pom.xml and a rule's execute() is what a unit test
assumes: that Maven resolves the rule jar, that Sisu finds the class behind a
@Named element, and that Plexus binds each configuration element to the field
of that name. A rule can be correct and still never run — a misspelled
parameter fails the build outright, a misspelled rule name never runs at all —
and none of that is visible from inside the rule. So
claude-code-enforcer's e2e package
runs real Maven builds:
EnforcerRuleBuildITbuilds a throwaway project and enforces a configuration that addresses every shipped rule with every parameter it accepts — held against the compiled classes by reflection, so a new rule or parameter no fixture addresses is named rather than silently unexercised — and pins the shared behaviours across the Maven boundary: collect-then-report,severity=warngoing green while still logging, the HTML report landing on disk, and a baseline suppressing recorded violations.RepositoryEnforcementITruns this repository's ownmvn -N validate -DenforceClaudeMd, the check the pull-request workflow runs, and confirms every wired rule reported a pass — a rule that never ran would otherwise leave the test green while guarding nothing. It also holds the shipped catalogue against the profile, so a rule cannot ship unwired unnoticed, and then breaks a copy of the checkout to prove the wiring bites.ForeignRepositoryEnforcementITpoints that complete configuration at eight real repositories shallow-cloned from GitHub, none of which has ever heard of this enforcer — among them a realAGENTS.md, a real.mcp.jsonand a real.claude/agentsdirectory beside a real.claude/settings.json, so the rules that read those meet a file somebody else wrote rather than only its absence. None of them passes and none should; what is asserted is that every rule reaches a verdict on every one of them — a pass, or a failure naming what is wrong — rather than going red on the rules' own account with an unbindable parameter or an exception escaping a file it did not expect. Only the log tells those apart, so the per-rulepassed/failed with message:lines are read back out of it. The checkouts are also asked, after all eight builds, to be exactly asgitproduced them — these are somebody else's projects, and a rule that wrote into one would have edited a project that asked it for a verdict.
The adoption's steps are unit-tested through the CommandRunner abstraction,
without spawning a process. What that cannot show is what the pipeline does to a
project nobody prepared for it — which is every project it will ever be
pointed at. Two *ITs in adopt
clone from GitHub with the real git and answer that:
MultiRepoAdoptionITadopts several repositories in one run through the command line an operator uses, proving a batch gives each repository its own checkout, that an uncloneable repository costs only itself, and that two URLs for one repository — or a checkout holding a different repository — are refused. A recording stub and synthetic URLs prove the wiring; only agit clonethat really ran twice into one workspace proves the checkouts.ForeignRepositoryAdoptionITadopts seven real repositories —google/gson,square/okhttp,anthropics/anthropic-quickstarts,anthropics/claude-code,github/gitignore,JakeWharton/timberandmodelcontextprotocol/servers— chosen for the shapes they put in front of the steps that read a checkout: a multi-module Maven build, a Gradle build on the Kotlin DSL and another on the Groovy one, a realCLAUDE.md, a real.claudedirectory, a project whose own files already sit where two of the starter assets go, a default branch called neithermainnormaster, and a very large flat tree with no build file at all. Between them they are the only place all three build systems — and both Gradle DSLs — meet build files this repository did not write. What is asserted is that the adoption's work is its own and nothing else: the guard that lands is the one the checkout's build files ask for and is written in that build script's own DSL, the commits carry only the paths the pipeline claims to write, the guard commit removes nothing the build file already declared, the starter assets the project already keeps at those paths come through byte-identical to the blobs that were cloned, the default branch — read from the remote rather than guessed — is left as cloned, GitHub itself is asked withls-remoteand has no branch of the adoption's, and adopting the same batch a second time commits nothing.
Both adoption *ITs stop short of the push and the pull request, so the runs stay
read-only towards GitHub, need no logged-in gh, and may be repeated as often
as the suite likes.
mvn clean install
The clean part is needed since the build contain code generation so if you remove a source of generation and do not use clean then the result of previous build may remain in target. If you do not remove anything you could benefit from faster:
mvn install
In order to release a new version - X you need to:
- Change the revision property to X in root pom.xml
- Commit and push
- Check if all builds pass
- Release and mark as latest in GitHub
This project is licensed under the MIT License.