Files
ng-eventually/.project/rules/nextgraph/no-divergence-from-nextgraph.md
T
Sylvain Duchesne 4e5b703949 feat: ancre la version que les citations de provenance decrivent
Une citation nomme un chemin et un symbole, jamais une version. Or le
checkout contre lequel toutes les citations `engine/` et `sdk/` se
resolvent declare des versions inferieures a celles que ce depot
installe, donc une citation prouve "c'etait vrai a ces versions-la" la
ou un lecteur lit "c'est vrai de ce qu'on construit".

La version est une propriete du CHECKOUT, pas d'une citation : aucune
des annotations n'est touchee. Elle est enregistree une seule fois, dans
l'`ANCHOR` de provenance.test.ts, a cote du resolveur qui s'en sert.

Ce que le mecanisme verifie maintenant, aux DEUX bouts de l'ecart, parce
qu'un enonce d'ecart peut perimer par l'un ou l'autre et a deja perime
par le bout eloigne : les versions que le checkout declare (a vide quand
le checkout est absent, comme la verification de resolution), celles que
les manifestes de l'espace de travail epinglent, et celle qui est
reellement installee sous packages/polyfill.

Pas un nouveau `kind` : les kinds classent la preuve d'UN symbole,
l'anciennete de toute la preuve appartient a l'arbre.
2026-09-24 14:55:10 +02:00

11 KiB

name, summary, group, scope, created
name summary group scope created
no-divergence-from-nextgraph Never diverge from NextGraph — ask "does the target do this?" FIRST, and only then how badly a caller would be misled nextgraph local 2026-08-27

No divergence from NextGraph

Rule: Never implement anything that diverges from NextGraph. At every choice — a published signature as much as an internal mechanism — the first question is does the target do this? Verified in nextgraph-rs, or stated by its author. If the target does it differently, we do it their way, whatever that costs. Only where the target answers nothing at all do we invent, and then deliberately, documented, and named as a bet.

"Known" is narrow: read at the source, or stated by its author. Never inferred from what an npm package happens to expose, and never inferred from an absent implementation — "the engine does not do X" says nothing about whether the target will.

A definition is a fact; its being unwired is an absence. The two arrive together and the absence is the louder one, which is how it wins arguments it should lose. A type the target has declared but nothing constructs yet is the strongest thing to align on — implementing it is not inventing, it is realising a shape the target wrote down. Read the declaration and let the silence around it say nothing.

That distinction has its own place in the taxonomy below, because collapsing it into "invention" makes us believe we are further from the target than we are — and then a later reader, seeing "our own idea", feels free to change it.

The second question measures gravity, not permission: would an application coding against this have to unlearn it? Use it to rank what to fix first, and to judge an unavoidable divergence. Never use it to authorise one.

Why the order matters — it is the failure this rule was rewritten for. readUnion folded every triple of a document into one bag keyed by the document, so two entities written under two subjects came back merged and one written under another subject came back relabelled, silently. Asked the second question first, the answer was ambiguous: one entity per document is good placement practice anyway, so what exactly would a caller unlearn? The reasoning stalled there for hours. Asked the first question, it took one look: level 1 returns real subjects, and level 3's ORM carries @id and @graph on every object and generates the former when omitted — several objects per graph is the designed case. Divergence, done, no debate about perception.

Earlier instances, all of the same shape: "every document has a native inbox", written from general reasoning, false, and already an implementation; and a per-document inbox pointed at its owner's inbox to absorb a measured cost, emulating a many-to-one relation the target cannot express.

The pressure to deviate never announces itself as one. It arrives as a cost, a latency, an ergonomic wrinkle — all real, all legitimate. That disguise is what makes it dangerous. When shape and cost conflict: keep the shape and attack the cost elsewhere (the lever is usually who pays and when). If the cost is genuinely unsolvable, say so rather than bending the model quietly.

How to apply. The tells, each of which has produced a real hole here:

  • A recommendation enforced by the code. Ours may guide placement; it may not make the other arrangement invisible. A read path that silently normalises what it did not expect is the signature.
  • A symbol that makes the caller handle what the target will never hand it — a document's key, an inbox address, a store id, its own identity.
  • A named exception stops being one the moment it is published. A door documented as "only this internal caller uses it" is a door any application can open; the note is not a mechanism. Move it out of the published surface instead.
  • A symbol kept because it was already there is not a decision. At every surface change, re-ask whether an application still needs each neighbouring symbol.

When a divergence is genuinely unavoidable it must be deliberate, documented and invisible to the caller. What is forbidden is the silent one, adopted because it was convenient.

Every symbol says where it comes from

Answering does the target do this? once, in conversation, does not survive the week. So each symbol carries its own provenance, beside the code rather than in a document that drifts: the level that answers (1 the engine, 2 the binding, 3 the ORM), the path:symbol it cites — a symbol in nextgraph-rs, or, for the one kind that has nothing upstream to cite, the record in this working copy — and one kind from a closed set.

