Skip to content

Repository files navigation

tools

Library of tooling for various purposes.

Table of Contents

Claude Code files Maven enforcer

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 that CLAUDE.md exists and is non-empty, starts with the # CLAUDE.md title (a leading UTF-8 BOM is tolerated), references AGENTS.md, and contains every required section heading.
  • agentsMdFormat (AgentsMdFormatRule) — applies the same structural checks to AGENTS.md: it must start with the # AGENTS.md title and contain every required section heading.
  • skillFilesExist (SkillFilesExistRule) — checks that every skill directory under .claude/skills contains a non-empty SKILL.md that opens with a YAML front matter block declaring every required key (name, description by default). The name must follow the Claude Code naming convention (lower-case kebab-case, bounded length) and match the skill's directory name; the description must be non-empty and within maxDescriptionLength. An optional allowedFrontMatterKeys whitelist catches typos such as descripton.
  • subAgentFormat (SubAgentFormatRule) — treats every *.md file 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 a name that follows the naming convention and matches its file name. An optional allowedModels whitelist rejects a mistyped model such as claud-opus.
  • commandFormat (CommandFormatRule) — treats every *.md file 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 a description must be non-empty, a model must be one of allowedModels when that whitelist is configured, and an optional allowedFrontMatterKeys whitelist catches typos such as argument-hnt.
  • settingsJsonValid (SettingsJsonValidRule) — checks that .claude/settings.json exists, is non-empty, and parses as JSON. It can also assert policy on permissions.allow: requiredPermissions must all be present and forbiddenPermissions must all be absent, so a project can mandate a permission it relies on or ban an over-broad wildcard such as Bash(*).
  • hookCommandsValid (HookCommandsValidRule) — validates the hooks section of .claude/settings.json: every event must map to an array of groups, each group must carry a hooks array, and every hook must declare a non-blank type (a command hook also a non-blank command). 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 against projectDir and 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 optional allowedEvents whitelist rejects a mistyped event such as SessionSart, and validateScriptReferences can 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 under mcpServers must be a JSON object with a well-formed transport. A stdio server (the default when no type is declared) needs a non-blank command; an sse or http server needs a non-blank url. An explicit type outside the allowedTypes whitelist (stdio, sse, http by default) is reported, catching a mistyped htttp. requiredServers must all be present and forbiddenServers must 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.json server entry that mcpServersValid leaves unchecked: args must be an array of strings, env and headers must be objects whose values are all strings, a url must be a syntactically valid http/https URL (and https only when requireHttps is set), and a server must not mix transports by declaring both a command and a url. Like mcpServersValid it treats an absent file as a pass.
  • okfBundleFormat (OkfBundleFormatRule) — holds a bundle in Google's Open Knowledge Format at bundleDir to the specification's own conformance conditions: every non-reserved .md file carries a parseable YAML front matter block, every block declares a non-empty type (the one mandatory field), and the reserved names keep their structure — an index.md carries no front matter beyond an okf_version at the bundle root, and a log.md groups its entries under ISO 8601 YYYY-MM-DD headings. Where the format defines a closed vocabulary the value is checked too: a status must be draft, stable or deprecated, a stale_after must be a real calendar date, and a generated mapping must name the actor that produced the concept. Where the format leaves things open, nothing is imposed — type values are not registered centrally, so an unknown one is not a violation. Beyond that, requiredKeys adds front matter keys every concept must declare, okfVersion pins the version the bundle root declares, and requireIndex demands a listing in every directory holding concepts (off by default, since a consumer must tolerate a missing index.md). A bundle is optional, so an absent bundleDir is a pass and the rule starts enforcing the day a bundle is committed.
  • hooksFormat (HooksFormatRule) — validates the hook scripts under a configured hooksDir (e.g. .claude/hooks): every regular file must be non-empty, start with a #! shebang (requireShebang), and carry the executable bit (requireExecutable), and an optional allowedExtensions whitelist rejects a stray file. Where hookCommandsValid validates the JSON shape of the hooks section, this rule validates the scripts themselves; when a settingsFile is 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, and reportUnreferencedScripts flags a script no hook references. An absent hooksDir is a pass because hooks are optional.
  • uniqueDescriptions (UniqueDescriptionsRule) — reads the description from the front matter of every sub-agent (*.md), command (*.md), and skill (SKILL.md) in the configured commandsDir, agentsDir, and skillsDir, 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 with uniqueNames, 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 configured commandsDir, agentsDir, and skillsDir (a command's and a sub-agent's name is its *.md file 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) — keeps CLAUDE.md and AGENTS.md from contradicting each other. Each configured consistentPattern is a regular expression with one capturing group; the captured value must agree between the two files (or be absent from both). For example Java (\d+) fails the build if one file says Java 25 and the other Java 24.
  • readmeConsistency (ReadmeConsistencyRule) — keeps this README.md from drifting away from the agent docs (AGENTS.md, the single source of truth). Each configured consistentPattern (one capturing group) must capture the same value in both files, so a documented capability or version cannot silently disagree with the agent docs. Unlike crossDocConsistency, 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

