Decisions¶
Read this before reversing an architectural choice. Each entry says what was decided and why.
A framework-agnostic SDK, wrapped by framework packages¶
The server and client live in one package that depends only on PSR interfaces. Laravel, Drupal and any other host integrate through small wrapper packages. Why: Lambda Twelve needs the same implementation in more than one host, and a public package that pulls in a framework would be useless to everyone else. The boundary is spelled out in SDK boundary and enforced by a PHPStan rule.
Built from the specification, with NE:ONE as an oracle only¶
The code is written from IATA's MIT-licensed specification and ontologies. NE:ONE is run in the interoperability suite to compare outputs as RDF graphs, and read to understand behaviour where the specification is unclear, but no NE:ONE code is copied: its licence (OLFL-1.3) is not compatible with releasing this package under Apache-2.0.
Two API editions and two data model versions, together¶
API 2.2.0 and 2.3.0, data model 3.2 and 3.3 are supported at the same time, negotiated per request on the server and per partner in the client. Why: both editions are endorsed, partners run either, and 2.3/3.3 are supersets, so the cost is a version table and per-term metadata rather than two code paths. See API and data model versions.
Our own compliance collection¶
IATA's Postman collection is documentation: it carries no test assertions and placeholder bodies. The package ships its own newman collection built from IATA's request set and example bodies with assertions taken from the specification's MUST tables, run for every supported API version in CI.
A restricted JSON-LD processor, written here¶
ONE Record uses a small, predictable subset of JSON-LD. Implementing exactly that subset (and rejecting the rest clearly) is smaller, faster and safer than depending on a general processor, and the maintained PHP ones are old. The subset is documented in JSON-LD subset.
RS256 only, nothing negotiable from the token header¶
The JWT verifier accepts RS256 and nothing else; issuer, expiry, not-before and (optionally) audience are checked; keys come from a resolver, never from the token. Why: the ONE Record security model needs one algorithm, and every JWT weakness in the wild starts with trusting the header.
Forgetting data is a first-class host operation¶
The API has no delete for logistics objects and keeps an audit trail. Real deployments still have data-protection obligations, so the SDK makes "forget" explicit: access is closed at once and the store is asked to erase. The wrapper decides when.
First release is 1.0.0-beta1¶
The first tagged release will be 1.0.0-beta1, not 0.1.0. When the roadmap is complete the package has feature parity with a working implementation (NE:ONE) on both API editions, a compliance collection run per edition and an interop suite that compares graphs as RDF; a 0.x number would signal "experimental" to someone evaluating it, which would be false. "Beta" is the honest label for what is actually left: use by the Laravel wrapper and by partners may still change the public API, and betas allow that. 1.0.0 follows once a wrapper has run against a partner without API changes.
Adversarial review rounds are folded in, finding by finding¶
Before the first beta an independent adversarial review (2026-10-03) was
run against the full working tree. Every confirmed finding became a
regression test named by its id (AdversarialFindingsTest and the unit
suites) and the smallest correction that removes the cause, not the
reproducer; findings that are really unanswered questions in the
specification went into the spec questions register
(25 to 29) with the interim choice stated as such. Three rules came out of
it that now hold everywhere: a client's credentials go only to the origin it
was built for; lifecycle decisions are compare-and-set on the stored state,
never on a caller's snapshot; and rendering a document for an older edition
may drop a property, but requesting an authorisation may never silently
widen it.
A second round on the remediated tree found no new high findings but showed
that four first-round fixes had been too narrow or too wide (R2-003, R2-004,
R2-006, R2-012) and that the canonical labelling was still not a canonical
form (R2-001). Two further rules follow. A fix is checked against the
reviewer's reproducer and against the neighbouring option it did not
exercise (historical and embedded, excluded and generic ranges, metadata
present and metadata written), because a patch shaped to one reproducer is
how the regressions arrived. And where a correct answer needs a search, the
search is exhaustive under a budget and the budget's exhaustion is an error
the caller sees (ComparisonBudgetExceeded), never a silent fallback to a
heuristic.
The third round found the second's shared-node fix had only covered
introduction and whole-subtree removal, and that the writer's alias fallback
could still be shadowed by a @vocab term. The lesson generalises: an
invariant about a graph (one node however many links, a key means what it
expands to) is decided on the whole graph, not inside the recursion that
happens to meet one link or one key. Graph-wide decisions now live in one
place each: ChangeBuilder::deleteUnreachable() for what a change removes,
Context::keyCandidates() for what a key may be.
The fourth round showed that deciding deletions graph-wide was still not enough while the pairing of old and new nodes happened inside the recursion. The builder now has two phases, a correspondence over the whole graphs and then emission from it, and the rule is stated plainly: identity is decided before any operation is written. The same round found that the writer validated keys against the finished context while the reader built definitions against prefixes alone; both sides now meet at the definition, written with prefixes only and read through other terms when it names them.