test: rendre la suppression inexécutable plutôt que détectée

Trois trouvailles adverses survivaient à leur premier correctif. Elles avaient
la même faiblesse : ce qui prouve le code n'exerçait pas le code.

L'adaptateur n'était pas testé dans son comportement — six méthodes sur sept
pouvaient être vidées avec la suite verte. Une doublure de polyfill en mémoire
les exerce désormais toutes : elle RÉPOND au lieu de rendre des constantes, et
elle EXÉCUTE le SPARQL, avec un analyseur qui n'accepte qu'un INSERT DATA ancré
de triplets littéraux. Les six vidages ont été vérifiés rouges, entre 2 et 10
échecs chacun.

Et la garde anti-suppression change de nature. Elle n'instrumentait que deux
méthodes, et cinq contournements passaient — dont un littéral coupé placé dans
readDeposits, qui est le chemin de la prochaine fonctionnalité. Maintenant le
moteur REFUSE la requête : « expected the keyword INSERT, found DELETE ». La
suppression n'est plus détectée, elle est inexécutable — ce qui ne se contourne
pas par une écriture plus habile.

Une nouvelle méthode est couverte par construction, sans liste à tenir : le test
lit Object.keys du port. Vérifié — une huitième méthode sans pilote fait rougir.

Le README disait qu'une entrée ne change jamais après sa création, alors qu'une
valeur plus petite l'écrase. Remplacé par ce qui est vrai — un objet déjà indexé
n'est jamais relu — avec le cas reproduit. Et « ne fait que grandir » dit
désormais que la suppression n'a JAMAIS été construite, pas qu'elle a été
retirée : celui qui en aura besoin doit le lire, pas le déduire d'une fonction
manquante.

Enfin la forme d'écriture s'aligne sur celle du polyfill — l'écriture ancrée
sans clause GRAPH, le document nommé une seule fois comme ancre. buildInsertTriple
ne prend plus de graphe, donc elle ne peut plus dériver.
This commit is contained in:
Sylvain Duchesne
2026-08-17 09:23:24 +02:00
parent 469346aef3
commit 75378fc5a4
7 changed files with 834 additions and 155 deletions
+9 -4
View File
@@ -27,7 +27,7 @@ That boundary is held by one file. `src/polyfill-adapter.ts` is the only place t
## An index only ever grows
**Nothing is ever removed from an index — by anyone, including its owner.** This is a deliberate limitation, written down here rather than left to be discovered.
**Nothing is ever removed from an index — by anyone, including its owner.** An index only ever grows. There is no removal function, and there never was one: removal was deliberately **never built**, not built and then withdrawn, and nothing is planned. This is written down here rather than left to be deduced from a missing function, because someone who needs an entry gone should learn that it was never possible instead of hunting for the call that does it. Today the only answer to that need is a fresh index.
It is what makes the failure story trivial. Since the only write is an addition, a reference that does not resolve — the object is gone, or unreadable, or the broker simply did not answer — can only ever mean *not added this time*. It cannot damage what is already there, and a later deposit adds it. Nothing has to tell an absence from a failure, so nothing can get that wrong.
@@ -45,6 +45,8 @@ What holds now: entries take **at least one value, smallest wins**, deterministi
The lesson is worth keeping: **"nothing removes" is a claim about the write path, and an invariant about what a reader can *see* has to be checked on the read path too.**
What enforces the write half is no longer a pattern over source. `test/adapter.test.ts` runs the real adapter on `test/fake-polyfill.ts`, an in-memory polyfill whose SPARQL is **executed** by an engine that understands one statement — an anchored `INSERT DATA` of literal triples — and refuses everything else. A removal is therefore not *detected*, it is **unrunnable**: `DELETE WHERE …` fails on the first keyword, a second statement smuggled after the closing brace fails on the trailing text, a keyword hidden inside a literal stays inside the literal because a parser tokenises where a regex only matches, and splitting the keyword across concatenated strings buys nothing, since it is one string by the time it arrives. The regex over `src/` in `test/units.test.ts` stays as a cheap tripwire that names the file early; it is not the proof.
**A failed resolve is still a failure, and still surfaces.** Harmless is not the same as invisible. Every reference that could not be resolved comes back as an `unresolved` outcome in the curation report and is warned about — a failure that looks exactly like a normal outcome teaches nobody anything.
## Open questions
@@ -53,8 +55,10 @@ Deliberately not settled. Each is implemented in its narrowest form and reported
- **An object that carries nothing for the index's field.** Narrow behaviour: it is not added, and reported as `skipped: "no-field"`. There is no key to index it by, and inventing one — a placeholder, the deposit's timestamp — would put something in the index that the object does not say. Whether it should instead be indexed under an absent key, or refused louder, is open.
- **An object that carries several values for the field.** Not added, reported as `skipped: "several-values"`. Which of them the entry would hold has not been decided.
- **An entry never changes after it is made.** An already-indexed object is not even re-read, so an object whose field value changes later keeps its original value in the index. Refreshing it would be a write nobody asked for, and it is the same question as removal.
- **An already-indexed object is never re-read.** Curation skips it outright, so an object whose field value changes later keeps its original value in the index. Refreshing it would be a write nobody asked for, and it is the same question as removal.
- **Which value a raced entry should keep.** Two curation runs racing each other can leave an entry with two values; the smallest is chosen so that readers agree and the entry stays visible. That the entry must survive is settled; *which* of the two it should hold is not.
Note what this means, since the two points above are easy to read as one: an entry's value **can** change after it is made. Not by re-reading the object — that never happens — but because a *second* value arriving for the same entry can be the smaller one, and `read()` returns the smallest. Index an object at `2026-05-05`, let a raced run add `2026-01-01`, and `read()` answers `2026-01-01`. What never changes is the set of entries and the fact that each stays visible; the value one of them reports is settled by "smallest wins", not by arrival order.
- **How an index recovers from an ambiguous declaration.** Today it does not: curation refuses for good and the only way forward is a fresh index. Since nothing here removes anything, giving it a way back needs a mechanism that does not exist yet.
- **Deposits are never retired.** Every curation run sees every deposit ever made. That is affordable because re-applying one is a no-op, but it is linear in the history. How a curator retires an applied deposit is open — `inbox.processInbox` may be the answer, but its semantics are not published.
- **What an entry holds besides the object reference and the field value**, and **how several index kinds would coexist**, are both untouched.
@@ -70,8 +74,9 @@ Deliberately not settled. Each is implemented in its narrowest form and reported
| `src/curator.ts` | Resolving references and adding what is there |
| `src/indexing.ts` | The public surface, bound to one identity |
| `src/sparql.ts` | The one statement this package writes — no deletion exists |
| `test/fake-nextgraph.ts` | An in-memory NextGraph enforcing the polyfill's published guarantees |
| `test/adapter-write-path.test.ts` | Runs the real adapter against a recorder and reads back every query it emits |
| `test/fake-nextgraph.ts` | An in-memory NextGraph behind `NextGraphPort`, enforcing the polyfill's published guarantees |
| `test/fake-polyfill.ts` | An in-memory `@ng-eventually/polyfill` that **executes** the SPARQL — an engine that can only add |
| `test/adapter.test.ts` | Runs the real adapter on it: behaviour, every query emitted, and every method driven |
## Depends on