kind meaning
passthrough upstream's own symbol, same name and shape
aligned our name or ergonomics, upstream's semantics and model
declared-not-wired upstream defines the shape; nothing constructs it yet; we implement it
announced-not-written upstream's developers have stated this is the direction; nothing in the tree carries it — not a definition, not even a name
reserved-name upstream has declared a NAME with no shape behind it — it binds nothing, so building on it is a bet
invention nothing at any level answers — a deliberate, documented bet
divergent upstream does this, and we do it differently

A citation names a symbol, not a version - and the checkout is behind. Every citation into engine/ and sdk/ resolves against one sibling checkout, which declares older releases than the packages this repo installs. So a citation proves "this was true at the cited releases", never "this is true of what we build against". The version is a property of that checkout, not of a citation, so it is recorded once - the ANCHOR in packages/polyfill/test/provenance.test.ts - and checked at both ends: the releases the checkout declares, and the ones the workspace pins and installs. Both, because a gap statement goes stale from either end, and it went stale from the far one already: the packages moved and the checkout, which cannot follow, was left describing releases nothing here installs.

The consequence is a rule about evidence, and it has already been broken here: a claim about a shipped release cannot be refuted by reading the checkout. Doing so refutes it against the older releases instead, which is a different statement. The tell is a sentence of the form "the published package declares X, but upstream foo() still does Y" - foo() at the cited release, and that says nothing about the release in the build. Worse, the usual shape of Y is an ABSENCE, and inferring from an absent implementation is what the top of this rule forbids: the pinned engine building its ORM config from json!({}) behind a // TODO was read as "the engine discards the config" and written into two leaves, when the declaration it feeds is complete, every field of it is consumed downstream, and the only missing hop is a binding parameter the shipped binding already has. Settle a claim about a release against that release's own shipped artifact, and say plainly when the artifact cannot settle it.

A test requires the annotation on every published symbol; it cannot check that the citation is true, and that is fine — a citation is refutable by a reader in seconds, prose is not. It does check that the citation still resolves, which a reader cannot: into nextgraph-rs when that sibling checkout is present — absent, it asks nothing rather than going red for a reason that would only teach people to skip the suite — and into this working copy always, for the one kind whose record lives here. The classification also applies to unpublished code that claims to resemble the target: the mistake this section exists for was made on an internal function, and being unexported protected nobody.

An announced target has no file to cite, and that is not a reason to call it ours. announced-not-written covers the case the other kinds leave out: the developers have said what the direction is, and the tree carries nothing yet — no definition, not even a name. It is the strongest justification available there, because deviating toward what the target has announced is alignment, and it is the rest of the surface that will have to catch up. The two moves it replaces are both bad: citing an approximate symbol, which is the readUnion failure — a true citation attached to a false claim — or declaring invention, which understates the alignment and tells a later reader the shape is ours to change when it is not.

Being the strongest, it is the hardest to assert. It names who stated it and when (announced-by=<who>@<YYYY-MM-DD>), and its ref is not a symbol upstream — there is none — but the document in this working copy where the statement is on record, plus the announced name it is on record for. That record is prose and may never be one of our own source files: our code standing as the evidence for its own shape is a circle, and the circle would wear the strongest label in the set. An announcement nobody wrote down is hearsay, and hearsay is exactly how "every document has a native inbox" became an implementation before anyone checked it. The date is there to be weighed: a direction announced long ago and still unwritten is worth less than one stated last week, and without a date nobody can tell the two apart.

A reserved name is not a declared shape, and the gap between them is the whole point. declared-not-wired is strong because the shape constrains us: implementing it is realising something the target wrote down. A name with nothing behind it constrains nothing, so what we build under it is our own invention wearing the target's vocabulary — the most dangerous kind, because the borrowed name makes it read as alignment.

The tell is mechanical: open the declaration and look for a payload. Several of InboxMsgContent's variants — Link, Patch, ServiceRequest, ExtRequest, RemoteQuery, DialogRequest — are bare unit variants carrying nothing at all, while ContactDetails and SocialQuery carry data and have processors. Presenting the first group as "the strongest thing to align on" was an error made here, in the same breath as citing this rule; see knowledge_what-an-inbox-is-upstream for what those variants do and do not offer.

Why this exists. A maintainer read that AddLinkV0 is defined upstream but constructed nowhere, and reported our implementation of it as our own invention — reasoning from the absence while quoting, in the same session, the rule against exactly that. The correct reading was declared-not-wired, which is the best alignment available. Prose said otherwise elsewhere too: docs/api-contract.md claimed docs.* was a 1:1 passthrough while two of its signatures diverged, and stated a member count that was simply wrong. Both errors are the same one — a claim about the target that nothing forced anyone to check.