Releases: lambda-twelve/one-record
Release list
1.0.0-beta7
Added
Model\GraphValidator: one validator for a whole logistics-object or
event graph, root and every embedded node, used byChangeApplier,
POST /logistics-objectsand the logistics-events endpoints alike, so no
ingestion path accepts what another refuses (adversarial review 10).
Fixed
- A change could introduce an embedded node of an unknown class, which then
escaped validation along with any property attached to it; a node of a
logistics object class; or a node of the wrong class for the property.
ChangeApplierrefuses the class up front and the validator judges the
rest (R10-001, R10-002). Comparernormalised only five of the twelve XSD integer types, so a
delete spelled asxsd:bytedid not match a storedxsd:integerand an
add of the same number was not a duplicate; it now takes the integer
family fromRdf\Xsd(R10-003).POST /logistics-objectsvalidated the root's properties only; embedded
nodes, property kinds and ranges are now validated (R10-004). Posted
events are validated the same way, embedded locations included (D10-001).
One exception stays deliberate: a nested logistics object in a POST body
(the spec's own example A2) is still accepted and kept embedded, since
splitting it into an object of its own is not implemented (spec question
33); a change or an event may not introduce one.- An
xsd:dateoffset is bounded to 14:00 like adateTime's (D10-002). - An embedded node a client identified itself (an
https:orurn:id,
NE:ONE'sneone:scheme) escaped validation on creation and on posted
events, could not be addressed by a change afterwards and was never
cleansed when unlinked. Every non-root subject of a graph is now an
embedded node: the validator judges it,withEmbeddedIds()gives it a
server-mintedinternal:id when the object is stored, and the change
applier addresses and cleanses it whatever id it carries (adversarial
review 11). - A typed link, a reference that states the class of what it points to and
nothing else, escaped validation when its class was unknown to the
ontology (R12-001), and an unrelated change removed its class as an
orphan (R12-002). Its class must now exist, as a class of the data model
or as a code list, and whatever a reachable node links to is kept. A
type-only node with an id of its own is a reference by design, not an
embedded node: a change cannot describe it in place, and the guide says
how to embed instead (R12-003). - A code list did not count as a class when a property's range was checked,
so a unit typed with the wrong list, or a code list where a class was
expected, passed (R13-001). It counts now, and an untyped member IRI of
the wrong list is caught by its IRI when the property expects a list. - A nested logistics object that creation had accepted (spec question 33)
made every later change to the object fail, since the change applier
judged the finished graph without that allowance (R14-001). It is allowed
there now; a change still cannot introduce one, and the type of any node,
embedded nodes included, can no longer be changed through a change. - A language-tagged literal skipped the datatype range check, so
"many"@en
entered an integer property (R14-002). Tagged text now fits a string range
and nothing else. - The bulk event route dropped a malformed
cargo:eventForvalue and
reported no failure (R14-003); it answers 400 like the single-object route. - A non-string or empty
@idon creation was treated as absent and a URI
with a space became a 500 (R14-004); every malformed@idis a 400. JwksKeyResolverignoredkey_ops; a key published for encryption only
could verify tokens (D14-001). A presentkey_opsmust includeverify,
and the selection policy is in the guide. Akey_ops,useoralg
member that is present but malformed,nullincluded, now fails closed
as the guide says, and a skipped key is logged with its reason (R15-001,
D15-001).
1.0.0-beta6
Added
ServerConfig::problems(array $settings): what is wrong with a set of
settings, in plain sentences, without constructing anything, for a host's
status page on an install that is not configured yet. The constructor
throws the first of them.tools/consumer-smoke.phpand a CI job that installs the package into an
empty project without development dependencies and exercises the runtime.Server\Deprecationslogs, at notice level, every deprecated term an
object carries when it is created or revised, as the compatibility guide
promised.
Changed
-
ActionRequests::create()queues the Pending notification before
dispatchingActionRequestCreated, and every decision queues its
notification before its event, so a listener that decides a request
synchronously cannot put its decision's notification ahead of the state it
decided;create()and every decision return the stored request, which a
listener may have advanced.DataHolder::change()and the holder's HTTP
change path no longer decide a request a creation listener already decided;
ChangeFailedis also thrown when such a listener rejected it
(adversarial review 7, R7-001). -
An integral
xsd:doubleis written as an explicit value object, since a
JSON number without a fraction reads back asxsd:integerin any JSON-LD
processor; the client uses the package encoder (R7-002). -
The expander applies a term's datatype coercion to native numbers and
booleans, and an explicit value object without@languageis a plain
string under a default language (R7-003). A term definition never expands
a document-relative@id(R7-004). -
JWT time claims are NumericDate with their fraction or an absolute RFC 3339
instant with a zone; anything else present is refused with
JwtException::INVALID_CLAIMrather than read as absent (R7-005). -
A posted logistics event may name only the object it is posted on in
cargo:eventFor, however many values and in any order (R7-006). -
Changes validate literals by each datatype's own grammar and against the
property's range, as the checked builder does (R7-007). -
DeliveryVerdictrejects a request PSR-18 reports as unusable
(RequestExceptionInterface) instead of retrying it (R7-008). -
The token endpoint refuses two client authentication methods in one
request and repeated form parameters withinvalid_request(R7-009). -
Servicesvalidates against the newest configured data model version
(ServerConfig::validationModel()), as the configuration documented; a
server configured for 3.2 alone refuses 3.3-only terms (D7-001). -
Claims::expiresAt(),notBefore()andissuedAt()return?float
instead of?int, so a fractional NumericDate keeps its fraction. A
consumer that needs whole seconds decides its own rounding, for example
(int) floor($claims->expiresAt())beforeDateTimeImmutable::setTimestamp();
understrict_typespassing the float directly is aTypeError. -
DataHolder::subscribe()honours a creation listener's decision instead of
accepting a second time (R8-001). Range checks follow XSD derivation
(Rdf\Xsd): anxsd:intsatisfies anxsd:integerrange, any integer or
decimal type satisfies a double, and bounded integer types are checked
against their bounds; the builder and the change applier share the rules
(R8-002). A native number or boolean under an@id-coerced term expands to
the value it is rather than being refused (R8-003). The JWT verifier
compares claims against the clock with its fraction (R8-004), refuses a
presentnullor malformednbf/iat, and validates the hour, minute,
second and offset ranges of an RFC 3339 claim before parsing it (R8-005). -
ActionRequestStore::save()states its envelope: the server saves a
request once and moves it on withtransition(); a latersave()under
the same IRI may replace status, history and errors, while type, payload,
objects, requester and request time are fixed by the first save. The
contract no longer asks a store to move a re-saved request to another
object (beta5's test is withdrawn), since the SDK never does that and two
hosts' query columns had been caught between the two readings (Laravel
integration review, round 3). -
AccessDelegationkeeps each permission, delegate and logistics object
once, in order, so a store projecting one row per (request, object) never
sees a repeat.
1.0.0-beta5
Added
Client\TokenEndpointException(extendsClientException) with the status
and the OAutherrorcode, thrown byClientCredentialsTokenProviderwhen
the token endpoint answers without a token.- The
ActionRequestStorecontract checks that replacing a request under the
same IRI moves it to the object it now concerns, inauditTrail()and
pendingChanges().
Fixed
Client\DeliveryVerdict::of()walks the chain of previous exceptions, so a
transport failure the SDK client wrapped in aClientException, or a
token-endpoint outage, isRetryrather thanReject; refused credentials
stay final (Drupal integration review, round 3).
1.0.0-beta4
Added
Spi\Volatile, a marker for stores with nothing to roll back; the
in-memory stores carry it and the identity-unit-of-work warning is decided
by it rather than by class name.ServerBuilder::check(Services)returns
the same findings for a host's status page.Testing\RacingActionRequestStore: stages a decision lost to another
worker, outright or through a host callback that flips its own row first.Testing\FixedClock::set()for jumping to an absolute instant.Client\DeliveryVerdict: the retry classification every outbox worker
needs (Retryfor transport failures, 5xx, 408 and 429;Reject
otherwise), and a guide section specifying the outbox row, claim and
outcome model two hosts converged on.
Changed
ActionRequestStatusChangedand the status notification fire after the
decision's side effects (grants, revision, revocation) are in place, once
per decision, with the status the request had before it; a change that
fails to apply reportsFailedfromPendingeven though the store
recordedAcceptedin between (Drupal integration review, round 2).
1.0.0-beta3
Changed
- Every decision on an action request stores its new status (the
compare-and-set) before any side effect: grants, the new revision,
notifications. A decision that loses the race against another worker now
writes nothing even under a host without a transactional unit of work; it
used to commit the grants first (Laravel integration review, round 2). Serviceslogs a warning when noUnitOfWorkis given and a store is not
one of the SDK's in-memory ones, since operations are then not atomic.- The contract traits' fixture constants are prefixed (
CONTRACT_OBJECT,
CONTRACT_PARTNER, ...) so a host test case with constants of its own can
use the traits. NotificationOutboxand the SPI guide state whenenqueue()runs relative
to the unit of work, that delivery is at least once, and that PSR-14
listeners run inside the unit of work before the fan-out.
1.0.0-beta2
Added
Testing\Contract\*ContractTests: every store contract is also a trait,
for hosts whose test cases must extend a framework base class (Testbench,
KernelTestBase) and so cannot extend the abstract contract; the abstract
class is now the trait on a bareTestCase, so the two cannot drift.
Changed
-
LogisticsEventStorestates that every event it receives carries
cargo:eventDate(the server and the checked builder both refuse one
without), so stores need no semantics for date-less events. -
A status-change notification for an action request over several logistics
objects (an access delegation, typically) no longer names an arbitrary first
object inapi:hasLogisticsObject, whose cardinality is at most one; it
names none and the request inapi:isTriggeredBylists them all (spec
question 32, IATA-Cargo/ONE-Record#437). Type subscriptions keep matching
declared@typevalues only, now recorded as spec question 31
(IATA-Cargo/ONE-Record#412).
1.0.0-beta1
First release: the server and the client for API 2.2.0 and 2.3.0 with data
model 3.2 and 3.3, the compliance collection green for both editions, the
NE:ONE interoperability suite green, and five independent adversarial review
rounds folded in. Public API changes remain possible between betas and are
recorded here.
Added
- Repository skeleton: Composer package, PHPUnit 11, PHPStan (level max) with an
SDK-boundary rule, php-cs-fixer (PER-CS 2.0), DDEV configuration, GitHub
Actions, MkDocs documentation site. Spec\Edition,Spec\ApiVersion,Spec\DataModelVersionand
Spec\Namespaces: the supported editions (2025-07: API 2.2.0 / data model
3.2; 2026-07: API 2.3.0 / data model 3.3) and the IRIs ONE Record is built
from.Rdf\Graphand its terms (Iri,BlankNode,Literal,Triple): the RDF
layer everything else works on.- Generated vocabulary (
Vocabulary\Generated): every class, property and
named individual of the cargo and API ontologies and every code list, merged
across both editions with per-termsince/deprecatedIn/removedIn
metadata, plusVocabulary\Vocabularyfor runtime questions (accepted
properties per class, logistics-object classes, most specific type, code-list
membership) and version-limited views. bin/generate-vocabulary: regenerates the vocabulary from IATA's ontologies
at pinned commits; CI fails on a diff.JsonLd: the restricted JSON-LD processor ONE Record needs.Expander
turns a compacted document into triples (inline object contexts with
prefixes,@vocab,@base,@languageand@type-coercing term
definitions;@id,@type, value objects, arrays, embedded objects and
references) and rejects everything outside that subset with a message
naming the construct and its path.Writercompacts a graph back to
deterministic JSON-LD.Comparerdecides graph isomorphism modulo blank-node
labels, with embedded-object IRIs (internal:,neone:) treated as blank
nodes and numeric literals normalised;Diffreports the differing triples.Model:LogisticsObject,LocalGraph(objects linked by local key, resolved
to URIs with anIriMinter;UuidIriMintermints deterministic UUIDs under a
base URL),ObjectBuilder/Embedded/Valuesbuilders validated against the
ontology (with an optional data model version ceiling), and
EmbeddedIdMinterfor the spec's stableinternal:<uuid5>embedded ids.Change: theapi:Changemodel with JSON-LD read/write,ChangeBuilder
(diff two versions of an object into ADD/DELETE operations, editing embedded
objects in place or replacing them via blank nodes) andChangeApplier
(apply a change atomically with the spec's rules: revision check, no event
edits, deletes before adds, ontology validation, orphan cleanup).Api\Error,Api\ErrorDetail,Api\Severityvalue objects.Auth:Rs256Verifier(RS256 only, keys from a resolver, issuer/expiry/
not-before/audience checks),Rs256Signer,StaticKeyResolver,
JwksKeyResolver(PSR-18 + PSR-16, refresh on unknown key id),Jwk(RSA
JWK to PEM),JwtAuthenticatorfor the server'sAuthenticatorSPI, and
TokenEndpoint, a PSR-15 OAuth 2.0 client-credentials endpoint with a
ClientCredentialsVerifierSPI and an in-memory reference implementation.Server: the PSR-15 ONE Record server.OneRecordServernegotiates the
API version, routes, authenticates and answers every failure as an
api:Error;ServerConfigholds base URL, base path, data holder,
versions, languages and limits. Endpoints for server information, logistics
objects (read with?at=and?embedded=, create, change, verify), audit
trail, logistics events (single and 2.3 bulk), notifications,
subscriptions, access delegations and action requests.ActionRequests
implements the lifecycle (accept applies changes, grants delegations,
rejects stale pending changes);DataHolderis the host's PHP API
(create,update,publish,announce,accept/reject/acknowledge/
revoke,subscribe,forget);Notification\Fanoutqueues notifications
in the outbox. PSR-14 events for created/revised objects, received events
and notifications, and action-request changes.Server\Spi: the interfaces a host implements (LogisticsObjectStore,
LogisticsEventStore,ActionRequestStore,SubscriptionStore,
AccessDelegationStore,NotificationOutbox,Authenticator,
AccessPolicy) with in-memory implementations inServer\InMemoryand
InMemoryServerwiring them all;SystemClock.Api: value objects for every API document (Subscription,
AccessDelegation,Verification,Notification,ServerInformation,
ActionRequest,Collection,ErrorDocument) andSpec\ApiFeatures,
the table of properties gated by API version.- Compliance collection (
tests/Compliance/): a newman collection generated
from the specification's examples with assertions from its MUST tables, run
in CI againstbin/servefor API 2.2.0 and 2.3.0 (ddev compliancelocally). bin/serve: the in-memory server under PHP's built-in web server with an
OAuth 2.0 token endpoint, for development and the compliance collection.- Review round with the Laravel and Drupal wrapper teams:
Server\Spi\UnitOfWork(the host's transaction boundary around every
mutating request and holder operation);Server\Spi\IdGeneratoras an
interface withUuidIdGenerator; anidonOutboundNotification;
SubscriptionStore::offer()/withdraw(),ActionRequestStore::accepted(),
LogisticsEventStore::eraseFor(),AccessDelegationStore::eraseFor()and a
scopedDataHolder::forget();Server\GrantAccessPolicy(public grants
stored as grants toEVERYONE;InMemoryAccessPolicydeprecated);
millisecondSystemClock,ActionRequest::toStorageJsonLd(),
LogisticsEvent::fromStored()andObjectBuilder::ofEvent();
Auth\Jwt\ChainKeyResolver, an optional, fault-tolerant cache on
JwksKeyResolver,Auth\JwksEndpoint;ServerBuilder::routes(); objects
created over HTTP fan out to subscribers; and theTestingnamespace with
the doubles andTesting\Contractstore contract tests. - Adversarial review round (2026-10-03), one regression test per finding:
client credentials bound to the partner's origin (additionalOriginsfor
multi-host partners);ActionRequestStore::transition()compare-and-set
and lifecycle decisions made on stored state; shared visited set in graph
collectors; strictxsd:dateTimeparsing (a malformed expiry is a 400, not
"no expiry"); the client refuses to request an expiring delegation from a
2.2 partner; fan-out consults the access policy before disclosing a body;
DataHolder::change()/update()/publish()throwChangeFailed;
language-tagged literals refused in changes; exactxsd:decimal
comparison; sound canonical blank-node labels; order-independent change
validation; discovery falls back through API versions on 406; publishers
may query offers for their own objects; bulk events validated like single
ones; client checks the answer is about the requested object; typed links
rewritten in historical reads; RFC 6749 form-encoding of Basic
credentials;q=0exclusions and configured body versions honoured;
isTriggeredBynever names an organisation; global event IRI uniqueness;
malformed?at=and non-finite numbers answer 400;Idempotency-Keyon
sent and received notifications; JWKS refresh cooldown; only the requestor
or the policy may revoke; reference stores keep snapshots; position-aware
JSON-LD compaction, RFC 3986@baseresolution, term-scoped coercion and
labelled blank roots. - Second adversarial round (2026-10-03), again one regression test per
finding: canonical blank-node labels by exhaustive individualisation search
(ComparisonBudgetExceededwhen a graph is too symmetric, instead of a
guess); the writer uses a term alias only when its coercion fits the values
and never writes a node id as a bare prefix; historical reads with
embedded=trueaccepted by the client; a subscriber may revoke a
SubscriptionRequest a third party created (spec question 30); the in-memory
event store and outbox keep snapshots, with contract tests; changes may not
write API-namespace properties and final validation tolerates only the two
revision properties; content negotiation evaluates every served version
against the most specific matching range;xsd:dateTimeranges checked,
24:00:00accepted as the next midnight; dot-segment removal for
network-path references; the JWKS refresh cooldown kept in the shared
cache; a shared embedded node introduced once in a change; a store's status
conflict answers 409. - Third adversarial round (2026-10-03): the change builder plans deletions
over the whole before/after graphs, so a shared embedded node is edited
once, keeps its triples while any link reaches it, and loses them once when
none does; a node under several root properties reports every one of them
as changed; every compact key the writer emits must read back as its IRI,
so shadowed@vocabnames and wrong coercions are skipped instead of
dropping a predicate or turning a string into an IRI; the client names both
identities it accepts before a flattened answer's root is chosen; in-memory
outbox reads hand out copies and the outbox contract states ownership. - Fourth adversarial round (2026-10-03): the change builder now builds a
correspondence between old and new embedded nodes over the whole graphs
before emitting any operation (unchanged content first in every slot, then
one-to-one in-place pairs), so an unchanged branch is never edited to serve
another, shared nodes split and merge exactly as the target does, and
topology-only changes are changes; term definitions in a written@context
use prefixes only and the reader resolves definitions...