CultMesh TS: a document is read only under an id its type declares

Soul F3 and F4, and F1 in CultMesh's schema descriptor.

- The schema descriptor built from a definition uses schemaIdentityOf, so
  a handle's schema id is the id the cache stamps.
- Store documents (F3): a handle reads the record under its schema id, or,
  for a definition, under an id the definition lists as compatible, as the
  cache resolves it. The catalog-entry name/version match and both parse
  fallbacks are deleted: a record under another id is not this document,
  whatever its payload parses as.
- Peer snapshots (F4): syncDocumentFromPeerSnapshot and definition-named
  peer-snapshot handles take the peer's record at the key under the type's
  schema id only. The shape filter is deleted and no match is not-found.
  P8a (syncing A at k while the peer holds only B at k overwrote the local
  B and then threw) and P8b (a peer's C stored as A) write nothing now.
- HELD: a peer-snapshot handle named by a bare schema-id string still takes
  the first record at the key. Eve Electron reads the C# Aetheria daemon
  that way (EveElectron src/cultmesh-provider-client.mjs:129-142): the
  daemon stamps SHA-256 schema ids and the client asks by
  "gamecult.eve.provider_advertisement.v1", so only the fallback finds its
  records. How a TS reader names a C#-hosted schema is raised as a fork;
  the fallback goes when it is ruled.

Tests: the store- and peer-snapshot tests that asserted shape routing are
replaced by tests of the refusal, including P8a and P8b, and one that pins
the held fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 19:49:57 +02:00
commit 9118e24976
2 changed files with 93 additions and 148 deletions

View file

@ -5,6 +5,7 @@ import {
CultCache,
SchemaConflictError,
SingleFileMessagePackBackingStore,
schemaIdentityOf,
type AnyCultCacheDocumentDefinition,
type CacheBackingStore,
type CultCacheEnvelope,
@ -2549,7 +2550,7 @@ export function cultMeshDocumentFromStore(
const documentId = options.documentId ?? resolvedSchemaId;
const read = async () => {
const records = await store.pullAll();
const record = resolveCultMeshStoreDocumentRecord(records, schema, schemaOrDefinition);
const record = resolveCultMeshStoreDocumentRecord(records, schemaOrDefinition);
if (!record) {
throw new Error(`CultMesh store document '${documentId}' did not contain schema ${resolvedSchemaId}.`);
}
@ -2673,16 +2674,7 @@ export function cultMeshDocumentFromPeerSnapshot(
{
timeoutMs: options.timeoutMs,
messageIdPrefix: options.messageIdPrefix ?? documentId,
accepts: typeof schemaOrDefinition === "string"
? undefined
: record => {
try {
schemaOrDefinition.schema.parse(decodeCultNetRawDocumentPayload(record));
return true;
} catch {
return false;
}
},
firstRecordAtKey: typeof schemaOrDefinition === "string",
},
),
),
@ -4004,14 +3996,6 @@ export class CultMeshNode {
{
timeoutMs: options.timeoutMs,
messageIdPrefix: options.messageIdPrefix ?? key,
accepts: record => {
try {
definition.schema.parse(decodeCultNetRawDocumentPayload(record));
return true;
} catch {
return false;
}
},
},
);
await this.documents.applyRawDocumentPutMessage(this.cache, {
@ -6170,12 +6154,8 @@ export class CultMesh {
function cultMeshSchemaFromDefinition(
definition: AnyCultCacheDocumentDefinition,
): CultMeshDocumentSchemaDescriptor {
return {
type: definition.type,
schemaId: definition.schemaId ?? definition.type,
schemaName: definition.schemaName,
schemaVersion: definition.schemaVersion,
};
const { schemaId, schemaName, schemaVersion } = schemaIdentityOf(definition);
return { type: definition.type, schemaId, schemaName, schemaVersion };
}
function normalizeCultMeshDocumentSchema(
@ -6243,57 +6223,22 @@ function inferCultMeshSchemaName(schemaId: string | undefined): string | undefin
return /^\d+$/.test(version) ? schemaId.slice(0, marker) : undefined;
}
// The record a store document reads: the one under the handle's schema id, else, for a definition, one under an id the
// definition lists as compatible, as CultCache resolves it. A record under any other id is not this document, whatever
// its payload parses as.
function resolveCultMeshStoreDocumentRecord(
records: readonly CultCacheEnvelope[],
schema: CultMeshDocumentSchemaDescriptor,
schemaOrDefinition: string | AnyCultCacheDocumentDefinition,
): CultCacheEnvelope | undefined {
const schemaId = schema.schemaId ?? schema.type;
return (
records.find(candidate => schemaId !== undefined && candidate.schemaId === schemaId) ??
records.find(candidate =>
cultMeshEnvelopeMatchesSchema(candidate, schema) &&
(typeof schemaOrDefinition === "string" ||
cultMeshEnvelopeParsesAsDefinition(candidate, schemaOrDefinition))) ??
(typeof schemaOrDefinition === "string"
? undefined
: records.find(candidate => cultMeshEnvelopeParsesAsDefinition(candidate, schemaOrDefinition)))
);
const schemaIds = cultMeshAcceptedSchemaIds(schemaOrDefinition);
return records.find(candidate => candidate.schemaId === schemaIds[0])
?? records.find(candidate => candidate.schemaId !== undefined && schemaIds.includes(candidate.schemaId));
}
function cultMeshEnvelopeMatchesSchema(
envelope: CultCacheEnvelope,
schema: CultMeshDocumentSchemaDescriptor,
): boolean {
const entry = envelope.catalogEntry;
if (!entry) {
return false;
}
if (
schema.schemaId &&
(entry.schemaId === schema.schemaId ||
envelope.schemaId === schema.schemaId ||
entry.compatibleSchemaIds?.includes(schema.schemaId))
) {
return true;
}
return Boolean(
schema.schemaName &&
entry.schemaName === schema.schemaName &&
(schema.schemaVersion ?? "") === (entry.schemaVersion ?? ""),
);
}
function cultMeshEnvelopeParsesAsDefinition(
envelope: CultCacheEnvelope,
definition: AnyCultCacheDocumentDefinition,
): boolean {
try {
definition.schema.parse(decode(envelope.payload));
return true;
} catch {
return false;
}
function cultMeshAcceptedSchemaIds(schemaOrDefinition: string | AnyCultCacheDocumentDefinition): readonly string[] {
return typeof schemaOrDefinition === "string"
? [schemaOrDefinition]
: schemaIdentityOf(schemaOrDefinition).compatibleSchemaIds;
}
function cultMeshSchemaLabel(schema: CultMeshDocumentSchemaDescriptor): string {
@ -6336,6 +6281,13 @@ function normalizeRudpDocumentPut(
};
}
// The peer's record at the key under exactly this schema id. A record at the key under another id is another type's, and
// is never taken in its place; none under this id is not-found.
//
// HELD (fork raised to Self, 2026-09-30): a handle named by a bare schema-id string still takes the first record at the
// key when none carries that id. Eve Electron reads the C# Aetheria daemon this way: the daemon stamps SHA-256 schema ids
// and the client asks by "gamecult.eve.provider_advertisement.v1", so only this fallback finds its records. It is removed
// when the fork on how a TS reader names a C#-hosted schema is ruled. Definition handles and sync never take it.
async function requestCultNetRawSnapshotDocument(
peerOrProvider: CultNetPeer | (() => CultNetPeer | Promise<CultNetPeer>),
schemaId: string,
@ -6343,7 +6295,7 @@ async function requestCultNetRawSnapshotDocument(
options: {
timeoutMs?: number;
messageIdPrefix?: string;
accepts?: (document: CultNetRawDocumentRecord) => boolean;
firstRecordAtKey?: boolean;
} = {},
): Promise<CultNetRawDocumentRecord> {
const peer = await resolveCultNetPeer(peerOrProvider);
@ -6352,11 +6304,9 @@ async function requestCultNetRawSnapshotDocument(
messageId,
timeoutMs: options.timeoutMs,
});
const candidates = response.documents.filter(candidate => candidate.recordKey === recordKey);
const document =
candidates.find(candidate => candidate.schemaId === schemaId) ??
(options.accepts ? candidates.find(options.accepts) : undefined) ??
candidates[0];
const atKey = response.documents.filter(candidate => candidate.recordKey === recordKey);
const document = atKey.find(candidate => candidate.schemaId === schemaId)
?? (options.firstRecordAtKey ? atKey[0] : undefined);
if (!document) {
throw new Error(`CultMesh peer snapshot did not return ${schemaId} at ${recordKey}.`);
}

View file

@ -357,57 +357,43 @@ test("CultMesh TS documents with both capabilities require an immutable writer c
assert.deepEqual(writes, ["authoritative:canonical", "prediction:predicted"]);
});
test("CultMesh TS store document handles choose compatible foreign schema payloads", async () => {
const store = {
async pullAll() {
return [
{
key: "note:foreign",
type: "runtime.generated.incompatible-note",
schemaId: "runtime.generated.incompatible-note.v1",
storedAt: "2026-06-27T00:00:00Z",
payload: encode({ nope: "not the requested shape" }),
catalogEntry: {
schemaId: "runtime.generated.incompatible-note.v1",
schemaName: "runtime.generated.incompatible-note",
schemaVersion: "runtime.generated.incompatible-note.v1",
contentHash: "runtime.generated.incompatible-note.v1",
canonicalSchemaJson: "{}",
compatibleSchemaIds: ["runtime.generated.incompatible-note.v1"],
},
},
{
key: "note:foreign",
type: "runtime.generated.cultmesh.note",
schemaId: "runtime.generated.cultmesh.note.ui.42",
storedAt: "2026-06-27T00:00:01Z",
payload: encode({
noteId: "note:foreign",
body: "foreign schema id, local store shape",
}),
catalogEntry: {
schemaId: "runtime.generated.cultmesh.note.ui.42",
schemaName: "cultmesh.note",
schemaVersion: "cultmesh.note.v1",
contentHash: "runtime.generated.cultmesh.note.ui.42",
canonicalSchemaJson: "{}",
compatibleSchemaIds: ["runtime.generated.cultmesh.note.ui.42"],
},
},
];
},
// A store document reads the record under its own schema id, or under an id its definition lists as compatible. A record
// under any other id is another type's, and is never read in its place because its payload happens to parse.
test("CultMesh TS store document handles read only records under an id their definition declares", async () => {
const envelope = (schemaId: string, value: unknown) => ({
key: "note:store",
type: schemaId,
schemaId,
storedAt: "2026-06-27T00:00:00Z",
payload: encode(value),
});
const storeOf = (...records: ReturnType<typeof envelope>[]) => ({
async pullAll() { return records; },
async push() {},
async delete() {},
};
const document = CultMesh.documentFromStore(store, noteDocument, {
documentId: "note:foreign.store",
});
const foreign = envelope("runtime.generated.cultmesh.note.ui.42", { noteId: "note:store", body: "foreign id, local shape" });
assert.deepEqual(await document.latest(), {
noteId: "note:foreign",
body: "foreign schema id, local store shape",
await assert.rejects(
CultMesh.documentFromStore(storeOf(foreign), noteDocument).latest(),
/did not contain schema cultmesh\.note\.v0/,
);
await assert.rejects(
CultMesh.documentFromStore(storeOf(foreign), "cultmesh.note.v0").latest(),
/did not contain schema cultmesh\.note\.v0/,
);
const own = envelope("cultmesh.note.v0", { noteId: "note:store", body: "own id" });
assert.equal((await CultMesh.documentFromStore(storeOf(foreign, own), noteDocument).latest()).body, "own id");
const renamed = defineDocumentType({
type: "cultmesh.note-renamed",
schemaId: "cultmesh.note-renamed.v1",
compatibleSchemaIds: ["cultmesh.note.v0"],
schema: noteDocument.schema,
});
assert.equal((await CultMesh.documentFromStore(storeOf(foreign, own), renamed).latest()).body, "own id");
await assert.rejects(CultMesh.documentFromStore(storeOf(foreign), renamed).latest(), /did not contain schema/);
});
test("CultMesh TS reactive documents submit predictions from explicit updates", async () => {
@ -3169,21 +3155,22 @@ test("CultMesh TS syncs remote RUDP snapshots into a local node", async () => {
}
});
test("CultMesh TS peer snapshot handles choose compatible foreign schema payloads", async () => {
// A peer snapshot is read, and synced, only under the requested type's schema id. A record the peer holds at the key under
// another id is another type's: it is not-found for this type, and syncing never writes it anywhere.
test("CultMesh TS peer snapshots never take another type's record at the key", async () => {
const connectionId = 0x10203049;
const node = await CultMesh.startNode(
const source = await CultMesh.startNode(
join(await mkdtemp(join(tmpdir(), "cultmesh-ts-rudp-foreign-snapshot-")), "node.ccmp"),
{
documents: [incompatibleNoteDocument, foreignNoteDocument],
},
{ documents: [incompatibleNoteDocument, foreignNoteDocument] },
);
await node.put(incompatibleNoteDocument, "note:foreign", {
nope: "not the requested shape",
});
await node.put(foreignNoteDocument, "note:foreign", {
noteId: "note:foreign",
body: "foreign schema id, local shape",
});
await source.put(incompatibleNoteDocument, "note:b", { nope: "remote b" });
// Parses as a note: shape routing would store it as one.
await source.put(foreignNoteDocument, "note:c", { noteId: "note:c", body: "remote c, note-shaped" });
const target = await CultMesh.startNode(
join(await mkdtemp(join(tmpdir(), "cultmesh-ts-rudp-foreign-target-")), "node.ccmp"),
{ documents: [noteDocument, incompatibleNoteDocument] },
);
await target.put(incompatibleNoteDocument, "note:b", { nope: "local b" });
const server = CultMesh.createRudpDocumentServer(
"cultmesh-ts-rudp-foreign-snapshot-server",
@ -3193,7 +3180,7 @@ test("CultMesh TS peer snapshot handles choose compatible foreign schema payload
defineCultNetDocumentBinding({ definition: incompatibleNoteDocument }),
defineCultNetDocumentBinding({ definition: foreignNoteDocument }),
]),
getCache: () => node.cache,
getCache: () => source.cache,
bindHost: "127.0.0.1",
bindPort: 0,
resendDelayMs: 25,
@ -3219,20 +3206,28 @@ test("CultMesh TS peer snapshot handles choose compatible foreign schema payload
},
);
const foreign = CultMesh.documentFromPeerSnapshot(
peer,
noteDocument,
"note:foreign",
{
documentId: "note:foreign.local",
timeoutMs: 1_000,
},
for (const key of ["note:b", "note:c"]) {
const notFound = new RegExp(`did not return cultmesh\\.note\\.v0 at ${key}`);
await assert.rejects(
CultMesh.documentFromPeerSnapshot(peer, noteDocument, key, { timeoutMs: 1_000 }).latest(),
notFound,
);
await assert.rejects(target.syncDocumentFromPeerSnapshot(peer, noteDocument, key, { timeoutMs: 1_000 }), notFound);
assert.equal(target.get(noteDocument, key), undefined, key);
}
// The failed sync of a note at "note:b" left the local record of the other type there untouched.
assert.deepEqual(target.getRequired(incompatibleNoteDocument, "note:b"), { nope: "local b" });
// HELD for the schema-naming fork: a handle named by a bare id string still takes the first record at the key, which
// is how Eve Electron reads the C# daemon's hash-stamped records.
assert.deepEqual(
await CultMesh.documentFromPeerSnapshot(peer, "cultmesh.note.v0", "note:c", { timeoutMs: 1_000 }).latest(),
{ noteId: "note:c", body: "remote c, note-shaped" },
);
// Its own type's record under its own id still syncs.
assert.deepEqual(
await target.syncDocumentFromPeerSnapshot(peer, incompatibleNoteDocument, "note:b", { timeoutMs: 1_000 }),
{ nope: "remote b" },
);
assert.deepEqual(await foreign.latest(), {
noteId: "note:foreign",
body: "foreign schema id, local shape",
});
} finally {
peer?.close();
server.close();