CultMesh TS: a catalog refuses a second type for a schema it holds, typed

F7 (operator, 2026-09-30): "Schema aliasing sounds like a really bad idea,
I support retiring it." A catalog indexed handles by schema id and by schema
name and version with plain map writes, so a handle of another type for a
held schema silently replaced it for every lookup by schema alone.

- Document and collection catalogs refuse, with the SchemaConflictError the
  registries use, a typed handle whose type differs from the one holding its
  schema id or schema name and version, or whose type already holds another
  schema id. Nothing is indexed when a handle is refused.
- Allowed: handles of one type for one schema (one per record), the same
  handle again, and a handle without a type, which claims none.
- Test: other-type same-id, other-type same-name-and-version and same-type
  other-id claimants, both orders, via add and via construction, for both
  catalogs; the first handle still answers afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 17:37:32 +02:00
commit 3763a53cef
2 changed files with 73 additions and 0 deletions

View file

@ -3,6 +3,7 @@ import { decode, encode } from "@msgpack/msgpack";
import {
CultCache,
SchemaConflictError,
SingleFileMessagePackBackingStore,
type AnyCultCacheDocumentDefinition,
type CacheBackingStore,
@ -1473,6 +1474,7 @@ export class CultMeshDocumentCatalog {
}
public add<TDocument>(document: CultMeshDocumentHandle<TDocument>): this {
cultMeshRefuseSecondCatalogType(document.schema, this.#byType, this.#bySchemaId, this.#bySchemaNameVersion);
this.#byDocumentId.set(document.documentId, document as CultMeshDocumentHandle<any>);
if (document.schema.type) {
this.#byType.set(document.schema.type, document as CultMeshDocumentHandle<any>);
@ -1684,6 +1686,7 @@ export class CultMeshCollectionCatalog {
}
public add<TDocument>(collection: CultMeshCollectionHandle<TDocument>): this {
cultMeshRefuseSecondCatalogType(collection.schema, this.#byType, this.#bySchemaId, this.#bySchemaNameVersion);
this.#byCollectionId.set(collection.collectionId, collection as CultMeshCollectionHandle<any>);
if (collection.schema.type) {
this.#byType.set(collection.schema.type, collection as CultMeshCollectionHandle<any>);
@ -6193,6 +6196,32 @@ function normalizeCultMeshDocumentSchema(
return normalized;
}
// One type owns a schema in a catalog, as in a registry. A handle whose type differs from the one already holding its
// schema id or schema name and version, or whose type already holds another schema, is refused. Handles of one type
// for one schema may repeat (one per record), and a handle without a type claims no type.
function cultMeshRefuseSecondCatalogType(
schema: CultMeshDocumentSchemaDescriptor,
byType: ReadonlyMap<string, { readonly schema: CultMeshDocumentSchemaDescriptor }>,
bySchemaId: ReadonlyMap<string, { readonly schema: CultMeshDocumentSchemaDescriptor }>,
bySchemaNameVersion: ReadonlyMap<string, { readonly schema: CultMeshDocumentSchemaDescriptor }>,
): void {
if (!schema.type) {
return;
}
const key = cultMeshSchemaNameVersionKey(schema);
const sameType = byType.get(schema.type)?.schema;
const holder =
(sameType?.schemaId && schema.schemaId && sameType.schemaId !== schema.schemaId ? sameType : undefined) ??
[schema.schemaId ? bySchemaId.get(schema.schemaId) : undefined, key ? bySchemaNameVersion.get(key) : undefined]
.map(held => held?.schema)
.find(held => held?.type !== undefined && held.type !== schema.type);
if (holder) {
const name = (held: CultMeshDocumentSchemaDescriptor) => held.schemaName ?? held.type ?? held.schemaId ?? "";
throw new SchemaConflictError(schema.schemaId ?? schema.type, [name(holder), name(schema)], "",
`CultMesh catalog schema ${cultMeshSchemaLabel(schema)} is held by type "${holder.type}" and cannot also be held by type "${schema.type}".`);
}
}
function cultMeshSchemaNameVersionKey(
schema: CultMeshDocumentSchemaDescriptor,
): string | undefined {

View file

@ -25,6 +25,7 @@ const noteDocument = defineDocumentType({
});
type Note = z.infer<typeof noteDocument.schema>;
type CultMeshDocumentSchemaDescriptorLike = { type?: string; schemaId?: string; schemaName?: string; schemaVersion?: string };
// Another type claiming noteDocument's schema. It is never registered beside noteDocument: one type owns a schema.
const otherTypeForNoteSchema = defineDocumentType({
@ -185,6 +186,49 @@ test("CultMesh TS catalogs infer a lookup's value type from its document definit
void typeOnly;
});
test("CultMesh TS catalogs hold one type per schema and refuse a second, typed", () => {
const sameNameVersion = { type: "cultmesh.note.named", schemaId: "sha256:note-named", schemaName: "cultmesh.note", schemaVersion: "cultmesh.note.v0" };
const sameTypeOtherSchema = { type: "cultmesh.note", schemaId: "cultmesh.note.v9" };
const note = { noteId: "note:held", body: "held" };
const kinds = [
{
catalog: (...schemas: CultMeshDocumentSchemaDescriptorLike[]) =>
CultMesh.documents(...schemas.map((schema, index) => CultMesh.document(`doc:${index}`, schema, async () => note))),
handle: (schema: CultMeshDocumentSchemaDescriptorLike, id: string) => CultMesh.document(id, schema, async () => note),
find: (catalog: any, schema: CultMeshDocumentSchemaDescriptorLike) => catalog.document(schema).documentId,
},
{
catalog: (...schemas: CultMeshDocumentSchemaDescriptorLike[]) =>
CultMesh.collections(...schemas.map((schema, index) => CultMesh.collection(`doc:${index}`, schema, async () => [note]))),
handle: (schema: CultMeshDocumentSchemaDescriptorLike, id: string) => CultMesh.collection(id, schema, async () => [note]),
find: (catalog: any, schema: CultMeshDocumentSchemaDescriptorLike) => catalog.collection(schema).collectionId,
},
];
for (const kind of kinds) {
for (const claimant of [otherTypeForNoteSchema, sameNameVersion, sameTypeOtherSchema]) {
for (const [first, second] of [[noteDocument, claimant], [claimant, noteDocument]] as const) {
const catalog = kind.catalog(first);
assert.throws(() => catalog.add(kind.handle(second, "doc:second") as any), (error: unknown) => {
assert.ok(error instanceof SchemaConflictError, String(error));
assert.equal(error.schemaId, second.schemaId);
assert.equal(error.recordKey, "");
return true;
});
assert.throws(() => kind.catalog(first, second), SchemaConflictError);
// The refused handle left nothing behind: the first type still answers.
assert.equal(kind.find(catalog, first), "doc:0");
assert.equal(kind.find(catalog, { schemaId: first.schemaId }), "doc:0");
}
}
// Handles of one type for one schema may repeat, the same handle may be added again, and an untyped handle claims no type.
const held = kind.handle(noteDocument, "doc:held");
const catalog = kind.catalog(noteDocument);
catalog.add(held as any).add(held as any);
catalog.add(kind.handle({ schemaId: "cultmesh.note.v0" }, "doc:untyped") as any);
assert.equal(kind.find(catalog, noteDocument), "doc:held");
}
});
test("CultMesh TS document handles submit predictions through configured authority hooks", async () => {
let current = {
noteId: "note:prediction",