Imported from guestgraph/engine (
AGENTS.md). Install upstream withnpx skills add guestgraph/engine. Copyright stays with the author.
Shared conventions of the robertblust, guestgraph and companygraph organizations live in conventions/, vendored from robertblust/conventions at the release conventions.json names. Read them before writing or committing anything here.
conventions/WRITING.md— how we write: one voice, three registers, English and German.conventions/WORKING.md— how we work with git and GitHub.conventions/REPOSITORIES.md— the family: what each repository is and what pins what.conventions/WRITER.md,conventions/TRANSLATOR.md,conventions/EDITOR.md,conventions/BACKREADER.md,conventions/GLOSSARY.md,conventions/GERMAN.md— the four roles that make a text, the terms they keep and the German they write.
Everything below this block is this repository's own. sh conventions/conventions-sync check says whether the copy matches the release, sync brings it to the release the pin names, and sh conventions/conventions-check holds this repository's own Markdown to WRITING.md, and sh conventions/conventions-format to its one form, which fix writes. Edit a shared file in robertblust/conventions, never here.
The code-level rules of every guestgraph service on the Spring stack live in service-conventions/, vendored from guestgraph/service-conventions at the release service-conventions.json names: the parent build every pom.xml takes by path, the source rules, the architecture rules in src/test/java/ServiceRulesTest.java, the diagram script, the API generator regen-api that writes the one openapi.yaml the service serves from the sources named beside it, the workflow in .github/workflows/verify.yml, and this block. sh service-conventions/service-conventions-sync check says whether the copy matches the release, sync brings it to the release the pin names, and sh service-conventions/service-conventions-check says what of the list the service lacks. What every service has, whatever its stack, is SERVICE.md there. Edit a shared file in guestgraph/service-conventions, never here.
GuestGraph — working conventions
Open-source (Apache-2.0) guest identity graph. Spec-driven with spec-kit; the constitution at .specify/memory/constitution.md is non-negotiable (tenant isolation, immutable source records, never drop parseable data, explainable/reversible resolution, API-first RFC 9457, TDD on the resolution engine).
Build & verify
./mvnw verify # tests (Testcontainers, needs Docker), the rules, PMD, Spotless
./mvnw spotless:apply # fix formatting (google-java-format) — check fails otherwise
sh service-conventions/regen-er # after schema changes — CI checks ER-diagram drift
sh service-conventions/service-conventions-check # what of the list every guestgraph service has this one lacks
sh conventions/conventions-check # the prose
Code conventions
- Imports, not inline FQNs. Types are referenced by simple name with a proper
import — never
java.time.LocalDateinline in code. Enforced by PMD (UnnecessaryFullyQualifiedName,service-conventions/pmd-ruleset.xml) inverify. Exception: JPQL query strings, where FQNs are required syntax (enum literals, constructor expressions) — PMD doesn't look inside strings. - Formatting is google-java-format via Spotless; don't hand-format.
- Comments state constraints the code can't show; no narration.
- Mechanical guardrails live in three places, each with its job, and all three come vendored
from guestgraph/service-conventions at the release
service-conventions.jsonnames: Spotless (format) and PMD (source-level conventions) from the parent build every service'spom.xmltakes by path, ArchUnit fromsrc/test/java/ServiceRulesTest.java(tenantIdon every repo method or a justified@TenantAgnostic,@Query-only repositories, no CrudRepository, no ad-hoc EntityManager queries,JdbcClientonly in the classes the pin'sjdbcClientAllowednames, JPA confined topersistence). A rule that every service needs changes there, never here; a rule only the engine needs sits beside the shared files. - Packages are
io.guestgraph.engine, the family's root and the repository's name, with the endpoints, filters and error answers underapi, as in every guestgraph service. - Refusals are
ServiceExceptions from the vendored packageio.guestgraph.service, each with its slug, status and title, thrown where the refusal is decided and written by the shared advice; a filter refuses through the sharedProblems.write. No problem detail, status exception or advice is written here, and the service check'serror-shapeitem names the file that does. A new slug is added to the problems page on guestgraph.io before the engine answers it.
Architecture in one paragraph
The resolution engine (resolution package: ResolutionEngine, strategies, gates, operations) is pure JVM behind the GraphPort seam — table-driven scenario tests run it against InMemoryGraph, production wires PostgresGraph (JPA + MapStruct, immutable entities, bulk-update-only mutations). Everything is tenant-scoped; merges are recorded as append-only merge_events with matcher name + confidence + evidence and are reversible (unmerge) with steward splits persisted as negative match rules. New matchers implement ResolutionStrategy — do not redesign the engine.
Non-obvious pitfalls (all bitten before)
- Hibernate's camel-case naming maps trailing single capitals wrong (
recordA→recorda): name such columns explicitly with@Column. - Java
UUID.compareTo(signed longs) disagrees with Postgres uuid ordering: order UUID pairs bytoString()when a DB CHECK depends on it. @Servicebeans get no persistence exception translation — only@Repositorydoes.- Spring AOT generates
*__*classes intotarget/classes; ArchUnit imports must filter them (already done inServiceRulesTest). - Test harness truncates tables in
PostgresIntegrationTest.resetDatabase— add new tables there or every integration test fails on FK truncate errors. - Until the first release,
V1__core_schema.sql/V2__*.sqlmay be edited in place; local Flyway checksum mismatch →docker compose down -v. Additive-only after tagging. - The engine lives in schema
engine, and every pooled connection's search path is that schema alone. Apsqlsession or a script outside the pool sees no tables until it qualifies names or runsset search_path to engine—\dtshowing nothing is that, not a missing migration.
Documentation ownership (prevents drift)
Every fact has one owning file; everywhere else links to it. The ambiguity about who owns what is what causes drift, so the map is explicit:
| Fact | Owner |
|---|---|
| Constants, thresholds, algorithms | the code |
| Matching behavior | docs/matching.md, sectioned per matcher version |
| What a guest's profile says | docs/profile.md, sectioned per survivorship version |
| What counts as identity | docs/identifiers.md, sectioned per normalization version |
| What a source record is | docs/records.md |
| Where a guest stays | docs/timeline.md |
| What a stored guest id still means | docs/continuity.md |
| Which phases shipped; which services run and what they talk to | guestgraph/.github, profile/README.md |
| One slice's decisions | specs/NNN-*/ — frozen at merge |
| Cross-slice decisions, roadmap, deferred work | docs/roadmap-notes.md |
| API surface | specs/*/contracts/openapi.yaml, the records; served as the one generated src/main/resources/api/openapi.yaml, held to them by regeneration in CI |
| Why a reader should care | README.md — concepts, never values |
The edit test. Before writing a number, threshold, or algorithm name into prose, ask: if this changes, how many files must I touch? More than one → link instead of restating. This is why the README describes the weighted feature vector without naming a single weight.
Specs are frozen history. A merged spec records what was decided then. Never retro-edit one; corrections and amendments go forward into docs/roadmap-notes.md or the owning doc — slice 3 amended R4-1 there rather than rewriting slice 2's spec. Spelling and form are the two exceptions: a British form brought to conventions/WRITING.md, and a table's padding or a blank line brought to the family's Markdown form, change no decision, and the conventions job reads merged specs and the append-only documents below like everything else.
Three of the six concept documents are append-only, docs/matching.md, docs/profile.md and docs/identifiers.md, for one reason: a merge event permanently records the matcherName that decided it, a profile was computed under one survivorship rule, and a value was stored under one normalization rule, so old data stays interpretable only while the rule that produced it stays readable. Each new version gets its own section and the old one remains. The other three, docs/records.md, docs/timeline.md and docs/continuity.md, describe invariants rather than a rule that is superseded, so they are written forward and edited in place.
A second repository links, never restates, and that holds in both directions. The org profile at guestgraph/.github once drifted to "Core in development" while two slices had shipped, because it restated a roadmap living here. No CI in one repo can catch that. The two facts a visitor needs before opening any repository — which phases shipped, and which services run and what they talk to — are owned there now and linked from this README; restating either one here would be the same fault the other way round.
Process
- Slices follow
/speckit-specify→plan→tasks→implementon aNNN-*branch; roadmap-notes (docs/roadmap-notes.md) feed each slice's spec. - TDD is mandatory for engine logic: failing scenario tests first, pure JVM.
Checks
Four jobs, all required by the ruleset on main: verify, er-drift and service-conventions from the vendored workflow, the last holding the vendored copy to its release and the engine to the list every guestgraph service meets, and conventions, called from robertblust/conventions at the pinned tag and shown by GitHub as conventions / conventions. The prose check leaves out target, build output; .specify and .claude, spec-kit's templates and skills, which are tooling and not this repository's prose; and docs/superpowers, whose specs quote the very words it scans for. The feature specs under specs/ are this repository's own writing and are scanned. Everything about how to write and how to work with git is in conventions/.