Code generation

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.

gRPC example

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:

  1. protobuf-maven-plugin compiles the proto definitions into protobuf message classes and gRPC service stubs (GreeterGrpc).
  2. protogen-maven-plugin (this repo) generates compile-time-safe builders (AddressBuilder, PersonBuilder, HelloRequestBuilder, HelloReplyBuilder) that refuse to call build() until every required field 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 under src/test/java because the protogen-generated builders are written to target/generated-test-sources. Run the example with mvn -pl grpc-example -am test.

See the grpc-example module for full details and how to run the example.

Context engineering

Java code context build up

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 code context build up

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 code context build up

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.

Project tree

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, .kt or .scala, depending on the configured Language) it resolves the project classes it uses with the same regex-based Context, skipping the file's own class. depth controls how deep the usage search goes, exactly as in the Context interface above.
  • ContextFactory — decouples the builder from the concrete dependency finder (defaulting to Finder), 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.

Output formats

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; serializePretty produces 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 Mermaid flowchart, which renders inline on GitHub, in Markdown viewers and in many gen-AI agent surfaces without any external tooling.

Open Knowledge Format bundles

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 a ProjectTreeNode tree 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 the index.md entry linking to it.
  • OkfFrontmatter — renders the YAML block. OKF makes exactly one field mandatory, type; the rest are recommended, and v0.2 records production as generated: { 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. OkfBundleConformanceTest holds every bundle OkfBundler emits 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 ordinary test phase, so mvn test, mvn package and mvn install all catch a drifting emitter without opting into anything.
  • A bundle on disk. The okfBundleFormat enforcer rule checks a bundle a repository ships, at validate under -DenforceClaudeMd — the check the pull-request workflow already runs.

Token-budget-aware context

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, default 4), rounded up so any non-empty text costs at least one token.
  • BudgetedContext — a Context decorator that returns the highest-priority prefix of the dependency graph that fits the budget.

Data

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 .gz files; 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 /mcp
      • stateless-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 install produces data/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.

Open-addressing map

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");  // false

How 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/h2 are derived from the key's hashCode() and a prime chosen as the largest prime smaller than the array length.
  • Tombstones for removal: remove marks a slot as removed rather than clearing it, so probe sequences that ran through that slot still find the entries placed after it. put reuses 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.2 factor and all live entries are re-hashed into the new array (tombstones are dropped in the process). The initial capacity can be set via new OpenAddressingMap<>(size) (minimum effective size is 3); a non-positive size is rejected with IllegalArgumentException.

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 supportedput rejects one with a NullPointerException. As the Map contract requires of a map that cannot hold such a key, containsKey/get/remove report it absent (false/null) rather than throwing.
  • Null values are stored faithfully and reported by containsKey; only get cannot tell a stored null from a missing key.
  • The iterators of keySet, values and entrySet are fail-fast: a structural modification made through anything but the iterator's own remove() makes the next next()/remove() throw ConcurrentModificationException.
  • It is not thread-safe; guard external synchronization if shared across threads.

Open-addressing set

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");       // true

It 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).

Primitive int-keyed map

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, unboxed

It 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.

Network kill-switch

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 off

Switch.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 volatile flag; calling it again is a no-op that logs a warning and returns false. The monitor is deliberately not Switch.class: that object is reachable from anything on the classpath, so a static synchronized method 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.

