From 2bf48cd34f7860640e13180824723535a62a74c1 Mon Sep 17 00:00:00 2001 From: William Callahan Date: Fri, 23 Jan 2026 01:19:50 -0800 Subject: [PATCH 1/4] fix(ci): checkout main branch explicitly in UpdateReadmeVersion workflow When triggered by release:published events, GitHub Actions checks out the release tag (detached HEAD) rather than a branch. This caused git push to fail with "You are not currently on a branch" error. Fixed by explicitly specifying ref: main in the checkout step, ensuring the workflow always operates on the main branch regardless of trigger. --- .github/workflows/UpdateReadmeVersion.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/UpdateReadmeVersion.yaml b/.github/workflows/UpdateReadmeVersion.yaml index 041545b..f97d1c3 100644 --- a/.github/workflows/UpdateReadmeVersion.yaml +++ b/.github/workflows/UpdateReadmeVersion.yaml @@ -20,8 +20,10 @@ jobs: update-readme: runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout default branch uses: actions/checkout@v6 + with: + ref: main - name: Resolve latest release version from Maven Central metadata id: resolve From 22f8e6a50e502c7af85f15dec188d4d9eab09ea9 Mon Sep 17 00:00:00 2001 From: William Callahan Date: Thu, 5 Feb 2026 14:16:40 -0800 Subject: [PATCH 2/4] docs(agents): codify rules and change contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repository’s agent guidance needed to be more explicit and linkable, with stable rule hashes that can be cited during reviews and automated changes. This formalizes additional non-negotiables (verification, SRP, LOC ceilings, dependency source inspection) and adds a single “code change contract” so new work follows consistent placement and splitting decisions. - Expand AGENTS rule summary and add missing rule sections/hashes - Add dependency source verification workflow (Gradle cache / sources JAR) - Add LOC ceiling and no-monolith decision rules (new file vs edit) - Add a contracts doc with a decision matrix and verification gates --- AGENTS.md | 71 ++++++++++++++++++++-- docs/contracts/code-change.md | 111 ++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 docs/contracts/code-change.md diff --git a/AGENTS.md b/AGENTS.md index 0c51d0e..0ede4fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,8 +13,14 @@ alwaysApply: true ## Rule Summary [SUM] -- [GT1a-d] Git & Permissions (elevated-only git; no destructive commands) -- [FS1a-g] File Creation & Type Safety (typed records, no maps, no raw types) +- [ZA1a-d] Zero Tolerance Policy (zero assumptions, validation, forbidden practices, dependency verification) +- [GT1a-h] Git & Permissions (elevated-only git; no destructive commands) +- [CC1a-d] Clean Code & DDD (Mandatory) +- [ID1a-d] Idiomatic Patterns & Defaults +- [DS1a-e] Dependency Source Verification +- [FS1a-h] File Creation & Type Safety (typed records, no maps, no raw types) +- [LOC1a-e] Line Count Ceiling (350 lines max; SRP enforcer; zero tolerance) +- [MO1a-g] No Monoliths (Strict SRP; Decision Logic; Extension/OCP) - [ND1a-c] Naming Discipline (intent-revealing identifiers only) - [AB1a-c] Abstraction Discipline (YAGNI; no anemic wrappers; earn reuse) - [CS1a-f] Code Smells / Clean Code (DRY; primitive obsession; magic literals) @@ -24,12 +30,47 @@ alwaysApply: true - [TS1a-d] Testing Standards (coverage mandatory; observable behavior; refactor-resilient) - [VR1a-c] Verification Loops (build/test/lint steps) +## [ZA1] Zero Tolerance Policy + +- [ZA1a] **Zero Assumptions**: Do not assume behavior, APIs, or versions. Verify in the codebase/docs first. +- [ZA1b] **Source Verification**: For dependency code questions, inspect `~/.m2` JARs or `~/.gradle/caches/` first; fallback to upstream GitHub; never answer without referencing code. +- [ZA1c] **Forbidden Practices**: + - No `Map`, raw types, unchecked casts, `@SuppressWarnings`, or `eslint-disable` in production. + - No trusting memory—verify every import/API/config against current docs. +- [ZA1d] **Mandatory Research**: You MUST research dependency questions and correct usage. Never use legacy or `@deprecated` usage from dependencies. Ensure correct usage by reviewing related code directly in `node_modules` or Gradle caches and using online tool calls. + ## [GT1] Git & Permissions - [GT1a] All git commands require elevated permissions; never run without escalation. -- [GT1b] Never remove `.git/index.lock` automatically—stop and ask the user. -- [GT1c] No destructive git commands (`git restore`, `git reset`, force checkout) unless explicitly ordered. -- [GT1d] Do not skip commit signing or hooks; no `--no-verify`. +- [GT1b] Never remove `.git/index.lock` automatically—stop and ask the user or seek explicit approval. +- [GT1c] Read-only git commands (e.g., `git status`, `git diff`, `git log`, `git show`) never require permission. Any git command that writes to the working tree, index, or history requires explicit permission. +- [GT1d] Do not skip commit signing or hooks; no `--no-verify`. No `Co-authored-by` or AI attribution. +- [GT1e] Destructive git commands are prohibited unless explicitly ordered by the user (e.g., `git restore`, `git reset`, force checkout). +- [GT1f] Treat existing staged/unstaged changes as intentional unless the user says otherwise; never “clean up” someone else’s work unprompted. +- [GT1g] Examples of write operations that require permission: `git add`, `git commit`, `git checkout`, `git merge`, `git rebase`, `git reset`, `git restore`, `git clean`, `git cherry-pick`. +- [GT1h] When in doubt whether a git command writes, treat it as write and request explicit approval. + +## [CC1] Clean Code & DDD (Mandatory) + +- [CC1a] **Mandatory Principles**: Clean Code principles (Robert C. Martin) and Domain-Driven Design (DDD) are **mandatory** and required in this repository. +- [CC1b] **DRY (Don't Repeat Yourself)**: Avoid redundant code. Reuse code where appropriate and consistent with clean code principles. +- [CC1c] **YAGNI (You Aren't Gonna Need It)**: Do not build features or abstractions "just in case". Implement only what is required for the current task. +- [CC1d] **Clean Architecture**: Dependencies point inward. Domain logic has zero framework imports. + +## [ID1] Idiomatic Patterns & Defaults + +- [ID1a] **Defaults First**: Always prefer the idiomatic, expected, and default patterns provided by the framework, library, or SDK (Java 21+, etc.). +- [ID1b] **Custom Justification**: Custom implementations require a compelling reason. If you can't justify it, use the standard way. +- [ID1c] **No Reinventing**: Do not build custom utilities for things the platform already does. +- [ID1d] **Dependencies**: Make careful use of dependencies. Do not make assumptions—use the correct idiomatic behavior to avoid boilerplate. + +## [DS1] Dependency Source Verification + +- [DS1a] **Locate**: Find source JARs in Gradle cache: `find ~/.gradle/caches/modules-2/files-2.1 -name "*-sources.jar" | grep `. +- [DS1b] **List**: View JAR contents without extraction: `unzip -l | grep `. +- [DS1c] **Read**: Pipe specific file content to stdout: `unzip -p `. +- [DS1d] **Search**: To use `ast-grep` on dependencies, pipe content directly: `unzip -p | ast-grep run --pattern '...' --lang java --stdin`. No temp files required. +- [DS1e] **Efficiency**: Do not extract full JARs. Use CLI piping for instant access. ## [FS1] File Creation & Type Safety @@ -40,6 +81,26 @@ alwaysApply: true - [FS1e] Domain has zero framework imports; dependencies point inward. - [FS1f] No generic utilities: reject `*Utils/*Helper/*Common`; use domain-specific names. - [FS1g] Domain value types: wrap identifiers, amounts, and values with invariants in records. +- [FS1h] File size discipline: see [LOC1a] and [MO1a]. + +## [LOC1] Line Count Ceiling (Repo-Wide) + +- [LOC1a] All written, non-generated source files in this repository MUST be <= 350 lines (`wc -l`), including `AGENTS.md` +- [LOC1b] SRP Enforcer: This 350-line "stick" forces modularity (DDD/SRP); > 350 lines = too many responsibilities (see [MO1d]) +- [LOC1c] Zero Tolerance: No edits allowed to files > 350 LOC (even legacy); you MUST split/retrofit before applying your change +- [LOC1d] Enforcement: run line count checks and treat failures as merge blockers +- [LOC1e] Exempt files: generated content, lockfiles, and large example/data dumps + +## [MO1] No Monoliths + +- [MO1a] No monoliths: avoid multi-concern files and catch-all modules +- [MO1b] New work starts in new files; when touching a monolith, extract at least one seam +- [MO1c] If safe extraction impossible, halt and ask +- [MO1d] Strict SRP: each unit serves one actor; separate logic that changes for different reasons +- [MO1e] Boundary rule: cross-module interaction happens only through explicit, typed contracts with dependencies pointing inward; don’t reach into other modules’ internals or mix web/use-case/domain/persistence concerns in one unit +- [MO1f] Decision Logic: New feature → New file; Bug fix → Edit existing; Logic change → Extract/Replace +- [MO1g] Extension (OCP): Add functionality via new classes/composition; do not modify stable code to add features +- Contract: `docs/contracts/code-change.md` ## [ND1] Naming Discipline diff --git a/docs/contracts/code-change.md b/docs/contracts/code-change.md new file mode 100644 index 0000000..89ea2c8 --- /dev/null +++ b/docs/contracts/code-change.md @@ -0,0 +1,111 @@ +--- +title: "Code change policy contract" +usage: "Use whenever creating/modifying files: where to put code, when to create new types, and how to stay SRP/DDD compliant" +description: "Evergreen contract for change decisions (new file vs edit), repository structure/naming, and domain model hierarchy; references rule IDs in `AGENTS.md`" +--- + +# Code Change Policy Contract + +See `AGENTS.md` ([LOC1a-e], [MO1a-g], [FS1a-h], [ND1a-c], [CC1a-d], [TS1a-d], [VR1a-c]). + +## Non-negotiables (applies to every change) + +- **SRP/DDD only**: each new type/method has one reason to change ([MO1d], [CC1a]). +- **New feature → new file**; do not grow monoliths ([MO1b], [FS1b]). +- **No edits to >350 LOC files**; first split/retrofit ([LOC1c]). +- **Domain is framework-free**; dependencies point inward ([CC1d], [FS1e]). +- **No DTOs**; domain records/interfaces are the API response types. +- **No map payloads** (`Map`, stringly helpers, map-based mappers) ([ZA1c], [FS1b]). + +## Decision matrix: create new file vs edit existing + +Use this as a hard rule, not a suggestion. + +| Situation | MUST do | MUST NOT do | +|----------|---------|-------------| +| New user-facing behavior (new endpoint, new domain capability) | Add a new, narrowly scoped type in the correct layer/package ([MO1b]) | “Just add a method” to an unrelated class ([MO1a], [MO1d]) | +| Bug fix (existing behavior wrong) | Edit the smallest correct owner; add/adjust tests to lock behavior ([MO1f], [TS1a]) | Create a parallel/shadow implementation ([RC1d]) | +| Logic change in stable code | Extract/replace via composition; keep stable code stable ([MO1g]) | Add flags, shims, or “compat” paths to hide uncertainty ([RC1e]) | +| Touching a large/overloaded file | Extract at least one seam (new type + typed contract) ([MO1b], [MO1e]) | Grow the file further ([MO1a]) | +| Reuse needed across features | Add a domain value object / explicit port / explicit service with intent-revealing name ([CS1a], [AR1c], [ND1a]) | Add `*Utils/*Helper/*Common/*Base*` grab bags ([FS1f]) | + +### When adding a method is allowed + +Adding to an existing type is allowed only when all are true: + +- It is the **same responsibility** as the type’s existing purpose ([MO1d]). +- The method’s inputs belong together (avoid data clumps/long parameter lists; extract a parameter record when needed) ([CS1b], [CS1c]). +- The method does not pull in a new dependency direction (dependencies still point inward) ([CC1d]). + +If any bullet fails, create a new type and inject it explicitly. + +## Create-new-type checklist (before you write code) + +1. **Search/reuse first**: confirm a type/pattern doesn’t already exist ([FS1a], [ZA1a]). +2. **Pick the correct layer** (web → use case → domain → adapters/out) ([AR1a]). +3. **Pick the correct feature package** (feature-first, lowercase, singular nouns). +4. **Name by role** (ban generic names; suffix declares meaning) ([ND1a-b]). +5. **Keep the file small** (stay comfortably under 350 LOC; split by concept early) ([LOC1a], [MO1d]). +6. **Add/adjust tests** using existing patterns/utilities ([TS1a], [TS1b]). +7. **Verify** with repo-standard commands (`./gradlew build`, `./gradlew spotlessCheck`) ([VR1a], [VR1c]). + +## Repository structure and naming (placement is part of the contract) + +### Canonical roots (Java) + +Only these root packages are allowed ([AR1a]): + +- `com.williamcallahan.applemaps.boot` +- `com.williamcallahan.applemaps.adapters` +- `com.williamcallahan.applemaps.cli` +- `com.williamcallahan.applemaps.domain` + +### Feature-first package rule + +All layers organize by **feature first**, then by role. + +Examples: +- `domain/model/place/...` +- `domain/port/place/...` +- `adapters/mapsserver/...` + +### No mixed packages + +A package contains either: +- Direct classes only, or +- Subpackages only (plus optional `package-info.java`). + +If you need both, insert one more nesting level. + +## Domain model hierarchy + +- Domain records are immutable value objects. +- Validation happens in constructors. +- Domain → API mapping happens once at the boundary. + +## Layer responsibility contract + +### Controllers / CLI + +Allowed: +- Bind/validate inputs. +- Delegate to use cases/domain services. +- Return domain records. + +Prohibited: +- Business logic. + +### Domain + +Allowed: +- Pure business logic. +- Invariants. + +Prohibited: +- Framework imports. +- Persistence details. + +## Verification gates (do not skip) + +- LOC enforcement: manual check / script ([LOC1c]). +- Build/test/lint: `./gradlew build` ([VR1a]). From c0f40284e448ff0d4f28468a15ad3f18ea471adc Mon Sep 17 00:00:00 2001 From: William Callahan Date: Thu, 5 Feb 2026 14:16:41 -0800 Subject: [PATCH 3/4] docs(mintlify): add docs config and page titles Docs pages currently lack frontmatter metadata and there is no Mintlify docs configuration to define navigation and site identity. This adds the Mintlify `docs.json` plus per-page titles so the documentation can be rendered and navigated consistently. - Add docs/docs.json with theme, name, and grouped navigation - Add YAML frontmatter titles to core docs pages - Include Contracts navigation entry pointing at the code-change contract page --- docs/authorization.md | 4 ++++ docs/cli.md | 4 ++++ docs/docs.json | 37 +++++++++++++++++++++++++++++++++++++ docs/tests.md | 4 ++++ docs/usage.md | 4 ++++ 5 files changed, 53 insertions(+) create mode 100644 docs/docs.json diff --git a/docs/authorization.md b/docs/authorization.md index c49f2e5..9b63c80 100644 --- a/docs/authorization.md +++ b/docs/authorization.md @@ -1,3 +1,7 @@ +--- +title: "Authorization (Apple Maps Server API)" +--- + # Authorization (Apple Maps Server API) To call the Apple Maps Server API, you must provide an Apple-issued **authorization token** (a JWT). diff --git a/docs/cli.md b/docs/cli.md index 57e4dd1..c6e9b67 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,3 +1,7 @@ +--- +title: "CLI" +--- + # CLI This repo includes a small CLI for running Apple Maps Server queries from your terminal. diff --git a/docs/docs.json b/docs/docs.json new file mode 100644 index 0000000..bc6239d --- /dev/null +++ b/docs/docs.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://mintlify.com/docs.json", + "theme": "mint", + "name": "Apple Maps Server SDK for Java", + "colors": { + "primary": "#007ec6" + }, + "navigation": { + "groups": [ + { + "group": "Getting Started", + "pages": [ + "usage", + "authorization" + ] + }, + { + "group": "CLI", + "pages": [ + "cli" + ] + }, + { + "group": "Testing", + "pages": [ + "tests" + ] + }, + { + "group": "Contracts", + "pages": [ + "contracts/code-change" + ] + } + ] + } +} diff --git a/docs/tests.md b/docs/tests.md index 93ba318..741fc99 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -1,3 +1,7 @@ +--- +title: "Tests" +--- + # Tests This repo has unit tests and an optional integration test that calls the live Apple Maps Server API. diff --git a/docs/usage.md b/docs/usage.md index 1655723..a87f4a6 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,3 +1,7 @@ +--- +title: "Usage" +--- + # Usage ## Quick start From 3134895ba2c5c98ccc1964d4f43e0f92a0bed671 Mon Sep 17 00:00:00 2001 From: William Callahan Date: Fri, 13 Feb 2026 20:22:07 -0800 Subject: [PATCH 4/4] ci: guard SNAPSHOT releases; publish snapshot versions --- .github/workflows/CI.yaml | 16 ++++++++------ .github/workflows/Release.yaml | 12 +++++++++++ README.md | 38 ++++++++++++++++++++++++++++++++++ gradle.properties | 2 +- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index c4b7061..6ecaa18 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -29,21 +29,25 @@ jobs: - name: Build and test run: ./gradlew build - - name: Extract snapshot version - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - id: snapshot + - name: Check if SNAPSHOT version + id: version_check run: | VERSION=$(grep '^VERSION_NAME=' gradle.properties | cut -d'=' -f2) - echo "VERSION=${VERSION}-SNAPSHOT" >> $GITHUB_OUTPUT + echo "Version: $VERSION" + if [[ "$VERSION" == *-SNAPSHOT ]]; then + echo "is_snapshot=true" >> $GITHUB_OUTPUT + else + echo "is_snapshot=false" >> $GITHUB_OUTPUT + fi - name: Publish SNAPSHOT (main only) - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.version_check.outputs.is_snapshot == 'true' env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - run: ./gradlew publishAllPublicationsToCentralPortalSnapshots -Pversion=${{ steps.snapshot.outputs.VERSION }} -x test + run: ./gradlew publishAllPublicationsToCentralPortalSnapshots -x test - name: Update dependency graph uses: gradle/actions/dependency-submission@v5 diff --git a/.github/workflows/Release.yaml b/.github/workflows/Release.yaml index 98ccc4b..f94a3c5 100644 --- a/.github/workflows/Release.yaml +++ b/.github/workflows/Release.yaml @@ -47,6 +47,18 @@ jobs: echo "VERSION=$VERSION" >> $GITHUB_OUTPUT echo "Release version: $VERSION" + - name: Fail if SNAPSHOT tag + shell: bash + run: | + set -euo pipefail + VERSION="${{ steps.version.outputs.VERSION }}" + if [[ "$VERSION" == *-SNAPSHOT ]]; then + echo "::error::Refusing to publish SNAPSHOT version '$VERSION' with the release workflow." + echo "::error::Create a non-SNAPSHOT release tag (e.g., v0.1.5) for Maven Central releases." + echo "::error::For snapshots, push a -SNAPSHOT version on main and publish via publishAllPublicationsToCentralPortalSnapshots." + exit 1 + fi + - name: Build and test run: ./gradlew build -Pversion=${{ steps.version.outputs.VERSION }} diff --git a/README.md b/README.md index d53b3de..eb41b97 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,44 @@ dependencies { ``` +### Snapshots + +Snapshots are published to Sonatype's snapshot repository (separate from Maven Central releases): + +```text +https://central.sonatype.com/repository/maven-snapshots/ +``` + +Gradle: + +```groovy +repositories { + mavenCentral() + maven { url "https://central.sonatype.com/repository/maven-snapshots/" } +} + +dependencies { + implementation("com.williamcallahan:apple-maps-java:0.1.6-SNAPSHOT") +} +``` + +Maven: + +```xml + + + sonatype-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + +``` + ## Configuration ### `APPLE_MAPS_TOKEN` (required) diff --git a/gradle.properties b/gradle.properties index 22fea59..0ee243e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ GROUP=com.williamcallahan POM_ARTIFACT_ID=apple-maps-java -VERSION_NAME=0.1.5 +VERSION_NAME=0.1.6-SNAPSHOT POM_NAME=Apple Maps Java POM_DESCRIPTION=Apple Maps Java implements the Apple Maps Server API for use in JVMs.