@rikaidev/yomi 0.5.0 → 0.6.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.
Files changed (62) hide show
  1. package/README.md +28 -0
  2. package/dist/ambient/ambient-event-graph.js +172 -0
  3. package/dist/ambient/ambient-observer.js +71 -0
  4. package/dist/ambient/event-graph-reducer.js +300 -0
  5. package/dist/ambient/ingestion-adapter.js +203 -0
  6. package/dist/ambient/journal/crypto.js +58 -0
  7. package/dist/ambient/journal/journal-store.js +284 -0
  8. package/dist/ambient/journal/types.js +7 -0
  9. package/dist/ambient/summaries/retriever.js +55 -0
  10. package/dist/ambient/summaries/summary-engine.js +128 -0
  11. package/dist/ambient/summaries/types.js +4 -0
  12. package/dist/attention/policy.js +22 -16
  13. package/dist/auth/credential-store.js +15 -6
  14. package/dist/cli/query.js +276 -69
  15. package/dist/cli/work-item.js +24 -0
  16. package/dist/line/client/auth-service/client.js +7 -8
  17. package/dist/line/client/buddy-service/client.js +73 -0
  18. package/dist/line/client/index.js +28 -1
  19. package/dist/line/client/obs-media-client.js +2 -1
  20. package/dist/line/client/square-service/client.js +298 -0
  21. package/dist/line/client/talk-service/command-client.js +9 -1
  22. package/dist/line/client/talk-service/message-id-query.js +27 -3
  23. package/dist/line/client/talk-service/requests.js +9 -0
  24. package/dist/line/core/auth-session-runtime.js +1 -1
  25. package/dist/line/core/chat-event.js +2 -0
  26. package/dist/line/core/chat-runtime-service.js +18 -3
  27. package/dist/line/core/config.js +1 -0
  28. package/dist/line/core/flex-message.js +323 -0
  29. package/dist/line/core/service.js +0 -1
  30. package/dist/mcp/handlers/contacts.js +12 -6
  31. package/dist/mcp/handlers/conversations.js +54 -0
  32. package/dist/mcp/handlers/send.js +8 -11
  33. package/dist/mcp/media.js +101 -2
  34. package/dist/mcp/names.js +78 -6
  35. package/dist/search/default-embedder.js +12 -3
  36. package/dist/version.js +1 -1
  37. package/dist/work-item/adapter.js +1 -0
  38. package/dist/work-item/destination-contracts.js +1 -0
  39. package/dist/work-item/event-context.js +73 -0
  40. package/dist/work-item/gold-contract.js +101 -0
  41. package/dist/work-item/gold-coverage.js +96 -0
  42. package/dist/work-item/gold-derivation.js +67 -0
  43. package/dist/work-item/gold-split.js +143 -0
  44. package/dist/work-item/index.js +23 -0
  45. package/dist/work-item/line-adapter.js +111 -0
  46. package/dist/work-item/llama-cpp-work-event-adapter.js +239 -0
  47. package/dist/work-item/local-provider.js +38 -0
  48. package/dist/work-item/local-work-event-provider.js +56 -0
  49. package/dist/work-item/onnx-work-event-adapter.js +223 -0
  50. package/dist/work-item/provider.js +21 -0
  51. package/dist/work-item/registry.js +19 -0
  52. package/dist/work-item/service.js +85 -0
  53. package/dist/work-item/store.js +97 -0
  54. package/dist/work-item/types.js +79 -0
  55. package/dist/work-item/work-event-graph-engine.js +63 -0
  56. package/dist/work-item/work-event-inference.js +267 -0
  57. package/dist/work-item/work-event-matcher.js +105 -0
  58. package/dist/work-item/work-event-model-manifest.js +39 -0
  59. package/dist/work-item/work-event-store.js +125 -0
  60. package/dist/work-item/work-event.js +123 -0
  61. package/package.json +24 -3
  62. package/run.mjs +162 -9
package/dist/mcp/names.js CHANGED
@@ -12,6 +12,20 @@
12
12
  * process lifetime. Unresolvable MIDs are never given a fabricated
13
13
  * placeholder — callers get `null` and may fall back to the raw MID.
14
14
  */