Claude Code adoption

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:

  1. ToolchainStep — probes the external tools the pipeline shells out to (git, claude, gh) with a --version check 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, a claude init, and a Maven build have already run. Being installed is not enough for gh: gh --version succeeds for a CLI nobody is logged in to, so the login is probed too and an unauthenticated gh fails 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 than gh auth status, which reports a rejected GH_TOKEN as invalid and still exits zero, and rather than the user-scoped gh api user, which a GitHub App installation token (a CI run's GITHUB_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 to gh api user.
  2. CloneStep — clones the target repository into the workspace with git clone, giving the remaining steps a working checkout. A freshly cloned checkout then has its origin rewritten to the same URL without its credentials: git clone records the URL it was handed verbatim, so a run driven by a CI token would otherwise leave that token in the workspace's .git/config in plaintext for as long as the workspace survives. A checkout the adoption is reusing is left as its owner configured it.
  3. BuildToolchainStep — probes the adopted project's build tool, now that the clone has revealed which one it is. ToolchainStep cannot: the checkout does not exist yet. Without this a machine without the project's mvn or gradle only fails at VerifyStep, after a full claude init, a reshaped CLAUDE.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 through sh — is a no-op.
  4. BranchStep — creates and checks out the adoption feature branch with git checkout -B, so every later commit lands on that branch instead of the default branch. A fresh clone whose origin already 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.
  5. TrustStep — marks the checkout trusted in ~/.claude.json so the headless claude run is not blocked by the interactive folder-trust prompt.
  6. ClaudeInitStep — runs the Claude Code CLI in headless mode (claude -p /init by default; the invocation is configurable because the flags differ between environments) so it generates a CLAUDE.md, aborting if the file did not appear.
  7. ClaudeMdConformanceStep — reshapes that generated file so it satisfies the claudeMdFormat rule the next step is about to wire in, and writes the companion AGENTS.md the rule's reference has to resolve to. Without it the adoption fails its own VerifyStep: a generic claude init writes natural, project-specific headings and no AGENTS.md reference, 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.
  8. CommitStep — commits the generated CLAUDE.md and its companion (Adopt Claude Code: add CLAUDE.md), reported as commit:claude-md.
  9. EnforcerStep — detects the checkout's build system and wires the CLAUDE.md guard into it. A Maven project has the claude-code-enforcer added to its root pom.xml via PomEnforcerInstaller (namespace-aware and idempotent); a Gradle project has a enforceClaudeMd guard task appended to its build.gradle/build.gradle.kts via GradleGuardInstaller, wired into check. A repository with no recognised build file falls back to a FallbackBuildSystem that installs a GitHub Actions workflow and the portable .github/claude-md-guard.sh check it runs via WorkflowGuardInstaller. All installs are idempotent and none of them overwrites a file the project already carries — a repository with its own claude-md-guard.yml or its own enforceClaudeMd registration keeps it. Supporting a new build tool is a matter of adding a BuildSystem implementation rather than branching inside the step. For Maven that already-declared check asks the POM's own build/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 in pluginManagement, only inside a profile, or only under reporting is 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, which VerifyStep cannot catch — its mvn -N validate passes precisely because the rule never ran. The POM edit is spliced into the bytes the file already holds, at the source offsets PomDocument reads 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.
  10. CommitStep — commits the build change (Add claude-code-enforcer to the build), reported as commit:guard.
  11. AssetsStepCommitSteponly on --assets (assets on 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.
  12. VerifyStep — runs the detected build system's verification (a non-recursive mvn -N validate for Maven, the enforceClaudeMd task for Gradle, the .github/claude-md-guard.sh script for the fallback) so the freshly wired guard actually executes against the generated CLAUDE.md, failing the adoption locally if the file is missing or malformed rather than after the pull request lands.
  13. 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.pushurl override, since CloneStep deliberately leaves none in the checkout; git applies the override to the single invocation and stores it nowhere. Left out of a --dry-run pipeline, together with the step below it.
  14. PullRequestStep — opens a pull request from the branch with gh pr create, targeting the repository's default branch as the base. The pull request metadata is supplied through PullRequestOptions — 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. Like CommitStep it 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 a gh pr create that 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 list needs 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.

Architecture tests (ArchUnit)

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 packagesslices().matching(...).should().beFreeOfCycles() keeps the package graph acyclic in each module.
  • Loggers are constants — every org.apache.logging.log4j.Logger field must be private static final (the context module relaxes this to private final), because a logger is a shared, immutable, class-scoped collaborator.
  • *Exception types really are exceptions — any class whose simple name ends with Exception must be assignable to java.lang.Exception.
  • Abstract-prefixed names — a top-level abstract class must have a simple name starting with Abstract, 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 GeneralCodingRules forbid access to System.out/System.err, throwing generic exceptions, and using java.util.logging; libraries additionally must never call System.exit. The data module tightens this further, also rejecting the JDK's own System.Logger so all logging stays on log4j2. protogen-maven-plugin is the one exemption, and it has a rule of its own saying so: a Maven plugin reports through AbstractMojo.getLog(), which is what honours -q and -X and attributes a line to the plugin in the reactor output, so pluginLogsThroughTheMojoLog keeps log4j2 out of its production code and out of the plugin jar.
  • No raw stack traces — no class may call any Throwable.printStackTrace overload, because a failure must be reported through log4j2 rather than dumped to the console.
  • Public fields are immutable — every public field must be final, so a field that is part of a type's API surface cannot be reassigned from outside.
  • No legacy date libraryGeneralCodingRules forbid a dependency on Joda-Time, keeping date/time handling on the java.time API.

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 concrete source.db/source.file implementations; structure.internal is accessible only from structure; the reusable structure collections must not couple to data sources; every concrete *DataSource must implement IterableDataSource; the uniqueness core must not depend on its mcp adapter; and a layeredArchitecture pins 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 in structure may be synchronized, and structure may not depend on java.util.concurrent, so neither can silently suggest the collections are safe to share. Three more pin the network kill-switch to its documented shape: no method there may be synchronized, since a static synchronized method would lock the publicly reachable Switch.class; the LOCK it synchronizes on instead must be private static final; and its isOff flag must be volatile.
  • code/context — the finder/tree core must not depend on the mcp delivery package, and only that mcp package may build on the shared MCP scaffolding; every concrete *Serializer must honour the ProjectTreeSerializer contract.
  • code/protogen-maven-plugin — the reusable format package must not depend on the gen code generator that builds on it, and every concrete *Mojo must implement the Maven Mojo contract.
  • mcp-common — the McpTool SPI must stay an interface, every concrete *Tool must implement it, and the shared scaffolding must never call System.exit.
  • claude-code-enforcer — a layeredArchitecture pins the module's layers (text is the foundation, rule builds on it, and the feature packages definition/doc/mcp/settings build on rule without reaching sideways into one another), and every concrete *Rule must extend the shared ClaudeCodeEnforcerRule base.
  • adopt — the command runner layer must not depend on the step package, so the reusable command abstraction stays unaware of the adoption steps that build on it; and every concrete *Step in step must implement the AdoptionStep contract. The pipeline also carries the shared baseline in full, including that it reports failure by throwing AdoptionException and never calls System.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.

Integration tests

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.

MCP servers over a real transport

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.

The enforcer rules in a real Maven build

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:

  • EnforcerRuleBuildIT builds 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=warn going green while still logging, the HTML report landing on disk, and a baseline suppressing recorded violations.
  • RepositoryEnforcementIT runs this repository's own mvn -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.
  • ForeignRepositoryEnforcementIT points that complete configuration at eight real repositories shallow-cloned from GitHub, none of which has ever heard of this enforcer — among them a real AGENTS.md, a real .mcp.json and a real .claude/agents directory 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-rule passed / failed with message: lines are read back out of it. The checkouts are also asked, after all eight builds, to be exactly as git produced 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 against real repositories

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:

  • MultiRepoAdoptionIT adopts 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 a git clone that really ran twice into one workspace proves the checkouts.
  • ForeignRepositoryAdoptionIT adopts seven real repositories — google/gson, square/okhttp, anthropics/anthropic-quickstarts, anthropics/claude-code, github/gitignore, JakeWharton/timber and modelcontextprotocol/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 real CLAUDE.md, a real .claude directory, a project whose own files already sit where two of the starter assets go, a default branch called neither main nor master, 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 with ls-remote and 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.

Building

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

Releasing

In order to release a new version - X you need to:

  1. Change the revision property to X in root pom.xml
  2. Commit and push
  3. Check if all builds pass
  4. Release and mark as latest in GitHub

License

This project is licensed under the MIT License.

About

Claude Code enforcer and adopter, context engineering, data oriented tools, compile time safe protobuf Java code generation

Topics

Resources

Security policy

Stars

11 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages