From aeb8c7d157178baf7a87d0b1fafaefb3382e7345 Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Mon, 17 Aug 2026 10:10:23 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20l'engagement=20de=20la=20couche=20d'ind?= =?UTF-8?q?exation,=20et=20ses=20deux=20d=C3=A9clarations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le dépôt gagne sa doctrine et ses contrats, dans le régime bidirectionnel. Il publie son engagement — ce qu'un consommateur peut attendre de l'indexation — et déclare ce qu'il consomme lui-même, du polyfill et de ng-e2e-helpers. Les deux déclarations sont écrites depuis les appels réels, pas depuis ce que la surface offre : un usage non déclaré est la faute du consommateur en cas de rupture, et une surface offerte mais non déclarée reste librement modifiable. Version 1.0.0, pas 0.1.0 : sous semver, 0.x ne promet rien du tout, donc le majeur ne porte son signal qu'à partir de 1. Ce dépôt étant sur main, c'est une version pleine et non une pré-version. --- .gitignore | 3 + .project/VOCABULARY.md | 11 ++ .project/concepts/indexing/_overview.md | 42 +++++ .../indexing-layer/contract_indexing-layer.md | 146 ++++++++++++++++++ .../ng-e2e-helpers/usage_ng-helpers.md | 49 ++++++ .../polyfill-surface/usage_ng-helpers.md | 51 ++++++ .project/contracts.yaml | 35 +++++ AGENTS.md | 3 + package.json | 2 +- 9 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 .project/VOCABULARY.md create mode 100644 .project/concepts/indexing/_overview.md create mode 100644 .project/concepts/indexing/indexing-layer/contract_indexing-layer.md create mode 100644 .project/concepts/indexing/ng-e2e-helpers/usage_ng-helpers.md create mode 100644 .project/concepts/indexing/polyfill-surface/usage_ng-helpers.md create mode 100644 .project/contracts.yaml create mode 100644 AGENTS.md diff --git a/.gitignore b/.gitignore index 3331ef0..37ee4b1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ bun.lockb bun.lock e2e/.dist/ *.ngw + +# Per-developer contract access map — canonical identities are committed, local paths are not +.project/contracts.local.yaml diff --git a/.project/VOCABULARY.md b/.project/VOCABULARY.md new file mode 100644 index 0000000..080bea6 --- /dev/null +++ b/.project/VOCABULARY.md @@ -0,0 +1,11 @@ + + +## Project vocabulary — canonical terms: use VERBATIM in any language, marked `like this` + +```text + «indexing» + `index` an ordinary public document that holds one entry per indexed object, plus its own field declaration (never: catalogue, registry, listing) + `deposit` a bare object reference left in an index document's inbox — open to anyone, and never an instruction (never: message, submission, request) + `curate` the owner resolving the references deposited on its index and adding what it can (never: process, ingest, sync) + `entry` what an index holds for one indexed object — its NURI and its value for the index's field (never: row, record, item) +``` diff --git a/.project/concepts/indexing/_overview.md b/.project/concepts/indexing/_overview.md new file mode 100644 index 0000000..a6a279c --- /dev/null +++ b/.project/concepts/indexing/_overview.md @@ -0,0 +1,42 @@ +--- +type: overview +summary: An index is an ordinary public document that only ever grows — what this repo publishes to applications, and what it consumes from NextGraph +triggers: + keywords: [index, indexing, curate, curation, deposit, depositor, entry, descriptor, reference, only-grows] + paths: + - "src/**" + - "test/**" + - "e2e/**" + - "README.md" +vocabulary: + - term: index + gloss: an ordinary public document that holds one entry per indexed object, plus its own field declaration + not: [catalogue, registry, listing] + - term: deposit + gloss: a bare object reference left in an index document's inbox — open to anyone, and never an instruction + not: [message, submission, request] + - term: curate + gloss: the owner resolving the references deposited on its index and adding what it can + not: [process, ingest, sync] + - term: entry + gloss: what an index holds for one indexed object — its NURI and its value for the index's field + not: [row, record, item] +--- + +# indexing — an index built on top of NextGraph, and the boundaries around it + +NextGraph has no indexing concept and will not grow one, so this is a construction **above** it, in its own repository, and the dependency runs one way only: this repo depends on `@ng-eventually/polyfill`, and the polyfill must never learn anything about indexing. + +An index is an **ordinary document** in its creator's public store. What makes it an index is that an application references its NURI in its own source. Anyone may hand it a reference by depositing into its inbox; its owner resolves those references itself and adds what it finds. **An index only ever grows** — no removal was ever built, and none is planned. + +## Roles at the repository boundary + +This repo is a **provider** of `indexing-layer`, which the Festipod application consumes, and a **consumer** of two engagements published by `ng-eventually-js`: `polyfill-surface` and `ng-e2e-helpers`. Each pair lives in its own interface folder: the engagement is pulled and never hand-edited, our declaration beside it is ours to keep current. + +Frictions are the main path for telling a provider what we need. They go in our `usage_` leaf, the signal goes out of band, and the entry is pruned once the engagement absorbs it. + +## Read first + +- `indexing-layer/contract_indexing-layer` — what an application may rely on from `@ng-helpers/indexing`. +- `polyfill-surface/usage_ng-helpers` — the exact polyfill entries this layer stands on, and what it had to build for want of them. +- `ng-e2e-helpers/usage_ng-helpers` — what our end-to-end suite calls, and the peer-dependency constraint it must respect. diff --git a/.project/concepts/indexing/indexing-layer/contract_indexing-layer.md b/.project/concepts/indexing/indexing-layer/contract_indexing-layer.md new file mode 100644 index 0000000..0f5ca23 --- /dev/null +++ b/.project/concepts/indexing/indexing-layer/contract_indexing-layer.md @@ -0,0 +1,146 @@ +--- +type: contract +summary: The API @ng-helpers/indexing exposes to an application — creating an index, depositing references into it, curating it, and reading it back +--- + +# contract_indexing-layer — `@ng-helpers/indexing` + +## Scope + +This package builds an **index** on top of NextGraph: an ordinary public document that holds one entry per indexed object, keyed by that object's NURI and carrying its value for a single declared field. + +It covers creating an index, handing an index a reference to an object (open to anyone), the owner resolving those references and adding what it can, and reading the entries back in order. + +It does not cover NextGraph itself — documents, identity, sharing, inboxes, transport — all of which reach it through a port you supply. It does not cover search, filtering, pagination, or querying by anything but the index's own field. It **never removes anything**, from anywhere, and that is a property of the engagement rather than a missing feature. + +### Deployment requirements + +An application using this package must: + +- have a NextGraph session already open under the identity it wants to act as, and build the port from it — `polyfillPort({ sessionId })`, where `sessionId` is what `@ng-eventually/polyfill`'s own `init(…)` hands its callback; +- reach a broker, since every operation here is a document read, a document write, or an inbox deposit; +- **hardcode the index's NURI in its own source.** Nothing marks a document as an index; the reference is what makes it one, and it is the only way anyone reaches it. + +One handle is one identity: the port carries a session and no call takes an identifier. Two users mean two handles. + +## Surface + +Full typed shape: the package's `types` entry, `@ng-helpers/indexing`. The load-bearing signatures: + +```ts +// ── wiring: one handle, one identity ───────────────────────────────────────── +export function polyfillPort(options: PolyfillPortOptions): NextGraphPort; +export interface PolyfillPortOptions { readonly sessionId: string | number } +export function indexing(port: NextGraphPort): Indexing; + +// ── addressing (re-exported so you import them from here) ──────────────────── +export type Nuri = `did:ng:${string}`; +export type NuriLike = Nuri | string; +export type { PrincipalId, UnionSubject, NextGraphPort, IncomingDeposit, ObjectResolution }; + +// ── everything this package does ───────────────────────────────────────────── +export interface Indexing { + /** Creates an index in THIS identity's public store and opens its inbox. Any user may. + * `field` is the predicate an indexed object must carry, declared once and for good; + * an empty or blank one throws. Returns the NURI to hardcode. */ + createIndex(field: string): Promise; + /** Deposits a bare reference into the index's inbox. Open to ANYONE. Nothing lands in + * the index until its owner curates. Throws if the index has no inbox. */ + refer(index: NuriLike, object: NuriLike): Promise; + /** OWNER only — resolves the references received and adds what it can. */ + curate(index: NuriLike): Promise; + /** The entries, ordered by value. Sugar over `readUnion([index])`. */ + read(index: NuriLike): Promise; +} + +// ── what an index holds ────────────────────────────────────────────────────── +export interface IndexEntry { readonly object: Nuri; readonly value: string } +export interface IndexDescriptor { readonly field: string } + +// ── what curating reports ──────────────────────────────────────────────────── +export type CurationOutcome = + | { readonly result: "indexed"; readonly object: Nuri; readonly value: string } + | { readonly result: "unchanged"; readonly object: Nuri } + | { readonly result: "skipped"; readonly object: Nuri; readonly reason: SkipReason } + | { readonly result: "unresolved"; readonly object: Nuri; readonly reason: string } + | { readonly result: "foreign"; readonly reason: string }; +export type SkipReason = "no-field" | "several-values" | "self-reference"; +export interface CurationReport { + readonly index: Nuri; + readonly outcomes: readonly CurationOutcome[]; // one per deposit, in deposit order +} + +// ── what travels from a depositor to a curator ─────────────────────────────── +export type IndexDeposit = Nuri; // the reference IS the whole payload +export function decodeReference(payload: unknown): Nuri | null; // untrusted input + +// ── the IRIs, for a reader going straight to `readUnion` ───────────────────── +export const INDEX_FIELD: string; // on the index's own subject: the field it indexes by +export const ENTRY_VALUE: string; // on an entry: that object's value for the field +``` + +## Guarantees + +**An index is an ordinary public document, and nothing marks it as one.** It lives in its creator's public store, so any reader opens it from the reference alone; its creator owns it, and any user may create one. + +**The field is declared once, inside the document, and cannot be changed.** `createIndex` refuses an empty or blank field at the door, because nothing here deletes and an index created on a useless field is useless for good. Declaring it in the document rather than in an application's source is what stops two applications curating the same index on two different fields. + +**`createIndex` opens the index's inbox itself.** Only the owner can, and creation is the one moment the owner is present, so it is not left to a later call to remember. + +**Depositing is open to anyone; writing is the owner's alone.** `refer` is a deposit into the index document's inbox — not a write — so a stranger can contribute to an index they do not own. `curate` reads that inbox and writes the document, and both are refused to anyone but the owner. The deposit is a **bare reference**: it carries no operation, no index reference (the inbox address already identifies the index), and no copy of the indexed value. What the object itself says is what goes in. + +**An index ONLY EVER GROWS.** There is no call that removes an entry, for anyone including the owner, and none is planned. This package cannot express a removal at all. The only answer to "this entry must go" is a fresh index. + +**Curation is convergent and order-independent.** Deposits are never consumed, so every run sees every deposit again; re-applying one re-resolves the reference and lands on the same result. An already-indexed object is skipped outright as `unchanged`. Nothing depends on the order references arrived in. + +**A reference that does not resolve costs nothing and is reported.** It comes back as `unresolved`, nothing is written for it, and nothing already in the index is touched — a later deposit adds it. Every unresolved reference appears in `CurationReport.outcomes`: harmless is not the same as invisible. + +**Reading is per-entry tolerant.** `read` returns entries ordered by value, ties broken on the object NURI, so two readers of the same index always see the same order. Values are compared **as strings** — an index whose field holds ISO-8601 dates therefore comes out in chronological order. A subject that is not a NURI is skipped, never thrown on, and only own properties are read: one stray triple cannot make every real entry unreadable. + +**An entry carrying several values keeps the smallest, deterministically** — which two curation runs racing each other can produce. The entry stays visible and every reader agrees on it. + +**`read` refuses a document that declares no field at all**, rather than answering "an empty index". An unreadable document and an empty one arrive as the same empty result, so an empty answer would be a failure wearing the shape of a fact. Retry before concluding the document is malformed. + +**An index declaring SEVERAL fields refuses to CURATE, loudly and permanently — and stays readable.** Picking one would leave a single list ordered by two different properties, because entries already written are never re-read. Existing entries stay visible and correct; nothing new is added. The refusal cannot be undone, and it says so instead of suggesting a retry. + +**Reading needs nothing from this package.** An application that knows the NURI can call the polyfill's `readUnion([index])` and get the entries as subjects — one per indexed object, keyed by its NURI — plus the index's own subject declaring its field, which `read` drops. `INDEX_FIELD` and `ENTRY_VALUE` are published for exactly that reader. + +**Every inbox payload is untrusted.** Anyone may deposit anything; `decodeReference` returns `null` for everything that is not a reference, and such a payload is reported as `foreign` rather than crashing curation. + +## Non-guarantees + +**No removal, at any level, ever.** Not an oversight and not "not yet": it was deliberately never built. Do not design around a future delete. + +**No refresh.** An already-indexed object is never re-read, so an object whose field value changes later keeps its original value in the index, indefinitely. + +**No private data.** Indexing is limited to objects the curator can open itself. An object the index's owner cannot read is simply `unresolved`. + +**`unresolved` does not tell you why.** Gone, unreadable, and "the read failed" arrive identically and are deliberately not distinguished. Never read it as "the object does not exist". + +**The narrow behaviours are open questions, not promises.** An object carrying nothing for the field is `skipped: "no-field"`; one carrying several values is `skipped: "several-values"`; a raced entry keeps the smallest value. Each is implemented in its narrowest form and reported rather than generalised, and each may change. + +**No stable error text.** What a throw or an `unresolved` reason reads is for a human reading a report. Do not parse it or branch on it. + +**No timing and no delivery promise.** A deposit is not in the index until the owner curates, and nothing here schedules curation. There is no notification, no queue depth, and no ordering between a deposit and a read. + +**The report grows with the inbox.** Since deposits are never retired, `CurationReport.outcomes` has one entry per deposit ever made, not per change. + +**No cross-broker reach.** A NURI resolves for users of the same broker. + +**No depositor authentication or rate limit.** Anyone may deposit any number of payloads into any index's inbox. + +## Change policy + +**Semver, and majors are the normal case.** This layer sits on a polyfill that is itself converging on a NextGraph that does not ship yet, and several of its own behaviours are declared above as open questions. Settling one of them narrows this surface — the major number will move often, and that frequency is the honest signal about this package, not an apology. Refusing to version would not slow the churn down; it would only take away the one tool you have for managing it. Pin a version, upgrade deliberately, and re-pull this contract each time. + +What each level means here, in this package's own terms: + +- **major** — an exported symbol is removed or renamed, **or** an existing call narrows: it now throws where it returned, or reports a state you did not have to handle before. Settling an open question counts, and so does adding a `CurationOutcome` variant or a `SkipReason` — an exhaustive `switch` in your code stops being exhaustive. A signature change a caller must react to counts; one that only accepts more than before does not. +- **minor** — a symbol is added and nothing existing moves: a new read helper, a new optional option. +- **patch** — a fix that changes neither the exported surface nor anything above under `## Guarantees`, including the text of a throw, which is explicitly disclaimed above. + +**A tag says where it comes from.** A release cut on `main` carries a **full version** (`1.0.0`), and the three rules above govern what changes between two full versions. Work still on a branch carries a **pre-release** of the version it is heading for (`1.0.0-dev.3`), which sorts *below* that version by construction — so you can pin what exists today while the tag itself tells you the surface has not been released and may still move before it is. Between two pre-releases of the same version nothing is promised: re-pull and read this leaf again. When the branch lands, the full version appears alongside; the pre-release keeps resolving, so no reference you pinned is ever withdrawn from under you. + +`1.0.0` is a baseline, not a claim of maturity: it is the number that makes your pin mean something. Nothing was released before it. This engagement is cut on `main`, so `1.0.0` is what you pin, and your `usage_` leaf anchors `against:` on that exact string — `against: @ng-helpers/indexing@1.0.0`. Had you pinned a pre-release, `against:` would carry that string, pre-release suffix included. + +There is no changelog file and no deprecation window: **the sections above are the release note.** A removal or a narrowing lands in `## Surface` and `## Guarantees` in the same version that ships it. Diff this leaf between two pulls — `## Guarantees` and `## Non-guarantees` before `## Surface`, because that is where a narrowing shows up first. diff --git a/.project/concepts/indexing/ng-e2e-helpers/usage_ng-helpers.md b/.project/concepts/indexing/ng-e2e-helpers/usage_ng-helpers.md new file mode 100644 index 0000000..7fcb6fa --- /dev/null +++ b/.project/concepts/indexing/ng-e2e-helpers/usage_ng-helpers.md @@ -0,0 +1,49 @@ +--- +type: usage +summary: What this repo's end-to-end suite calls from ng-e2e-helpers, the peer-dependency constraint it must respect, and the gaps it fills itself +against: ng-e2e-helpers@1.0.0-dev.1 +--- + +# usage_ng-helpers — `@ng-helpers/indexing`'s end-to-end suite on `ng-e2e-helpers` + +The consumer is this repository's end-to-end suite: one run that builds a small indexing application, serves it, gets real people into it through the real broker, and drives depositing and curating between them. + +Everything generic — the wallet lifecycle, the broker crossing, per-run profiles, bounds, the report shape, the recognition of the known browser failure modes — comes from the engagement and is **used, never reimplemented**. What is specific to this repository is the page that carries our application and the name its runs mint, and nothing else. + +## Consumed surface + +**Bounds** — `within`, `armSuiteDeadline`, `closeQuietly`, `firstLine`. + +**Measurement** — `measured`. + +**Browser and profiles** — `launchWatchedContext`, `closeContext`, `newPage`, and the type `RunProfile`. + +**Wallet** — `mintWalletProfile` and the type `WalletCredentials`. Each run mints its own. + +**Broker crossing** — `setupBrokerPage`. + +**Serving** — `serveOnEphemeralPort`, for the application bundle. + +**Known failure modes** — `browserTrouble`, wired as the suite's `diagnose`. + +**Report** — `declareSuite` and the type `Prerequisite`. + +**Constants** — `BROKER_ROUND_TRIP_MS`, `NEW_PAGE_MS`. + +Everything else the engagement offers is NOT consumed here: the screen inventory and its types, `completeBrokerLogin`, `emptyProfileContext`, `importWalletFile`, `exportWalletBytes`/`exportWalletFile`, `mintWalletBytes`, `mintWalletProfileKeepingContext`, `createWalletInContext`, `newRunProfile`, `isAlive`, `browserLost`, `lossDeclared`, `enclosingBound`, `frameTrouble`, the exported error classes, and the remaining `*_MS` constants. It is safely evolvable as far as this suite is concerned. + +## Constraints + +**The browser types are DERIVED from the helpers, never imported from `playwright` here.** The engagement declares Playwright a peer dependency, and this package reaches it as symlinked files — so TypeScript resolves the helpers' `playwright` from where those files really live. Importing the driver in this repository as well produced two structurally different copies of `BrowserContext`, and a context this suite had opened could not be handed back to the helper that opens contexts. Taking the types from the calls that return them leaves exactly one set, and a version skew can no longer express itself as a type error in code that is correct. + +**One wallet per run, minted, never carried.** The run's wallet name is stable and its identity is not; nothing survives a run, and no result depends on a previous one. + +**Nothing generic is reimplemented here.** Where a helper exists, it is called. That is a standing rule for this suite, not a preference — the crossing alone has cost days of misdiagnosis upstream, and a local copy of it would not carry those lessons. + +**Every wait is entered bounded.** No page or frame operation runs outside `within` or a helper that bounds it itself. + +## Frictions + +**Nothing bounds a call into the application iframe.** `frame.evaluate` carries no timeout of its own, so this suite wraps every bridge call itself. The engagement offers no way to obtain that bound, so each consumer re-derives the same wrapper — and the derivation is not free: the calls this suite reached for outside its own wrapper are exactly the ones that can still hang it. A bounded `evaluate` here would delete the wrapper and close the gap in one move. + +**"Measured and bounded" is one intent and two calls.** Sizing a bound from its own measurement is the discipline the engagement itself prescribes, yet every step in this suite has to compose `measured(what, ms, (bound) => within(what, bound, task))` by hand. Two consumers writing the same three-line helper is the tell that the pair belongs on the engagement. diff --git a/.project/concepts/indexing/polyfill-surface/usage_ng-helpers.md b/.project/concepts/indexing/polyfill-surface/usage_ng-helpers.md new file mode 100644 index 0000000..cad6d43 --- /dev/null +++ b/.project/concepts/indexing/polyfill-surface/usage_ng-helpers.md @@ -0,0 +1,51 @@ +--- +type: usage +summary: The seven polyfill entries the indexing layer stands on, the constraints it holds itself to, and what it had to build for want of a published helper +against: "@ng-eventually/polyfill@1.0.0-dev.1" +--- + +# usage_ng-helpers — `@ng-helpers/indexing` on `polyfill-surface` + +The consumer is the indexing layer: an index is an ordinary public document, contributions reach it through its inbox, and its owner curates it. NextGraph has no indexing concept, so nothing of what a deposit *means* belongs upstream — the polyfill's inbox stays generic and carries opaque payloads, and this layer decides what they say. + +The whole runtime dependency passes through **one file**, the adapter that builds our `NextGraphPort`. Everything else in the package is written against that port, so a change to the engagement breaks exactly one file and nothing else. The types are imported type-only, so they are literally the published ones rather than a copy that can drift. + +## Consumed surface + +**Placement** — `storeRegistry.createEntityDoc("public")`, for the index document itself; `storeRegistry.openDocumentInbox(doc)`, called once at creation. + +**Reading** — `readUnion([doc])` and the type `UnionSubject`. Used both for the index document and for resolving a deposited reference. + +**Writing** — `docs.sparqlUpdate(sessionId, update, anchor)`, with the document named ONCE as the anchor so the statement carries no `GRAPH <…>` wrapper. It is the only write this package makes, and it is always an `INSERT DATA` of literal triples. + +**Inbox** — `inbox.postToDocument(doc, { payload })` for depositing, `inbox.readForDocument(doc)` for the owner draining it, and the shape of `Deposit` (`from` / `payload` / `ts`), which our `IncomingDeposit` mirrors. + +**Types** — `Nuri`, `NuriLike`, `PrincipalId`, `UnionSubject`. + +**Bootstrap, in the end-to-end application only** — `configure`, this package's `init` (for the `sessionId` its callback delivers), and `ensureIdentity`. + +Everything else on the engagement is offered and NOT consumed: `watchShape`, `useShape`, `subscribeDoc`/`subscribeDocs`, `docs.docCreate`, `docs.sparqlQuery`, `storeRegistry.listMyEntityDocs`/`resolveScopeGraph`/`resolveWriteGraph`, `inbox.share`/`post`/`read`/`readSynced`/`readSyncedForDocument`/`processInbox`/`watch`, `ng`, `initNg`. It is safely evolvable as far as this layer is concerned. + +## Constraints + +**One port is one identity.** The polyfill's session is one user's and no call takes an identifier, so an `Indexing` handle is one person's. Two users mean two handles — which is also what keeps our multi-actor tests honest: a depositor obtains the index NURI the way an application does, never through a shared variable. + +**`sessionId` is relayed, never converted.** We carry it at the engagement's own `string | number` and hand it back untouched. + +**The write is add-only, and structurally so.** There is no delete builder anywhere in this package, and the only statement it can compose is an anchored `INSERT DATA`. Our own tests execute that SPARQL against an engine that refuses anything else, so a removal is unrunnable rather than merely undetected. This constrains what we ask of the engagement: we need exactly one write primitive and no more. + +**The inbox is opened at creation, from one place.** The engagement disclaims coalescing `openDocumentInbox` across pages, so we never open an index's inbox anywhere but in `createIndex`, under its owner, at the moment the document is created. + +**Every payload out of an inbox is untrusted input.** Anyone may deposit anything, so nothing read from a deposit reaches a query before being checked; a non-reference is reported, never thrown on. + +**An empty read is never treated as "empty".** Nothing in this layer reads `[]` from `readUnion` as "a valid index that happens to hold nothing" — the descriptor check refuses a document declaring no field, and that refusal aborts curation before a single write. + +## Frictions + +**The engagement does not say what `readUnion` does with a document it cannot read.** It says what it returns for a document it can, and it says that a rejection means "unknown, never absent" — but not whether an unreadable document inside the list comes back as a rejection or is swallowed into the result. We assume the worst (swallowed, therefore indistinguishable from empty) and code defensively around it. A sentence in `## Guarantees` settling this would replace a guess we are carrying in every read path. + +**No published NURI type guard.** `## Guarantees` states plainly that no type guard is published, so this layer carries its own — and it needs one, because a NURI arrives here from an untrusted inbox deposit and must be checked before it can be written between angle brackets. The check we wrote is a guess at what the engagement considers a valid `Nuri`, and a wrong guess is either a rejected legitimate reference or an injected one. + +**No published escaping helpers.** The engagement lists no `escapeIri`/`escapeLiteral`, and we compose SPARQL against `docs.sparqlUpdate` — so this package carries its own escaping rather than reach into the provider's internals. That is a security-relevant duplication of something the provider certainly already has: two implementations of the same rule, one of which is not the one the provider tests. + +**No published way to write triples above the raw SPARQL primitive.** `docs.sparqlUpdate` is the level we had to align on, which is why the two frictions above exist at all. A published "add these triples to this document" would remove the query composition, the escaping and the NURI validation from this layer in one move. diff --git a/.project/contracts.yaml b/.project/contracts.yaml new file mode 100644 index 0000000..3fbfe25 --- /dev/null +++ b/.project/contracts.yaml @@ -0,0 +1,35 @@ +# Inter-repo contracts. `publish:` is this project's engagement toward its consumers — +# listing a leaf here IS the act of publishing it; an unlisted `contract_` leaf is a draft. +# +# This project is BOTH a provider and a consumer, and the two faces are isolated at document +# level: `indexing-layer` never mentions what we consume, and our `usage_` leaves never +# mention what we promise. What backs a published guarantee is internal doctrine. +# +# PROVIDES indexing-layer (concepts/indexing/indexing-layer/) +# consumer: the Festipod application, in its own repo. It has not declared a +# usage leaf, and we do not author one on its behalf — an interface with no +# declared consumer simply runs in the one-document mode. +# +# CONSUMES polyfill-surface and ng-e2e-helpers, both published by `ng-eventually-js`. +# We pull each engagement and author the `usage_ng-helpers.md` beside it. +# +# `pullFrom:` values are CANONICAL remote identities, because this file travels with the +# branch. Per-developer local access lives in `.project/contracts.local.yaml`, which is +# gitignored and must never be committed. + +publish: + # paths are relative to `.project/` + indexing-layer: concepts/indexing/indexing-layer/contract_indexing-layer.md + +consume: + - contract: polyfill-surface + type: git + pullFrom: git@gitea.reconnexion.apps.gueraud.net:Reconnexion/ng-eventually.git/.project/concepts/app-contract/polyfill-surface/contract_polyfill-surface.md + ref: caps-p1a-and-virtual-user-boundary + into: concepts/indexing/polyfill-surface/ + + - contract: ng-e2e-helpers + type: git + pullFrom: git@gitea.reconnexion.apps.gueraud.net:Reconnexion/ng-eventually.git/.project/concepts/e2e-harness/ng-e2e-helpers/contract_ng-e2e-helpers.md + ref: caps-p1a-and-virtual-user-boundary + into: concepts/indexing/ng-e2e-helpers/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e7ea3d0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +## Project vocabulary (always loaded) + +@.project/VOCABULARY.md diff --git a/package.json b/package.json index 93c919b..832a3d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ng-helpers/indexing", - "version": "0.0.0", + "version": "1.0.0", "private": true, "type": "module", "description": "An indexing layer built on top of NextGraph, via @ng-eventually/polyfill. An index is an ordinary public document; contributions reach it through its inbox; its owner curates it.",