@ixo/topic-protocol 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -0
- package/dist/schemas/action-receipt.schema.json +18 -0
- package/dist/schemas/agent-run.schema.json +18 -0
- package/dist/schemas/external-session-binding.schema.json +24 -0
- package/dist/schemas/room-policy.schema.json +15 -0
- package/dist/schemas/topic-capsule.schema.json +19 -0
- package/dist/schemas/topic-index-entry.schema.json +17 -0
- package/dist/schemas/topic-message.schema.json +18 -0
- package/dist/schemas/topic-operation.schema.json +19 -0
- package/dist/schemas/topic-preferences.schema.json +13 -0
- package/dist/schemas/topic-record.schema.json +18 -0
- package/dist/schemas/topic-relation.schema.json +16 -0
- package/dist/schemas/topic-root.schema.json +18 -0
- package/dist/schemas/topic-state.schema.json +19 -0
- package/dist/src/contracts/schemas.d.ts +569 -0
- package/dist/src/contracts/schemas.js +30 -0
- package/dist/src/contracts/schemas.js.map +1 -0
- package/dist/src/contracts/validator.d.ts +14 -0
- package/dist/src/contracts/validator.js +43 -0
- package/dist/src/contracts/validator.js.map +1 -0
- package/dist/src/errors.d.ts +6 -0
- package/dist/src/errors.js +11 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +12 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/matrix/anchors.d.ts +2 -0
- package/dist/src/matrix/anchors.js +55 -0
- package/dist/src/matrix/anchors.js.map +1 -0
- package/dist/src/matrix/content.d.ts +11 -0
- package/dist/src/matrix/content.js +64 -0
- package/dist/src/matrix/content.js.map +1 -0
- package/dist/src/matrix/events.d.ts +4 -0
- package/dist/src/matrix/events.js +94 -0
- package/dist/src/matrix/events.js.map +1 -0
- package/dist/src/matrix/pending.d.ts +22 -0
- package/dist/src/matrix/pending.js +83 -0
- package/dist/src/matrix/pending.js.map +1 -0
- package/dist/src/matrix/runtime.d.ts +18 -0
- package/dist/src/matrix/runtime.js +121 -0
- package/dist/src/matrix/runtime.js.map +1 -0
- package/dist/src/matrix/types.d.ts +84 -0
- package/dist/src/matrix/types.js +5 -0
- package/dist/src/matrix/types.js.map +1 -0
- package/dist/src/projector/payload.d.ts +27 -0
- package/dist/src/projector/payload.js +80 -0
- package/dist/src/projector/payload.js.map +1 -0
- package/dist/src/projector/prepare.d.ts +3 -0
- package/dist/src/projector/prepare.js +47 -0
- package/dist/src/projector/prepare.js.map +1 -0
- package/dist/src/projector/project.d.ts +2 -0
- package/dist/src/projector/project.js +89 -0
- package/dist/src/projector/project.js.map +1 -0
- package/dist/src/projector/state.d.ts +8 -0
- package/dist/src/projector/state.js +89 -0
- package/dist/src/projector/state.js.map +1 -0
- package/dist/src/projector/types.d.ts +76 -0
- package/dist/src/projector/types.js +19 -0
- package/dist/src/projector/types.js.map +1 -0
- package/dist/src/shared/compare.d.ts +2 -0
- package/dist/src/shared/compare.js +14 -0
- package/dist/src/shared/compare.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/matrix/types.ts"],"names":[],"mappings":"AAQA,MAAM,CAAC,MAAM,mBAAmB,GAAG,gBAAgB,CAAC;AACpD,MAAM,CAAC,MAAM,kBAAkB,GAAG,WAAW,CAAC;AAC9C,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AACnD,MAAM,CAAC,MAAM,0BAA0B,GAAG,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type TopicContext, type TopicOperation, type TopicStatus } from "./types.js";
|
|
2
|
+
interface DecisionPayload {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly summary: string;
|
|
5
|
+
}
|
|
6
|
+
export type ParsedPayload = {
|
|
7
|
+
readonly kind: "rename";
|
|
8
|
+
readonly title: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: "change-status";
|
|
11
|
+
readonly status: TopicStatus;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: "link-context" | "unlink-context";
|
|
14
|
+
readonly context: TopicContext;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: "record-decision";
|
|
17
|
+
readonly decision: DecisionPayload;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "redirect" | "branch" | "merge";
|
|
20
|
+
readonly targetTopicId: string;
|
|
21
|
+
} | {
|
|
22
|
+
readonly kind: "resolve-conflict";
|
|
23
|
+
readonly conflictRevision: string;
|
|
24
|
+
readonly selectedOperationId: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function parsePayload(operation: TopicOperation): ParsedPayload;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { TopicProtocolError } from "../errors.js";
|
|
2
|
+
import { TOPIC_STATUSES, } from "./types.js";
|
|
3
|
+
function invalid(operation, message) {
|
|
4
|
+
throw new TopicProtocolError("INVALID_OPERATION_PAYLOAD", `${operation.type} ${operation.operationId}: ${message}`, { operationId: operation.operationId, operationType: operation.type });
|
|
5
|
+
}
|
|
6
|
+
function recordValue(operation, key) {
|
|
7
|
+
const value = operation.payload[key];
|
|
8
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
9
|
+
return invalid(operation, `${key} must be an object`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function stringValue(operation, source, key) {
|
|
14
|
+
const value = source[key];
|
|
15
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
16
|
+
return invalid(operation, `${key} must be a non-empty string`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function topicIdValue(operation, key) {
|
|
21
|
+
const value = stringValue(operation, operation.payload, key);
|
|
22
|
+
if (!/^ixo:topic:[0-9a-f-]{36}$/u.test(value)) {
|
|
23
|
+
return invalid(operation, `${key} must be an ixo:topic UUID URI`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function contextValue(operation) {
|
|
28
|
+
const context = recordValue(operation, "context");
|
|
29
|
+
return {
|
|
30
|
+
type: stringValue(operation, context, "type"),
|
|
31
|
+
id: stringValue(operation, context, "id"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function decisionValue(operation) {
|
|
35
|
+
const decision = recordValue(operation, "decision");
|
|
36
|
+
return {
|
|
37
|
+
id: stringValue(operation, decision, "id"),
|
|
38
|
+
summary: stringValue(operation, decision, "summary"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function statusValue(operation) {
|
|
42
|
+
const status = stringValue(operation, operation.payload, "status");
|
|
43
|
+
if (!TOPIC_STATUSES.includes(status)) {
|
|
44
|
+
return invalid(operation, `status is not supported: ${status}`);
|
|
45
|
+
}
|
|
46
|
+
return status;
|
|
47
|
+
}
|
|
48
|
+
function titleValue(operation) {
|
|
49
|
+
const title = stringValue(operation, operation.payload, "title");
|
|
50
|
+
if (title.length > 280)
|
|
51
|
+
return invalid(operation, "title must be at most 280 characters");
|
|
52
|
+
return title;
|
|
53
|
+
}
|
|
54
|
+
function resolutionValue(operation) {
|
|
55
|
+
return {
|
|
56
|
+
kind: "resolve-conflict",
|
|
57
|
+
conflictRevision: stringValue(operation, operation.payload, "conflictRevision"),
|
|
58
|
+
selectedOperationId: stringValue(operation, operation.payload, "selectedOperationId"),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function parsePayload(operation) {
|
|
62
|
+
switch (operation.type) {
|
|
63
|
+
case "rename":
|
|
64
|
+
return { kind: operation.type, title: titleValue(operation) };
|
|
65
|
+
case "change-status":
|
|
66
|
+
return { kind: operation.type, status: statusValue(operation) };
|
|
67
|
+
case "link-context":
|
|
68
|
+
case "unlink-context":
|
|
69
|
+
return { kind: operation.type, context: contextValue(operation) };
|
|
70
|
+
case "record-decision":
|
|
71
|
+
return { kind: operation.type, decision: decisionValue(operation) };
|
|
72
|
+
case "redirect":
|
|
73
|
+
case "branch":
|
|
74
|
+
case "merge":
|
|
75
|
+
return { kind: operation.type, targetTopicId: topicIdValue(operation, "targetTopicId") };
|
|
76
|
+
case "resolve-conflict":
|
|
77
|
+
return resolutionValue(operation);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=payload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payload.js","sourceRoot":"","sources":["../../../src/projector/payload.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { assertContract } from "../contracts/validator.js";
|
|
2
|
+
import { TopicProtocolError } from "../errors.js";
|
|
3
|
+
import { compareText } from "../shared/compare.js";
|
|
4
|
+
import { parsePayload } from "./payload.js";
|
|
5
|
+
function sortedValue(value) {
|
|
6
|
+
if (Array.isArray(value))
|
|
7
|
+
return value.map(sortedValue);
|
|
8
|
+
if (typeof value !== "object" || value === null)
|
|
9
|
+
return value;
|
|
10
|
+
const entries = Object.entries(value)
|
|
11
|
+
.sort(([left], [right]) => compareText(left, right))
|
|
12
|
+
.map(([key, item]) => [key, sortedValue(item)]);
|
|
13
|
+
return Object.fromEntries(entries);
|
|
14
|
+
}
|
|
15
|
+
function canonicalJson(value) {
|
|
16
|
+
return JSON.stringify(sortedValue(value));
|
|
17
|
+
}
|
|
18
|
+
function prepareOperation(operation, topicId) {
|
|
19
|
+
assertContract("topic-operation", operation);
|
|
20
|
+
if (operation.topicId !== topicId) {
|
|
21
|
+
throw new TopicProtocolError("TOPIC_MISMATCH", `${operation.operationId} targets ${operation.topicId}, expected ${topicId}`, { operationId: operation.operationId, topicId: operation.topicId, expectedTopicId: topicId });
|
|
22
|
+
}
|
|
23
|
+
return { ...operation, parsedPayload: parsePayload(operation) };
|
|
24
|
+
}
|
|
25
|
+
function deduplicateGroup(group) {
|
|
26
|
+
const first = group[0];
|
|
27
|
+
if (first === undefined)
|
|
28
|
+
throw new Error("Operation group cannot be empty");
|
|
29
|
+
const fingerprints = new Set(group.map((operation) => canonicalJson(operation)));
|
|
30
|
+
if (fingerprints.size > 1) {
|
|
31
|
+
throw new TopicProtocolError("DUPLICATE_OPERATION", `Operation ID ${first.operationId} has divergent contents`, { operationId: first.operationId });
|
|
32
|
+
}
|
|
33
|
+
return first;
|
|
34
|
+
}
|
|
35
|
+
export function prepareOperations(operations, topicId) {
|
|
36
|
+
const groups = new Map();
|
|
37
|
+
for (const operation of operations) {
|
|
38
|
+
const prepared = prepareOperation(operation, topicId);
|
|
39
|
+
const group = groups.get(prepared.operationId) ?? [];
|
|
40
|
+
group.push(prepared);
|
|
41
|
+
groups.set(prepared.operationId, group);
|
|
42
|
+
}
|
|
43
|
+
return [...groups.values()]
|
|
44
|
+
.map(deduplicateGroup)
|
|
45
|
+
.sort((left, right) => compareText(left.operationId, right.operationId));
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=prepare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prepare.js","sourceRoot":"","sources":["../../../src/projector/prepare.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { assertContract } from "../contracts/validator.js";
|
|
2
|
+
import { TopicProtocolError } from "../errors.js";
|
|
3
|
+
import { compareText } from "../shared/compare.js";
|
|
4
|
+
import { prepareOperations } from "./prepare.js";
|
|
5
|
+
import { applyOperation, initialState, } from "./state.js";
|
|
6
|
+
function validateAnchor(roomId, rootEventId) {
|
|
7
|
+
if (roomId.length < 2 || rootEventId.length < 2 || !roomId.startsWith("!") || !rootEventId.startsWith("$")) {
|
|
8
|
+
throw new TopicProtocolError("INVALID_ANCHOR", "Matrix anchors require a room ID and a server-assigned root event ID", { roomId, rootEventId });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function authorise(operations, authorize) {
|
|
12
|
+
const authorised = [];
|
|
13
|
+
const ignored = [];
|
|
14
|
+
for (const operation of operations) {
|
|
15
|
+
try {
|
|
16
|
+
if (authorize(operation))
|
|
17
|
+
authorised.push(operation);
|
|
18
|
+
else
|
|
19
|
+
ignored.push({ operationId: operation.operationId, reason: "unauthorised" });
|
|
20
|
+
}
|
|
21
|
+
catch (cause) {
|
|
22
|
+
throw new TopicProtocolError("AUTHORIZATION_FAILED", `Authorisation failed for ${operation.operationId}`, { operationId: operation.operationId }, { cause });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { authorised, ignored };
|
|
26
|
+
}
|
|
27
|
+
function indexByPreviousRevision(operations) {
|
|
28
|
+
const index = new Map();
|
|
29
|
+
for (const operation of operations) {
|
|
30
|
+
const group = index.get(operation.previousRevision) ?? [];
|
|
31
|
+
group.push(operation);
|
|
32
|
+
group.sort((left, right) => compareText(left.operationId, right.operationId));
|
|
33
|
+
index.set(operation.previousRevision, group);
|
|
34
|
+
}
|
|
35
|
+
return index;
|
|
36
|
+
}
|
|
37
|
+
function conflictFor(previousRevision, contenders) {
|
|
38
|
+
const [canonical, ...competing] = contenders;
|
|
39
|
+
if (canonical === undefined || competing.length === 0)
|
|
40
|
+
return undefined;
|
|
41
|
+
return {
|
|
42
|
+
previousRevision,
|
|
43
|
+
canonicalOperationId: canonical.operationId,
|
|
44
|
+
competingOperationIds: competing.map((operation) => operation.operationId),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function traverse(initial, operations) {
|
|
48
|
+
const index = indexByPreviousRevision(operations);
|
|
49
|
+
const applied = [];
|
|
50
|
+
const ignored = [];
|
|
51
|
+
let state = initial;
|
|
52
|
+
let contenders = index.get(state.revision) ?? [];
|
|
53
|
+
while (contenders.length > 0) {
|
|
54
|
+
const [canonical, ...competing] = contenders;
|
|
55
|
+
if (canonical === undefined)
|
|
56
|
+
break;
|
|
57
|
+
const conflict = conflictFor(state.revision, contenders);
|
|
58
|
+
if (conflict !== undefined)
|
|
59
|
+
state = { ...state, conflicts: [...state.conflicts, conflict] };
|
|
60
|
+
ignored.push(...competing.map((operation) => ({ operationId: operation.operationId, reason: "conflicting-branch" })));
|
|
61
|
+
state = applyOperation(state, canonical);
|
|
62
|
+
applied.push(canonical.operationId);
|
|
63
|
+
contenders = index.get(state.revision) ?? [];
|
|
64
|
+
}
|
|
65
|
+
return appendUnreachable({ state, applied, ignored }, operations);
|
|
66
|
+
}
|
|
67
|
+
function appendUnreachable(result, operations) {
|
|
68
|
+
const handled = new Set([...result.applied, ...result.ignored.map(({ operationId }) => operationId)]);
|
|
69
|
+
const unreachable = operations
|
|
70
|
+
.filter((operation) => !handled.has(operation.operationId))
|
|
71
|
+
.map((operation) => ({ operationId: operation.operationId, reason: "unreachable-revision" }));
|
|
72
|
+
return { ...result, ignored: [...result.ignored, ...unreachable] };
|
|
73
|
+
}
|
|
74
|
+
function sortedIgnored(ignored) {
|
|
75
|
+
return [...ignored].sort((left, right) => compareText(left.operationId, right.operationId) || compareText(left.reason, right.reason));
|
|
76
|
+
}
|
|
77
|
+
export function projectTopic(input) {
|
|
78
|
+
assertContract("topic-root", input.root);
|
|
79
|
+
validateAnchor(input.anchor.roomId, input.anchor.rootEventId);
|
|
80
|
+
const prepared = prepareOperations(input.operations, input.root.id);
|
|
81
|
+
const authorisation = authorise(prepared, input.authorize);
|
|
82
|
+
const traversal = traverse(initialState(input.root, input.anchor), authorisation.authorised);
|
|
83
|
+
return {
|
|
84
|
+
...traversal.state,
|
|
85
|
+
appliedOperationIds: traversal.applied,
|
|
86
|
+
ignoredOperations: sortedIgnored([...authorisation.ignored, ...traversal.ignored]),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=project.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project.js","sourceRoot":"","sources":["../../../src/projector/project.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ParsedPayload } from "./payload.js";
|
|
2
|
+
import type { MatrixAnchor, TopicOperation, TopicProjection, TopicRoot } from "./types.js";
|
|
3
|
+
export type ProjectionState = Omit<TopicProjection, "appliedOperationIds" | "ignoredOperations">;
|
|
4
|
+
export interface PreparedOperation extends TopicOperation {
|
|
5
|
+
readonly parsedPayload: ParsedPayload;
|
|
6
|
+
}
|
|
7
|
+
export declare function initialState(root: TopicRoot, anchor: MatrixAnchor): ProjectionState;
|
|
8
|
+
export declare function applyOperation(state: ProjectionState, operation: PreparedOperation): ProjectionState;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { TopicProtocolError } from "../errors.js";
|
|
2
|
+
import { compareText } from "../shared/compare.js";
|
|
3
|
+
export function initialState(root, anchor) {
|
|
4
|
+
return {
|
|
5
|
+
topicId: root.id,
|
|
6
|
+
roomId: anchor.roomId,
|
|
7
|
+
rootEventId: anchor.rootEventId,
|
|
8
|
+
revision: anchor.rootEventId,
|
|
9
|
+
title: root.title,
|
|
10
|
+
status: root.status,
|
|
11
|
+
context: uniqueContexts(root.context ?? []),
|
|
12
|
+
decisions: [],
|
|
13
|
+
relations: [],
|
|
14
|
+
conflicts: [],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function contextKey(context) {
|
|
18
|
+
return `${context.type}\u0000${context.id}`;
|
|
19
|
+
}
|
|
20
|
+
function uniqueContexts(contexts) {
|
|
21
|
+
const values = new Map(contexts.map((context) => [contextKey(context), { ...context }]));
|
|
22
|
+
return [...values.values()].sort((left, right) => compareText(contextKey(left), contextKey(right)));
|
|
23
|
+
}
|
|
24
|
+
function withRelation(state, operation, type, targetTopicId) {
|
|
25
|
+
const relation = { type, targetTopicId, operationId: operation.operationId };
|
|
26
|
+
return { ...state, relations: [...state.relations, relation] };
|
|
27
|
+
}
|
|
28
|
+
function applyContext(state, payload) {
|
|
29
|
+
if (payload.kind !== "link-context" && payload.kind !== "unlink-context")
|
|
30
|
+
return state;
|
|
31
|
+
const key = contextKey(payload.context);
|
|
32
|
+
const contexts = state.context.filter((context) => contextKey(context) !== key);
|
|
33
|
+
return {
|
|
34
|
+
...state,
|
|
35
|
+
context: payload.kind === "link-context" ? uniqueContexts([...contexts, payload.context]) : contexts,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function applyDecision(state, operation) {
|
|
39
|
+
const payload = operation.parsedPayload;
|
|
40
|
+
if (payload.kind !== "record-decision")
|
|
41
|
+
return state;
|
|
42
|
+
const retained = state.decisions.filter((decision) => decision.id !== payload.decision.id);
|
|
43
|
+
const decision = { ...payload.decision, operationId: operation.operationId };
|
|
44
|
+
return { ...state, decisions: [...retained, decision] };
|
|
45
|
+
}
|
|
46
|
+
function applyRedirect(state, operation) {
|
|
47
|
+
const payload = operation.parsedPayload;
|
|
48
|
+
if (payload.kind !== "redirect")
|
|
49
|
+
return state;
|
|
50
|
+
const redirected = withRelation(state, operation, "redirects_to", payload.targetTopicId);
|
|
51
|
+
return { ...redirected, status: "redirected", redirectTo: payload.targetTopicId };
|
|
52
|
+
}
|
|
53
|
+
function applyGraphRelation(state, operation) {
|
|
54
|
+
const payload = operation.parsedPayload;
|
|
55
|
+
if (payload.kind === "branch") {
|
|
56
|
+
return withRelation(state, operation, "branches_from", payload.targetTopicId);
|
|
57
|
+
}
|
|
58
|
+
if (payload.kind === "merge") {
|
|
59
|
+
return withRelation(state, operation, "merged_into", payload.targetTopicId);
|
|
60
|
+
}
|
|
61
|
+
return state;
|
|
62
|
+
}
|
|
63
|
+
function applyResolution(state, payload) {
|
|
64
|
+
if (payload.kind !== "resolve-conflict")
|
|
65
|
+
return state;
|
|
66
|
+
const conflict = state.conflicts.find((candidate) => candidate.previousRevision === payload.conflictRevision);
|
|
67
|
+
if (conflict === undefined) {
|
|
68
|
+
throw new TopicProtocolError("UNKNOWN_CONFLICT", `Unknown conflict: ${payload.conflictRevision}`);
|
|
69
|
+
}
|
|
70
|
+
if (conflict.canonicalOperationId !== payload.selectedOperationId) {
|
|
71
|
+
throw new TopicProtocolError("NON_CANONICAL_CONFLICT_SELECTION", "Selecting a non-canonical branch requires an explicit merge operation", { selectedOperationId: payload.selectedOperationId });
|
|
72
|
+
}
|
|
73
|
+
return { ...state, conflicts: state.conflicts.filter((candidate) => candidate !== conflict) };
|
|
74
|
+
}
|
|
75
|
+
export function applyOperation(state, operation) {
|
|
76
|
+
const payload = operation.parsedPayload;
|
|
77
|
+
let updated = state;
|
|
78
|
+
if (payload.kind === "rename")
|
|
79
|
+
updated = { ...updated, title: payload.title };
|
|
80
|
+
if (payload.kind === "change-status")
|
|
81
|
+
updated = { ...updated, status: payload.status };
|
|
82
|
+
updated = applyContext(updated, payload);
|
|
83
|
+
updated = applyDecision(updated, operation);
|
|
84
|
+
updated = applyGraphRelation(updated, operation);
|
|
85
|
+
updated = applyRedirect(updated, operation);
|
|
86
|
+
updated = applyResolution(updated, payload);
|
|
87
|
+
return { ...updated, revision: operation.operationId };
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.js","sourceRoot":"","sources":["../../../src/projector/state.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export declare const TOPIC_STATUSES: readonly ["draft", "active", "waiting", "blocked", "resolved", "archived", "redirected"];
|
|
2
|
+
export declare const TOPIC_KINDS: readonly ["discussion", "question", "decision", "proposal", "task", "incident", "flow"];
|
|
3
|
+
export type TopicStatus = (typeof TOPIC_STATUSES)[number];
|
|
4
|
+
export type TopicKind = (typeof TOPIC_KINDS)[number];
|
|
5
|
+
export type TopicOperationType = "rename" | "change-status" | "link-context" | "unlink-context" | "record-decision" | "redirect" | "branch" | "merge" | "resolve-conflict";
|
|
6
|
+
export interface TopicContext {
|
|
7
|
+
readonly type: string;
|
|
8
|
+
readonly id: string;
|
|
9
|
+
}
|
|
10
|
+
export interface TopicRoot {
|
|
11
|
+
readonly version: 1;
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly title: string;
|
|
14
|
+
readonly kind?: TopicKind;
|
|
15
|
+
readonly status: TopicStatus;
|
|
16
|
+
readonly createdBy: string;
|
|
17
|
+
readonly createdAt: string;
|
|
18
|
+
readonly context?: readonly TopicContext[];
|
|
19
|
+
}
|
|
20
|
+
export interface MatrixAnchor {
|
|
21
|
+
readonly roomId: string;
|
|
22
|
+
readonly rootEventId: string;
|
|
23
|
+
}
|
|
24
|
+
export interface TopicOperation {
|
|
25
|
+
readonly version: 1;
|
|
26
|
+
readonly operationId: string;
|
|
27
|
+
readonly topicId: string;
|
|
28
|
+
readonly type: TopicOperationType;
|
|
29
|
+
readonly previousRevision: string;
|
|
30
|
+
readonly actor: string;
|
|
31
|
+
readonly createdAt: string;
|
|
32
|
+
readonly payload: Readonly<Record<string, unknown>>;
|
|
33
|
+
readonly capability?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface TopicDecision {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly summary: string;
|
|
38
|
+
readonly operationId: string;
|
|
39
|
+
}
|
|
40
|
+
export type TopicRelationType = "branches_from" | "merged_into" | "redirects_to";
|
|
41
|
+
export interface ProjectedTopicRelation {
|
|
42
|
+
readonly type: TopicRelationType;
|
|
43
|
+
readonly targetTopicId: string;
|
|
44
|
+
readonly operationId: string;
|
|
45
|
+
}
|
|
46
|
+
export interface TopicProjectionConflict {
|
|
47
|
+
readonly previousRevision: string;
|
|
48
|
+
readonly canonicalOperationId: string;
|
|
49
|
+
readonly competingOperationIds: readonly string[];
|
|
50
|
+
}
|
|
51
|
+
export type IgnoredOperationReason = "conflicting-branch" | "unauthorised" | "unreachable-revision";
|
|
52
|
+
export interface IgnoredOperation {
|
|
53
|
+
readonly operationId: string;
|
|
54
|
+
readonly reason: IgnoredOperationReason;
|
|
55
|
+
}
|
|
56
|
+
export interface TopicProjection {
|
|
57
|
+
readonly topicId: string;
|
|
58
|
+
readonly roomId: string;
|
|
59
|
+
readonly rootEventId: string;
|
|
60
|
+
readonly revision: string;
|
|
61
|
+
readonly title: string;
|
|
62
|
+
readonly status: TopicStatus;
|
|
63
|
+
readonly context: readonly TopicContext[];
|
|
64
|
+
readonly decisions: readonly TopicDecision[];
|
|
65
|
+
readonly relations: readonly ProjectedTopicRelation[];
|
|
66
|
+
readonly redirectTo?: string;
|
|
67
|
+
readonly conflicts: readonly TopicProjectionConflict[];
|
|
68
|
+
readonly appliedOperationIds: readonly string[];
|
|
69
|
+
readonly ignoredOperations: readonly IgnoredOperation[];
|
|
70
|
+
}
|
|
71
|
+
export interface ProjectionInput {
|
|
72
|
+
readonly root: TopicRoot;
|
|
73
|
+
readonly anchor: MatrixAnchor;
|
|
74
|
+
readonly operations: readonly TopicOperation[];
|
|
75
|
+
readonly authorize: (operation: TopicOperation) => boolean;
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const TOPIC_STATUSES = [
|
|
2
|
+
"draft",
|
|
3
|
+
"active",
|
|
4
|
+
"waiting",
|
|
5
|
+
"blocked",
|
|
6
|
+
"resolved",
|
|
7
|
+
"archived",
|
|
8
|
+
"redirected",
|
|
9
|
+
];
|
|
10
|
+
export const TOPIC_KINDS = [
|
|
11
|
+
"discussion",
|
|
12
|
+
"question",
|
|
13
|
+
"decision",
|
|
14
|
+
"proposal",
|
|
15
|
+
"task",
|
|
16
|
+
"incident",
|
|
17
|
+
"flow",
|
|
18
|
+
];
|
|
19
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/projector/types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,OAAO;IACP,QAAQ;IACR,SAAS;IACT,SAAS;IACT,UAAU;IACV,UAAU;IACV,YAAY;CACJ,CAAC;AACX,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,YAAY;IACZ,UAAU;IACV,UAAU;IACV,UAAU;IACV,MAAM;IACN,UAAU;IACV,MAAM;CACE,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const encoder = new TextEncoder();
|
|
2
|
+
/** Compare protocol identifiers by UTF-8 bytes, independent of locale. */
|
|
3
|
+
export function compareText(left, right) {
|
|
4
|
+
const leftBytes = encoder.encode(left);
|
|
5
|
+
const rightBytes = encoder.encode(right);
|
|
6
|
+
const length = Math.min(leftBytes.length, rightBytes.length);
|
|
7
|
+
for (let index = 0; index < length; index += 1) {
|
|
8
|
+
const difference = (leftBytes[index] ?? 0) - (rightBytes[index] ?? 0);
|
|
9
|
+
if (difference !== 0)
|
|
10
|
+
return difference;
|
|
11
|
+
}
|
|
12
|
+
return leftBytes.length - rightBytes.length;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=compare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.js","sourceRoot":"","sources":["../../../src/shared/compare.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAElC,0EAA0E;AAC1E,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,KAAa;IACrD,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IAC7D,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC/C,MAAM,UAAU,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACtE,IAAI,UAAU,KAAK,CAAC;YAAE,OAAO,UAAU,CAAC;IAC1C,CAAC;IACD,OAAO,SAAS,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;AAC9C,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ixo/topic-protocol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reference contracts and deterministic projection for IXO Topics",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ixoworld/topic-protocol.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/ixoworld/topic-protocol#readme",
|
|
12
|
+
"bugs": "https://github.com/ixoworld/topic-protocol/issues",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/src/index.js",
|
|
17
|
+
"types": "./dist/src/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/src/index.d.ts",
|
|
25
|
+
"import": "./dist/src/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./schemas/*": "./dist/schemas/*"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.build.json",
|
|
31
|
+
"check": "npm run lint && npm run function-size && npm run typecheck && npm run test && npm run duplication && npm run build && npm run package:check && python3 scripts/validate_protocol.py",
|
|
32
|
+
"duplication": "jscpd src test scripts test_python --threshold 0 --min-lines 6 --min-tokens 60 --reporters console",
|
|
33
|
+
"function-size": "python3 scripts/check_python_function_lengths.py",
|
|
34
|
+
"lint": "eslint .",
|
|
35
|
+
"package:check": "npm pack --dry-run --ignore-scripts --cache ./node_modules/.cache/npm",
|
|
36
|
+
"prepack": "npm run build",
|
|
37
|
+
"prepare": "npm run build",
|
|
38
|
+
"test": "npm run test:ts && npm run test:python",
|
|
39
|
+
"test:python": "python3 -m unittest discover -s test_python -p 'test_*.py'",
|
|
40
|
+
"test:ts": "vitest run",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"ajv": "^8.17.1",
|
|
46
|
+
"ajv-formats": "^3.0.1",
|
|
47
|
+
"uuid": "^11.1.1"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@eslint/js": "^9.39.1",
|
|
51
|
+
"@types/node": "^24.10.1",
|
|
52
|
+
"eslint": "^9.39.1",
|
|
53
|
+
"jscpd": "^4.0.8",
|
|
54
|
+
"typescript": "^5.9.3",
|
|
55
|
+
"typescript-eslint": "^8.48.1",
|
|
56
|
+
"vitest": "^4.0.15"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=22"
|
|
60
|
+
}
|
|
61
|
+
}
|