feat!: curer n'est plus un appel, c'est ce que fait le traitement de l'inbox

This commit is contained in:
Sylvain Duchesne
2026-08-20 11:04:31 +02:00
parent 2ce2113157
commit e2ed970cbd
21 changed files with 956 additions and 273 deletions
+9 -3
View File
@@ -23,6 +23,8 @@ That boundary is held by one file. `src/polyfill-adapter.ts` is the only place t
**A deposit is a bare reference. Nothing else.** It states no claim and gives no instruction: no operation, no index reference (the inbox address already identifies the index), no copy of the indexed value. When the owner curates, it resolves the reference and opens the object itself — which it can, because indexing is limited to public data for now. What the object says is what goes in. This is the shape NextGraph already uses upstream, where a `SocialQueryRequest` carries a reference to an RDF definition and the recipient composes its own update; a payload carrying an operation would be a licence for anyone to rewrite someone else's document.
**Curating is not an act an application performs.** There is no `curate` to call, and there never will be: it would ask an application to decide who owns an index and when curation runs, and neither is its decision. Curation is what happens when the index's inbox is processed — **at its creator's next connection, and on each deposit while the creator is connected**. `await indexing(port)` IS that connection: it goes through the inbox of every index that identity owns, and leaves each one watched. `src/observation.ts` is where that lives.
**Reading needs nothing new.** An application that knows the NURI calls `readUnion([indexNuri])` and gets the entries as subjects. `Indexing.read` is sugar over exactly that, dropping the index's own declaration subject.
## An index only ever grows
@@ -47,20 +49,22 @@ The lesson is worth keeping: **"nothing removes" is a claim about the write path
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.
**A failed resolve is still a failure, and still surfaces.** Harmless is not the same as invisible. Nobody asked for the run, so there is nobody to hand a report to: every reference that could not be resolved is warned about on this package's log stream, and so is a run, a watch or a store read that could not happen at all. None of them denies the application anything — reading an index and depositing into one never depended on that work, and the deposits stay in their inbox for the next connection.
## Open questions
Deliberately not settled. Each is implemented in its narrowest form and reported rather than generalised.
- **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 object that carries nothing for the index's field.** Narrow behaviour: it is not added, and nothing says so. 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 either. Which of them the entry would hold has not been decided.
- **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.
- **How an owner finds its own indexes.** Nothing marks a document as an index, so connecting reads that identity's whole public store and looks at each document. It is the only question the surface can ask, and it does not scale with a large store. A marker on the document, or a listing narrowed by shape, would both settle it — and both are decisions about what an index *is*.
- **Nothing stops a handle.** `inbox.watch`'s unsubscribe is dropped, so a session watches its own indexes until the page goes. An application that changes identity in one page has to drop the old handle and know that it goes on watching.
- **What an entry holds besides the object reference and the field value**, and **how several index kinds would coexist**, are both untouched.
## Layout
@@ -72,6 +76,8 @@ Deliberately not settled. Each is implemented in its narrowest form and reported
| `src/deposit.ts` | The deposit's shape: a bare reference |
| `src/index-document.ts` | An index's declaration and its entries |
| `src/curator.ts` | Resolving references and adding what is there |
| `src/observation.ts` | When that happens: this identity's indexes, caught up and watched |
| `src/coalescing.ts` | Runs a job, never twice at once, and grants exactly one more run |
| `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 behind `NextGraphPort`, enforcing the polyfill's published guarantees |