15
+ /**
16
+ * The name the authenticated user sees for a contact in the LINE app.
17
+ *
18
+ * LINE lets users rename friends locally; getContacts returns that name as
19
+ * `displayNameOverridden` alongside the friend's own `displayName`. The LINE
20
+ * app shows the override when present, so Yomi should too — otherwise names
21
+ * an agent reports don't match what the user sees.
22
+ *
23
+ * @param contact - Normalized contact from getContacts (may be null).
24
+ * @returns The user-set name, else the friend's own display name, else null.
25
+ */
26
+ export function contactDisplayName(contact) {
27
+ return contact?.displayNameOverridden || contact?.displayName || null;
28
+ }
15
29
  /**
16
30
  * Resolve display names for LINE user (contact) MIDs.
17
31
  *
@@ -35,9 +49,10 @@ export async function resolveUserNames(service, mids) {
35
49
  if (missing.length > 0) {
36
50
  const contacts = await service.client.getContacts(missing);
37
51
  for (const contact of contacts) {
38
- if (contact?.mid && contact.displayName) {
39
- service.nameCache.set(contact.mid, contact.displayName);
40
- resolved.set(contact.mid, contact.displayName);
52
+ const name = contactDisplayName(contact);
53
+ if (contact?.mid && name) {
54
+ service.nameCache.set(contact.mid, name);
55
+ resolved.set(contact.mid, name);
41
56
  }
42
57
  }
43
58
  }
@@ -106,6 +121,59 @@ async function resolveGroupNames(service, chatIds) {
106
121
  }
107
122
  return resolved;
108
123
  }
124
+ function mentionedUserMids(metadata) {
125
+ try {
126
+ const value = typeof metadata?.MENTION === 'string'
127
+ ? JSON.parse(metadata.MENTION)
128
+ : metadata?.MENTION;
129
+ const mentions = value?.MENTIONEES ?? value?.MENTIONS ?? [];
130
+ return mentions
131
+ .map((item) => item?.M ?? item?.mid)
132
+ .filter((mid) => typeof mid === 'string' && mid.startsWith('u'));
133
+ }
134
+ catch {
135
+ return [];
136
+ }
137
+ }
138
+ /** Legacy rooms have no chatName; LINE presents their participant names. */
139
+ async function resolveRoomNames(service, roomIds) {
140
+ const resolved = new Map();
141
+ await Promise.all(Array.from(new Set(roomIds.filter(Boolean))).map(async (roomId) => {
142
+ const cached = service.chatCache.get(roomId);
143
+ if (cached?.chatName) {
144
+ resolved.set(roomId, cached.chatName);
145
+ return;
146
+ }
147
+ try {
148
+ const messages = await service.getRecentMessages(roomId, 50);
149
+ const selfMid = service.profile?.mid;
150
+ const participantMids = Array.from(new Set(messages
151
+ .flatMap((message) => [
152
+ message.from,
153
+ ...mentionedUserMids(message.contentMetadata),
154
+ ])
155
+ .filter((mid) => typeof mid === 'string' &&
156
+ mid.startsWith('u') &&
157
+ mid !== selfMid)));
158
+ const names = await resolveUserNames(service, participantMids);
159
+ const title = participantMids
160
+ .map((mid) => names.get(mid))
161
+ .filter((name) => Boolean(name))
162
+ .slice(0, 4)
163
+ .join('、');
164
+ if (!title)
165
+ return;
166
+ resolved.set(roomId, title);
167
+ service.chatCache.set(roomId, {
168
+ ...(cached ?? {}),
169
+ chatMid: roomId,
170
+ chatName: title,
171
+ });
172
+ }
173
+ catch { }
174
+ }));
175
+ return resolved;
176
+ }
109
177
  /**
110
178
  * Resolve a human-readable name for each conversation id, dispatching by
111
179
  * LINE's MID prefix convention (u = 1:1 user, c = group, r = room).
@@ -116,18 +184,22 @@ async function resolveGroupNames(service, chatIds) {
116
184
  */
