Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +32 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard against missing VERSION_NAME to avoid silent skips.

If VERSION_NAME is absent or empty, this step will quietly set is_snapshot=false and skip snapshot publishing. Adding strict mode + a non-empty check makes the failure loud and easy to debug. Tiny CI hygiene win. 🌱

✅ Suggested hardening
       - name: Check if SNAPSHOT version
         id: version_check
+        shell: bash
         run: |
-          VERSION=$(grep '^VERSION_NAME=' gradle.properties | cut -d'=' -f2)
+          set -euo pipefail
+          VERSION=$(grep -E '^VERSION_NAME=' gradle.properties | head -n1 | cut -d'=' -f2 | tr -d '\r')
+          if [[ -z "$VERSION" ]]; then
+            echo "::error::VERSION_NAME not found in gradle.properties"
+            exit 1
+          fi
           echo "Version: $VERSION"
           if [[ "$VERSION" == *-SNAPSHOT ]]; then
-            echo "is_snapshot=true" >> $GITHUB_OUTPUT
+            echo "is_snapshot=true" >> "$GITHUB_OUTPUT"
           else
-            echo "is_snapshot=false" >> $GITHUB_OUTPUT
+            echo "is_snapshot=false" >> "$GITHUB_OUTPUT"
           fi
🤖 Prompt for AI Agents
In @.github/workflows/CI.yaml around lines 32 - 41, The version_check step
currently sets VERSION from gradle.properties but will silently treat a missing
or empty VERSION_NAME as non-snapshot; modify the run block for the step with id
version_check to enable strict shell mode (set -euo pipefail) and add an
explicit non-empty check for VERSION (e.g., if [ -z "$VERSION" ]; then echo
"ERROR: VERSION_NAME not found or empty" >&2; exit 1; fi) before the SNAPSHOT
test, so the job fails fast and loudly when VERSION_NAME is absent.


- 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
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/Release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/UpdateReadmeVersion.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 66 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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<String, Object>`, 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 <artifact>`.
- [DS1b] **List**: View JAR contents without extraction: `unzip -l <jar_path> | grep <ClassName>`.
- [DS1c] **Read**: Pipe specific file content to stdout: `unzip -p <jar_path> <internal/path/to/Class.java>`.
- [DS1d] **Search**: To use `ast-grep` on dependencies, pipe content directly: `unzip -p <jar> <file> | 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

Expand All @@ -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

Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,44 @@ dependencies {
</dependency>
```

### 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")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent README updater from rewriting snapshot coordinate

Adding this -SNAPSHOT dependency example introduces a regression with the existing UpdateReadmeVersion workflow: its global regex replacement updates every implementation("com.williamcallahan:apple-maps-java:...") occurrence, so the next scheduled/release run will overwrite this snapshot line to the latest release version and break the snapshot instructions. Scope the replacement to the release-install snippet (or explicitly exclude the snapshot block) so docs stay correct after automation runs.

Useful? React with 👍 / 👎.

}
```

Maven:

```xml
<repositories>
<repository>
<id>sonatype-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
```

## Configuration

### `APPLE_MAPS_TOKEN` (required)
Expand Down
4 changes: 4 additions & 0 deletions docs/authorization.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down
4 changes: 4 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
title: "CLI"
---

# CLI

This repo includes a small CLI for running Apple Maps Server queries from your terminal.
Expand Down
111 changes: 111 additions & 0 deletions docs/contracts/code-change.md
Original file line number Diff line number Diff line change
@@ -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<String,Object>`, 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]).
37 changes: 37 additions & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
}
}
4 changes: 4 additions & 0 deletions docs/tests.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
4 changes: 4 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
---
title: "Usage"
---

# Usage

## Quick start
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Loading