117
185
  export async function resolveConversationNames(service, chatIds) {
118
186
  const userIds = chatIds.filter((id) => id?.startsWith('u'));
119
- const groupIds = chatIds.filter((id) => id?.startsWith('c') || id?.startsWith('r'));
120
- const [userNames, groupNames] = await Promise.all([
187
+ const groupIds = chatIds.filter((id) => id?.startsWith('c'));
188
+ const roomIds = chatIds.filter((id) => id?.startsWith('r'));
189
+ const [userNames, groupNames, roomNames] = await Promise.all([
121
190
  userIds.length > 0
122
191
  ? resolveUserNames(service, userIds)
123
192
  : Promise.resolve(new Map()),
124
193
  groupIds.length > 0
125
194
  ? resolveGroupNames(service, groupIds)
126
195
  : Promise.resolve(new Map()),
196
+ roomIds.length > 0
197
+ ? resolveRoomNames(service, roomIds)
198
+ : Promise.resolve(new Map()),
127
199
  ]);
128
200
  const result = new Map();
129
201
  for (const id of chatIds) {
130
- result.set(id, userNames.get(id) ?? groupNames.get(id) ?? null);
202
+ result.set(id, userNames.get(id) ?? groupNames.get(id) ?? roomNames.get(id) ?? null);
131
203
  }
132
204
  return result;
133
205
  }
@@ -22,6 +22,8 @@
22
22
  * instruction prefix, stored documents do not. Using the model's official
23
23
  * recommended instruction for retrieval tasks.
24
24
  */
25
+ import { homedir } from 'node:os';
26
+ import { join } from 'node:path';
25
27
  const MODEL_ID = 'Xenova/bge-small-zh-v1.5';
26
28
  const BGE_QUERY_INSTRUCTION = '为这个句子生成表示以用于检索相关文章:';
27
29
  let pipelinePromise = null;
@@ -35,9 +37,16 @@ let pipelinePromise = null;
35
37
  function getPipeline() {
36
38
  if (!pipelinePromise) {
37
39
  pipelinePromise = import('@huggingface/transformers')
38
- .then(({ pipeline }) => pipeline('feature-extraction', MODEL_ID, {
39
- device: 'cpu',
40
- }))
40
+ .then(({ pipeline, env }) => {
41
+ env.cacheDir =
42
+ process.env.YOMI_MODEL_CACHE_DIR ||
43
+ (process.platform === 'darwin'
44
+ ? join(homedir(), 'Library', 'Caches', 'Yomi', 'models')
45
+ : join(process.env.LOCALAPPDATA || homedir(), 'Yomi', 'models'));
46
+ return pipeline('feature-extraction', MODEL_ID, {
47
+ device: 'cpu',
48
+ });
49
+ })
41
50
  .catch((error) => {
42
51
  // Reset the memo on failure so a later call can retry (e.g. the
43
52
  // first attempt failed offline, network comes back later) instead
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * virtual filesystem). MUST stay equal to the "version" field in
5
5
  * package.json — the test in version.test.ts enforces that.
6
6
  */
7
- export const YOMI_VERSION = '0.5.0';
7
+ export const YOMI_VERSION = '0.6.0';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Expand anchors over the event graph rather than taking a fixed number of
3
+ * neighbouring messages. Edges are traversed in both directions because a
4
+ * later delivery can revise the meaning of an earlier request.
5
+ */
6
+ export function assembleEventContext(allMessages, graph, anchorMessageIds, options = {}) {
7
+ const minimumWeight = options.minimumEdgeWeight ?? 0.3;
8
+ const maximumMessages = Math.max(1, options.maximumMessages ?? 64);
9
+ const byId = new Map(allMessages.map((message) => [message.messageId, message]));
10
+ const validAnchors = [...new Set(anchorMessageIds)].filter((id) => byId.has(id));
11
+ const chatId = validAnchors.map((id) => byId.get(id).chatId)[0] ?? '';
12
+ if (validAnchors.length === 0) {
13
+ return {
14
+ schemaVersion: 1,
15
+ chatId,
16
+ anchorMessageIds: [],
17
+ messages: [],
18
+ edges: [],
19
+ complete: false,
20
+ boundary: 'missing_anchor',
21
+ };
22
+ }
23
+ const adjacency = new Map();
24
+ for (const edge of graph.edges.filter((item) => item.weight >= minimumWeight)) {
25
+ const parent = byId.get(edge.parentId);
26
+ const child = byId.get(edge.childId);
27
+ if (!parent ||
28
+ !child ||
29
+ parent.chatId !== chatId ||
30
+ child.chatId !== chatId)
31
+ continue;
32
+ adjacency.set(edge.parentId, [
33
+ ...(adjacency.get(edge.parentId) ?? []),
34
+ { id: edge.childId, edge },
35
+ ]);
36
+ adjacency.set(edge.childId, [
37
+ ...(adjacency.get(edge.childId) ?? []),
38
+ { id: edge.parentId, edge },
39
+ ]);
40
+ }
41
+ const distance = new Map(validAnchors.map((id) => [id, 0]));
42
+ const queue = [...validAnchors];
43
+ const includedEdges = new Map();
44
+ let truncated = false;
45
+ while (queue.length > 0) {
46
+ const current = queue.shift();
47
+ for (const next of adjacency.get(current) ?? []) {
48
+ includedEdges.set(`${next.edge.parentId}:${next.edge.childId}`, next.edge);
49
+ if (distance.has(next.id))
50
+ continue;
51
+ if (distance.size >= maximumMessages) {
52
+ truncated = true;
53
+ continue;
54
+ }
55
+ distance.set(next.id, (distance.get(current) ?? 0) + 1);
56
+ queue.push(next.id);
57
+ }
58
+ }
59
+ const messages = [...distance.keys()]
60
+ .map((id) => byId.get(id))
61
+ .sort((left, right) => left.sentAt - right.sentAt);
62
+ const selected = new Set(messages.map((message) => message.messageId));
63
+ const edges = [...includedEdges.values()].filter((edge) => selected.has(edge.parentId) && selected.has(edge.childId));
64
+ return {
65
+ schemaVersion: 1,
66
+ chatId,
67
+ anchorMessageIds: validAnchors,
68
+ messages,
69
+ edges,
70
+ complete: !truncated,
71
+ boundary: truncated ? 'message_limit' : 'graph_boundary',
72
+ };
73
+ }
@@ -0,0 +1,101 @@
1
+ const ACTS = new Set([
2
+ 'inform',
3
+ 'ask',
4
+ 'answer',
5
+ 'directive',
6
+ 'commit',
7
+ 'delegate',
8
+ 'acknowledge',
9
+ 'deliver',
10
+ 'accept',
11
+ 'reject',
12
+ 'cancel',
13
+ ]);
14
+ const OWNERSHIP = new Set([
15
+ 'unassigned',
16
+ 'owned',
17
+ 'delegated',
18
+ 'unknown',
19
+ ]);
20
+ const OWNER_KINDS = new Set([
21
+ 'self',
22
+ 'conversation_member',
23
+ 'external_party',
24
+ 'unknown',
25
+ ]);
26
+ const DEPENDENCIES = new Set([
27
+ 'none',
28
+ 'waiting_person',
29
+ 'waiting_external_system',
30
+ 'waiting_decision',
31
+ 'unknown',
32
+ ]);
33
+ const LIFECYCLES = new Set([
34
+ 'proposed',
35
+ 'committed',
36
+ 'in_progress',
37
+ 'blocked',
38
+ 'delivered',
39
+ 'accepted',
40
+ 'cancelled',
41
+ 'resolved',
42
+ 'uncertain',
43
+ ]);
44
+ export function validateFactorizedGold(value) {
45
+ const errors = [];
46
+ if (!value.id)
47
+ errors.push('id is required');
48
+ if (!Array.isArray(value.context) || value.context.length === 0)
49
+ errors.push('context is required');
50
+ if (!Number.isInteger(value.targetIndex) ||
51
+ value.targetIndex < 0 ||
52
+ value.targetIndex >= value.context.length)
53
+ errors.push('targetIndex is outside context');
54
+ if (!value.dialogueActs.length ||
55
+ value.dialogueActs.some((act) => !ACTS.has(act)))
56
+ errors.push('dialogueActs are invalid');
57
+ if (value.createsWork && !value.action)
58
+ errors.push('work requires an action frame');
59
+ if (!OWNERSHIP.has(value.ownership.state))
60
+ errors.push('ownership is invalid');
61
+ if (!Array.isArray(value.ownership.ownerKinds) ||
62
+ value.ownership.ownerKinds.length === 0 ||
63
+ value.ownership.ownerKinds.some((kind) => !OWNER_KINDS.has(kind)))
64
+ errors.push('ownerKinds are invalid');
65
+ if (!DEPENDENCIES.has(value.dependency))
66
+ errors.push('dependency is invalid');
67
+ if (!LIFECYCLES.has(value.lifecycle))
68
+ errors.push('lifecycle is invalid');
69
+ if (value.completion.accepted && !value.completion.delivered)
70
+ errors.push('acceptance requires delivery');
71
+ if (value.lifecycle === 'delivered' && !value.completion.delivered)
72
+ errors.push('delivered lifecycle requires delivery evidence');
73
+ if (value.lifecycle === 'accepted' && !value.completion.accepted)
74
+ errors.push('accepted lifecycle requires acceptance evidence');
75
+ if (value.lifecycle === 'cancelled' && !value.completion.cancelled)
76
+ errors.push('cancelled lifecycle requires cancellation evidence');
77
+ if (value.evidenceIndexes) {
78
+ if (value.evidenceIndexes.length === 0)
79
+ errors.push('evidenceIndexes cannot be empty');
80
+ if (new Set(value.evidenceIndexes).size !== value.evidenceIndexes.length)
81
+ errors.push('evidenceIndexes must be unique');
82
+ if (value.evidenceIndexes.some((index) => !Number.isInteger(index) ||
83
+ index < 0 ||
84
+ index >= value.context.length)) {
85
+ errors.push('evidenceIndexes are outside context');
86
+ }
87
+ if (!value.evidenceIndexes.includes(value.targetIndex))
88
+ errors.push('evidenceIndexes must include targetIndex');
89
+ }
90
+ if (value.evidenceMessageIds) {
91
+ if (value.evidenceMessageIds.length === 0)
92
+ errors.push('evidenceMessageIds cannot be empty');
93
+ if (new Set(value.evidenceMessageIds).size !== value.evidenceMessageIds.length)
94
+ errors.push('evidenceMessageIds must be unique');
95
+ if (value.evidenceIndexes &&
96
+ value.evidenceMessageIds.length !== value.evidenceIndexes.length) {
97
+ errors.push('evidence index and message id counts differ');
98
+ }
99
+ }
100
+ return errors;
101
+ }
@@ -0,0 +1,96 @@
1
+ export const REQUIRED_GOLD_COVERAGE = {
2
+ work: ['true', 'false'],
3
+ dialogue: [
4
+ 'inform',
5
+ 'ask',
6
+ 'answer',
7
+ 'directive',
8
+ 'commit',
9
+ 'delegate',
10
+ 'acknowledge',
11
+ 'deliver',
12
+ 'accept',
13
+ 'reject',
14
+ 'cancel',
15
+ ],
16
+ ownership: ['unassigned', 'owned', 'delegated'],
17
+ dependency: [
18
+ 'none',
19
+ 'waiting_person',
20
+ 'waiting_external_system',
21
+ 'waiting_decision',
22
+ ],
23
+ lifecycle: [
24
+ 'proposed',
25
+ 'committed',
26
+ 'in_progress',
27
+ 'blocked',
28
+ 'delivered',
29
+ 'accepted',
30
+ 'cancelled',
31
+ 'resolved',
32
+ ],
33
+ completion: ['delivered', 'accepted', 'cancelled'],
34
+ };
35
+ export function factorizedCoverageLabels(record) {
36
+ return [
37
+ `work:${record.createsWork}`,
38
+ ...record.dialogueActs.map((label) => `dialogue:${label}`),
39
+ `ownership:${record.ownership.state}`,
40
+ `dependency:${record.dependency}`,
41
+ `lifecycle:${record.lifecycle}`,
42
+ ...(record.completion.delivered ? ['completion:delivered'] : []),
43
+ ...(record.completion.accepted ? ['completion:accepted'] : []),
44
+ ...(record.completion.cancelled ? ['completion:cancelled'] : []),
45
+ ];
46
+ }
47
+ function count(records, head, label) {
48
+ if (head === 'work')
49
+ return records.filter((record) => String(record.createsWork) === label)
50
+ .length;
51
+ if (head === 'dialogue')
52
+ return records.filter((record) => record.dialogueActs.includes(label)).length;
53
+ if (head === 'ownership')
54
+ return records.filter((record) => record.ownership.state === label).length;
55
+ if (head === 'dependency')
56
+ return records.filter((record) => record.dependency === label).length;
57
+ if (head === 'lifecycle')
58
+ return records.filter((record) => record.lifecycle === label).length;
59
+ return records.filter((record) => record.completion[label]).length;
60
+ }
61
+ /** Every factorized head must be observable; aggregate sample count is not a gate. */
62
+ export function auditGoldCoverage(split) {
63
+ const issues = [];
64
+ for (const partition of ['train', 'validation', 'test']) {
65
+ const records = split[partition];
66
+ const required = partition === 'train' ? 2 : 1;
67
+ for (const [head, labels] of Object.entries(REQUIRED_GOLD_COVERAGE)) {
68
+ for (const label of labels) {
69
+ const observed = count(records, head, label);
70
+ if (observed < required)
71
+ issues.push({ partition, head, label, observed, required });
72
+ }
73
+ }
74
+ }
75
+ return issues;
76
+ }
77
+ export function assertGoldCoverage(split) {
78
+ const issues = auditGoldCoverage(split);
79
+ if (issues.length > 0) {
80
+ const summary = issues
81
+ .map((issue) => `${issue.partition}.${issue.head}.${issue.label}=${issue.observed}/${issue.required}`)
82
+ .join(', ');
83
+ throw new Error(`Factorized gold coverage gate failed: ${summary}`);
84
+ }
85
+ }
86
+ export function auditAcceptanceCoverage(records) {
87
+ const observed = new Set(records.flatMap(factorizedCoverageLabels));
88
+ return Object.entries(REQUIRED_GOLD_COVERAGE)
89
+ .flatMap(([head, labels]) => labels.map((label) => `${head}:${label}`))
90
+ .filter((label) => !observed.has(label));
91
+ }
92
+ export function assertAcceptanceCoverage(records) {
93
+ const missing = auditAcceptanceCoverage(records);
94
+ if (missing.length > 0)
95
+ throw new Error(`Independent acceptance coverage gate failed: ${missing.join(', ')}`);
96
+ }
@@ -0,0 +1,67 @@
1
+ export function validateGoldDerivation(value) {
2
+ const errors = [];
3
+ if (value.schemaVersion !== 1)
4
+ errors.push('unsupported schemaVersion');
5
+ if (!value.derivationId)
6
+ errors.push('derivationId is required');
7
+ if (!Number.isInteger(value.revision) || value.revision < 1)
8
+ errors.push('revision must be positive');
9
+ if (value.revision === 1 && value.previousRevision !== null)
10
+ errors.push('first revision cannot have a predecessor');
11
+ if (value.revision > 1 && value.previousRevision !== value.revision - 1)
12
+ errors.push('revision chain is invalid');
13
+ if (!value.conversationId)
14
+ errors.push('conversationId is required');
15
+ if (!value.graphEventId)
16
+ errors.push('graphEventId is required');
17
+ if (value.reasons.length === 0)
18
+ errors.push('at least one derivation reason is required');
19
+ if (new Set(value.evidenceMessageIds).size !== value.evidenceMessageIds.length)
20
+ errors.push('evidenceMessageIds must be unique');
21
+ if (new Set(value.derivedGoldIds).size !== value.derivedGoldIds.length)
22
+ errors.push('derivedGoldIds must be unique');
23
+ if (value.disposition === 'included' && value.derivedGoldIds.length !== 1)
24
+ errors.push('included context requires exactly one gold record');
25
+ if (value.disposition === 'split' && value.derivedGoldIds.length < 2)
26
+ errors.push('split context requires at least two gold records');
27
+ if (!['included', 'split'].includes(value.disposition) &&
28
+ value.derivedGoldIds.length > 0)
29
+ errors.push('excluded context cannot produce gold records');
30
+ if (!value.contextComplete &&
31
+ !value.reasons.includes('context_boundary_truncated') &&
32
+ !value.reasons.includes('missing_antecedent')) {
33
+ errors.push('incomplete context requires a boundary reason');
34
+ }
35
+ return errors;
36
+ }
37
+ export function createGoldDerivation(input) {
38
+ const revision = {
39
+ ...input,
40
+ schemaVersion: 1,
41
+ revision: 1,
42
+ previousRevision: null,
43
+ };
44
+ const errors = validateGoldDerivation(revision);
45
+ if (errors.length > 0)
46
+ throw new Error(errors.join('; '));
47
+ return revision;
48
+ }
49
+ /** Revisions make exclusions and earlier interpretations explicitly reversible. */
50
+ export function reviseGoldDerivation(previous, input) {
51
+ if (previous.derivationId !== input.derivationId)
52
+ throw new Error('Cannot revise a different derivation');
53
+ if (previous.conversationId !== input.conversationId ||
54
+ previous.graphEventId !== input.graphEventId) {
55
+ throw new Error('Derivation source cannot change across revisions');
56
+ }
57
+ const revision = {
58
+ ...input,
59
+ schemaVersion: 1,
60
+ revision: previous.revision + 1,
61
+ previousRevision: previous.revision,
62
+ };
63
+ const errors = validateGoldDerivation(revision);
64
+ if (errors.length > 0)
65
+ throw new Error(errors.join('; '));
66
+ return revision;
67
+ }
@@ -0,0 +1,143 @@
1
+ function labelsFor(record) {
2
+ return [
3
+ `work:${record.createsWork}`,
4
+ ...record.dialogueActs.map((value) => `dialogue:${value}`),
5
+ `ownership:${record.ownership.state}`,
6
+ ...record.ownership.ownerKinds.map((value) => `owner_kind:${value}`),
7
+ `dependency:${record.dependency}`,
8
+ `lifecycle:${record.lifecycle}`,
9
+ `completion:delivered:${record.completion.delivered}`,
10
+ `completion:accepted:${record.completion.accepted}`,
11
+ `completion:cancelled:${record.completion.cancelled}`,
12
+ ];
13
+ }
14
+ function labelCounts(records) {
15
+ const counts = new Map();
16
+ for (const record of records) {
17
+ for (const label of labelsFor(record))
18
+ counts.set(label, (counts.get(label) ?? 0) + 1);
19
+ }
20
+ return counts;
21
+ }
22
+ function stableHash(value) {
23
+ let hash = 0x811c9dc5;
24
+ for (const character of value) {
25
+ hash ^= character.codePointAt(0) ?? 0;
26
+ hash = Math.imul(hash, 0x01000193);
27
+ }
28
+ return hash >>> 0;
29
+ }
30
+ function conversationId(record) {
31
+ return record.source?.chatId ?? `synthetic:${record.id}`;
32
+ }
33
+ /**
34
+ * Assign whole conversations, never windows, to partitions. Public acceptance
35
+ * fixtures are removed before grouping so they cannot leak into model fitting.
36
+ */
37
+ export function splitFactorizedGoldByConversation(records, options) {
38
+ const acceptance = new Set(options.acceptanceIds ?? []);
39
+ const excludedAcceptanceIds = [];
40
+ const groups = new Map();
41
+ for (const record of records) {
42
+ if (acceptance.has(record.id)) {
43
+ excludedAcceptanceIds.push(record.id);
44
+ continue;
45
+ }
46
+ const key = conversationId(record);
47
+ groups.set(key, [...(groups.get(key) ?? []), record]);
48
+ }
49
+ const validationFraction = options.validationFraction ?? 0.15;
50
+ const testFraction = options.testFraction ?? 0.15;
51
+ if (validationFraction < 0 ||
52
+ testFraction < 0 ||
53
+ validationFraction + testFraction >= 1) {
54
+ throw new Error('Invalid split fractions');
55
+ }
56
+ const validationGroups = groups.size >= 3
57
+ ? Math.max(1, Math.round(groups.size * validationFraction))
58
+ : 0;
59
+ const testGroups = groups.size >= 3 ? Math.max(1, Math.round(groups.size * testFraction)) : 0;
60
+ const trainGroups = Math.max(0, groups.size - validationGroups - testGroups);
61
+ const allRecords = [...groups.values()].flat();
62
+ const totals = labelCounts(allRecords);
63
+ const shares = {
64
+ train: 1 - validationFraction - testFraction,
65
+ validation: validationFraction,
66
+ test: testFraction,
67
+ };
68
+ const capacities = {
69
+ train: trainGroups,
70
+ validation: validationGroups,
71
+ test: testGroups,
72
+ };
73
+ const assigned = {
74
+ train: [],
75
+ validation: [],
76
+ test: [],
77
+ };
78
+ const assignedGroups = { train: 0, validation: 0, test: 0 };
79
+ const current = {
80
+ train: new Map(),
81
+ validation: new Map(),
82
+ test: new Map(),
83
+ };
84
+ const ordered = [...groups.entries()].sort(([leftId, left], [rightId, right]) => {
85
+ const rarity = (records) => [...new Set(records.flatMap(labelsFor))].reduce((sum, label) => sum + 1 / (totals.get(label) ?? 1), 0);
86
+ return (rarity(right) - rarity(left) ||
87
+ stableHash(`${options.seed}:${leftId}`) -
88
+ stableHash(`${options.seed}:${rightId}`));
89
+ });
90
+ for (const [groupId, records] of ordered) {
91
+ const counts = labelCounts(records);
92
+ const candidates = ['train', 'validation', 'test'].filter((name) => assignedGroups[name] < capacities[name]);
93
+ const scored = candidates
94
+ .map((name) => {
95
+ let labelNeed = 0;
96
+ for (const [label, count] of counts) {
97
+ const target = (totals.get(label) ?? 0) * shares[name];
98
+ labelNeed +=
99
+ (count * Math.max(0, target - (current[name].get(label) ?? 0))) /
100
+ Math.max(1, target);
101
+ }
102
+ const targetRecords = allRecords.length * shares[name];
103
+ const sizeNeed = Math.max(0, targetRecords - assigned[name].length) /
104
+ Math.max(1, targetRecords);
105
+ return {
106
+ name,
107
+ score: labelNeed * 4 + sizeNeed,
108
+ tie: stableHash(`${options.seed}:${groupId}:${name}`),
109
+ };
110
+ })
111
+ .sort((left, right) => right.score - left.score || left.tie - right.tie);
112
+ const destination = scored[0]?.name;
113
+ if (!destination)
114
+ throw new Error('No partition has remaining conversation capacity');
115
+ assigned[destination].push(...records);
116
+ assignedGroups[destination] += 1;
117
+ for (const [label, count] of counts)
118
+ current[destination].set(label, (current[destination].get(label) ?? 0) + count);
119
+ }
120
+ return {
121
+ seed: options.seed,
122
+ train: assigned.train,
123
+ validation: assigned.validation,
124
+ test: assigned.test,
125
+ excludedAcceptanceIds: excludedAcceptanceIds.sort(),
126
+ };
127
+ }
128
+ export function assertNoConversationLeakage(split) {
129
+ const seen = new Map();
130
+ for (const [partition, records] of Object.entries({
131
+ train: split.train,
132
+ validation: split.validation,
133
+ test: split.test,
134
+ })) {
135
+ for (const record of records) {
136
+ const key = conversationId(record);
137
+ const previous = seen.get(key);
138
+ if (previous && previous !== partition)
139
+ throw new Error(`Conversation ${key} leaked from ${previous} into ${partition}`);
140
+ seen.set(key, partition);
141
+ }
142
+ }
143
+ }
@@ -0,0 +1,23 @@
1
+ export * from './adapter.js';
2
+ export * from './destination-contracts.js';
3
+ export * from './event-context.js';
4
+ export * from './gold-contract.js';
5
+ export * from './gold-coverage.js';
6
+ export * from './gold-derivation.js';
7
+ export * from './gold-split.js';
8
+ export * from './line-adapter.js';
9
+ export * from './llama-cpp-work-event-adapter.js';
10
+ export * from './local-provider.js';
11
+ export * from './local-work-event-provider.js';
12
+ export * from './onnx-work-event-adapter.js';
13
+ export * from './provider.js';
14
+ export * from './registry.js';
15
+ export * from './service.js';
16
+ export * from './store.js';
17
+ export * from './types.js';
18
+ export * from './work-event.js';
19
+ export * from './work-event-graph-engine.js';
20
+ export * from './work-event-inference.js';
21
+ export * from './work-event-matcher.js';
22
+ export * from './work-event-model-manifest.js';
23
+ export * from './work-event-store.js';