@kb-labs/steward-core 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 ADDED
@@ -0,0 +1,5 @@
1
+ # @kb-labs/steward-core
2
+
3
+ > Part of [KB Labs](https://github.com/kb-labs-team/kb-labs) ecosystem.
4
+
5
+ Business logic for the steward plugin: projects, people, routing, and commitments. Pure, testable in isolation.
@@ -0,0 +1,135 @@
1
+ import { IDocumentDatabase } from '@kb-labs/sdk';
2
+ import { Project, Resource, ProjectMember, AddProjectInput, AddResourceInput, ListProjectsInput, UpdateProjectInput, Person, AddCompanyInput, Company, AddMemberInput, AddPersonInput, UpdatePersonInput, WhoToContactInput, AddCommitmentInput, Commitment, CommitmentDoneInput, CommitmentDropInput, CommitmentSnoozeInput, ListCommitmentsInput, AddEventInput, Event, EventSubjectType, ListEventsInput, AddTopicInput, Topic } from '@kb-labs/steward-contracts';
3
+
4
+ /**
5
+ * Resolves the plugin's document database and ensures collections/indexes
6
+ * exist. Idempotent — safe to call from every command handler.
7
+ */
8
+ declare function getDb(): Promise<IDocumentDatabase>;
9
+
10
+ declare function addProject(input: AddProjectInput): Promise<Project>;
11
+ declare function updateProject(input: UpdateProjectInput): Promise<Project | null>;
12
+ declare function listProjects(input: ListProjectsInput): Promise<Project[]>;
13
+ interface ProjectCard {
14
+ project: Project;
15
+ resources: Resource[];
16
+ members: ProjectMember[];
17
+ }
18
+ /** Resolves by id first, falling back to an exact name match. */
19
+ declare function getProject(idOrName: string): Promise<ProjectCard | null>;
20
+ declare function addResource(input: AddResourceInput): Promise<Resource>;
21
+ declare function listResources(projectId: string): Promise<Resource[]>;
22
+
23
+ /**
24
+ * Cheap dedup signal on the write path (ADR-0001 §8): a normalized-name
25
+ * match, not a hard constraint. The caller decides whether to still insert.
26
+ */
27
+ declare function findPossibleDuplicates(name: string): Promise<Person[]>;
28
+ interface AddPersonResult {
29
+ person: Person;
30
+ possibleDuplicates: Person[];
31
+ }
32
+ declare function addPerson(input: AddPersonInput): Promise<AddPersonResult>;
33
+ declare function updatePerson(input: UpdatePersonInput): Promise<Person | null>;
34
+ /** Resolves by id first, falling back to an exact name match. */
35
+ declare function getPerson(idOrName: string): Promise<Person | null>;
36
+ declare function listPeople(): Promise<Person[]>;
37
+ declare function addCompany(input: AddCompanyInput): Promise<Company>;
38
+ declare function listCompanies(): Promise<Company[]>;
39
+ declare function addMember(input: AddMemberInput): Promise<ProjectMember>;
40
+ declare function listMembers(projectId: string): Promise<ProjectMember[]>;
41
+
42
+ interface ContactCandidate {
43
+ person: Person;
44
+ source: 'project' | 'global';
45
+ /** Fallback order — lower is contacted first. Absent for global matches (unordered). */
46
+ priority?: number;
47
+ }
48
+ /**
49
+ * whoToContact(topic, projectId?) — hybrid lookup (ADR-0001 §Routing).
50
+ *
51
+ * With a `projectId`, `ProjectMember.topics` in that project take priority,
52
+ * ordered by `ProjectMember.priority` (the fallback chain). Falls back to
53
+ * `Person.globalTopics` across every contact.
54
+ *
55
+ * Topic array fields aren't filterable at the `DocumentFilter` level
56
+ * (array-operator semantics diverge across drivers), so matching happens
57
+ * in memory — fine at personal scale.
58
+ */
59
+ declare function whoToContact(input: WhoToContactInput): Promise<ContactCandidate[]>;
60
+
61
+ declare function isStale(c: Commitment, now?: number): boolean;
62
+ declare function addCommitment(input: AddCommitmentInput): Promise<Commitment>;
63
+ declare function listCommitments(input: ListCommitmentsInput): Promise<Commitment[]>;
64
+ declare function commitmentDone(input: CommitmentDoneInput): Promise<Commitment | null>;
65
+ declare function commitmentDrop(input: CommitmentDropInput): Promise<Commitment | null>;
66
+ declare function commitmentSnooze(input: CommitmentSnoozeInput): Promise<Commitment | null>;
67
+
68
+ /**
69
+ * Append-only write used both by the manual `event add` command and by every
70
+ * other function that records a lifecycle transition (commitment done/drop,
71
+ * project status change, ...). Never updates or deletes.
72
+ */
73
+ declare function appendEvent(input: {
74
+ subjectType: EventSubjectType;
75
+ subjectId: string;
76
+ kind: string;
77
+ text?: string;
78
+ reason?: string;
79
+ meta?: Record<string, unknown>;
80
+ }): Promise<Event>;
81
+ declare function addEvent(input: AddEventInput): Promise<Event>;
82
+ declare function listEvents(input: ListEventsInput): Promise<Event[]>;
83
+
84
+ declare function addTopic(input: AddTopicInput): Promise<Topic>;
85
+ declare function listTopics(): Promise<Topic[]>;
86
+ /**
87
+ * Resolves a free-text topic to its canonical dictionary name via name or
88
+ * alias match. Falls back to the raw input when nothing is registered —
89
+ * the dictionary is opt-in, not a hard gate (ADR-0001 §Routing).
90
+ *
91
+ * Array-field filtering (`aliases`) isn't part of `DocumentFilter`'s
92
+ * contract (semantics diverge across drivers), so this matches in memory —
93
+ * acceptable at the scale of a personal topic dictionary.
94
+ */
95
+ declare function resolveTopic(raw: string): Promise<string>;
96
+
97
+ interface DailyReview {
98
+ generatedAt: number;
99
+ lastBackupDaysAgo: number | null;
100
+ staleCommitments: Commitment[];
101
+ upcomingCommitments: Commitment[];
102
+ activeProjects: Project[];
103
+ }
104
+ /**
105
+ * Summary for the daily cron artifact and the `steward review` command.
106
+ * First field is `lastBackupDaysAgo` on purpose (ADR-0001 §Артефакты) — a
107
+ * silent backup failure must be the loudest line, not a swallowed log.
108
+ */
109
+ declare function getDailyReview(): Promise<DailyReview>;
110
+ interface IntegrityReport {
111
+ checkedAt: number;
112
+ counts: Record<string, number>;
113
+ previousCounts: Record<string, number> | null;
114
+ suspiciousDrops: string[];
115
+ }
116
+ /**
117
+ * Flags collections whose document count dropped >20% day-over-day without
118
+ * an explicit bulk delete — surfaced in review, not swallowed in a log
119
+ * (ADR-0001 §Бэкапы).
120
+ */
121
+ declare function checkIntegrity(): Promise<IntegrityReport>;
122
+
123
+ interface Snapshot {
124
+ exportedAt: number;
125
+ collections: Record<string, unknown[]>;
126
+ }
127
+ /**
128
+ * Dumps every collection to plain objects for the daily backup artifact.
129
+ * Writing the result to disk and pushing it to the private repo is a job
130
+ * concern (`entry/src/jobs/export-backup.ts`), not core's — core only
131
+ * produces the data (ADR-0001 §Бэкапы, §"Разделение core/entry").
132
+ */
133
+ declare function exportSnapshot(): Promise<Snapshot>;
134
+
135
+ export { type AddPersonResult, type ContactCandidate, type DailyReview, type IntegrityReport, type ProjectCard, type Snapshot, addCommitment, addCompany, addEvent, addMember, addPerson, addProject, addResource, addTopic, appendEvent, checkIntegrity, commitmentDone, commitmentDrop, commitmentSnooze, exportSnapshot, findPossibleDuplicates, getDailyReview, getDb, getPerson, getProject, isStale, listCommitments, listCompanies, listEvents, listMembers, listPeople, listProjects, listResources, listTopics, resolveTopic, updatePerson, updateProject, whoToContact };
package/dist/index.js ADDED
@@ -0,0 +1,448 @@
1
+ import { useDocumentDatabase } from '@kb-labs/sdk';
2
+ import { COLLECTIONS } from '@kb-labs/steward-contracts';
3
+
4
+ // src/db.ts
5
+ async function getDb() {
6
+ const docs = useDocumentDatabase();
7
+ if (!docs) {
8
+ throw new Error(
9
+ "steward requires a documentDatabase adapter \u2014 none is configured on this platform"
10
+ );
11
+ }
12
+ await ensureCollections(docs);
13
+ return docs;
14
+ }
15
+ var ensured = false;
16
+ async function ensureCollections(docs) {
17
+ if (ensured) {
18
+ return;
19
+ }
20
+ await Promise.all([
21
+ docs.ensureCollection(COLLECTIONS.projects, {
22
+ indexes: [{ path: "name" }, { path: "status" }]
23
+ }),
24
+ docs.ensureCollection(COLLECTIONS.resources, {
25
+ indexes: [{ path: "projectId" }]
26
+ }),
27
+ docs.ensureCollection(COLLECTIONS.people, {
28
+ indexes: [{ path: "name" }, { path: "companyId" }]
29
+ }),
30
+ docs.ensureCollection(COLLECTIONS.companies, {
31
+ indexes: [{ path: "name" }]
32
+ }),
33
+ docs.ensureCollection(COLLECTIONS.projectMembers, {
34
+ indexes: [{ path: ["personId", "projectId"] }]
35
+ }),
36
+ docs.ensureCollection(COLLECTIONS.commitments, {
37
+ indexes: [{ path: ["status", "remindAt"] }, { path: "projectId" }, { path: "personId" }]
38
+ }),
39
+ docs.ensureCollection(COLLECTIONS.events, {
40
+ indexes: [{ path: ["subjectType", "subjectId", "at"] }, { path: "kind" }]
41
+ }),
42
+ docs.ensureCollection(COLLECTIONS.topics, {
43
+ indexes: [{ path: "name" }]
44
+ })
45
+ ]);
46
+ ensured = true;
47
+ }
48
+ async function appendEvent(input) {
49
+ const docs = await getDb();
50
+ return docs.insertOne(COLLECTIONS.events, {
51
+ at: Date.now(),
52
+ kind: input.kind,
53
+ subjectType: input.subjectType,
54
+ subjectId: input.subjectId,
55
+ text: input.text,
56
+ reason: input.reason,
57
+ meta: input.meta
58
+ });
59
+ }
60
+ async function addEvent(input) {
61
+ return appendEvent({
62
+ subjectType: input.subjectType,
63
+ subjectId: input.subjectId,
64
+ kind: input.kind,
65
+ text: input.text,
66
+ reason: input.reason,
67
+ meta: input.meta
68
+ });
69
+ }
70
+ async function listEvents(input) {
71
+ const docs = await getDb();
72
+ return docs.find(
73
+ COLLECTIONS.events,
74
+ {
75
+ ...input.subjectType ? { subjectType: input.subjectType } : {},
76
+ ...input.subjectId ? { subjectId: input.subjectId } : {},
77
+ ...input.kind ? { kind: input.kind } : {},
78
+ ...input.since ? { at: { $gte: input.since } } : {}
79
+ },
80
+ { sort: { at: -1 } }
81
+ );
82
+ }
83
+
84
+ // src/functions/project.ts
85
+ async function addProject(input) {
86
+ const docs = await getDb();
87
+ const project = await docs.insertOne(COLLECTIONS.projects, {
88
+ name: input.name,
89
+ status: input.status,
90
+ description: input.description
91
+ });
92
+ await appendEvent({
93
+ subjectType: "project",
94
+ subjectId: project.id,
95
+ kind: "project.created",
96
+ text: project.name
97
+ });
98
+ return project;
99
+ }
100
+ async function updateProject(input) {
101
+ const docs = await getDb();
102
+ const before = await docs.findById(COLLECTIONS.projects, input.id);
103
+ if (!before) {
104
+ return null;
105
+ }
106
+ const updated = await docs.updateById(COLLECTIONS.projects, input.id, {
107
+ $set: {
108
+ ...input.status !== void 0 ? { status: input.status } : {},
109
+ ...input.description !== void 0 ? { description: input.description } : {}
110
+ }
111
+ });
112
+ if (updated && input.status && input.status !== before.status) {
113
+ await appendEvent({
114
+ subjectType: "project",
115
+ subjectId: input.id,
116
+ kind: "project.status_changed",
117
+ meta: { from: before.status, to: input.status }
118
+ });
119
+ }
120
+ return updated;
121
+ }
122
+ async function listProjects(input) {
123
+ const docs = await getDb();
124
+ return docs.find(
125
+ COLLECTIONS.projects,
126
+ input.status ? { status: input.status } : {},
127
+ { sort: { name: 1 } }
128
+ );
129
+ }
130
+ async function getProject(idOrName) {
131
+ const docs = await getDb();
132
+ let project = await docs.findById(COLLECTIONS.projects, idOrName);
133
+ if (!project) {
134
+ const [byName] = await docs.find(COLLECTIONS.projects, { name: { $eq: idOrName } });
135
+ project = byName ?? null;
136
+ }
137
+ if (!project) {
138
+ return null;
139
+ }
140
+ const [resources, members] = await Promise.all([
141
+ docs.find(COLLECTIONS.resources, { projectId: { $eq: project.id } }),
142
+ docs.find(COLLECTIONS.projectMembers, { projectId: { $eq: project.id } }, {
143
+ sort: { priority: 1 }
144
+ })
145
+ ]);
146
+ return { project, resources, members };
147
+ }
148
+ async function addResource(input) {
149
+ const docs = await getDb();
150
+ return docs.insertOne(COLLECTIONS.resources, {
151
+ projectId: input.projectId,
152
+ type: input.type,
153
+ label: input.label,
154
+ url: input.url,
155
+ content: input.content
156
+ });
157
+ }
158
+ async function listResources(projectId) {
159
+ const docs = await getDb();
160
+ return docs.find(COLLECTIONS.resources, { projectId: { $eq: projectId } });
161
+ }
162
+ function normalize(s) {
163
+ return s.trim().toLowerCase();
164
+ }
165
+ async function addTopic(input) {
166
+ const docs = await getDb();
167
+ return docs.insertOne(COLLECTIONS.topics, {
168
+ name: input.name,
169
+ aliases: input.aliases
170
+ });
171
+ }
172
+ async function listTopics() {
173
+ const docs = await getDb();
174
+ return docs.find(COLLECTIONS.topics, {}, { sort: { name: 1 } });
175
+ }
176
+ async function resolveTopic(raw) {
177
+ const needle = normalize(raw);
178
+ const all = await listTopics();
179
+ const hit = all.find(
180
+ (t) => normalize(t.name) === needle || t.aliases.some((a) => normalize(a) === needle)
181
+ );
182
+ return hit?.name ?? raw;
183
+ }
184
+
185
+ // src/functions/person.ts
186
+ function normalizeName(name) {
187
+ return name.trim().toLowerCase().replace(/\s+/g, " ");
188
+ }
189
+ async function resolveTopics(topics) {
190
+ return Promise.all(topics.map((t) => resolveTopic(t)));
191
+ }
192
+ async function findPossibleDuplicates(name) {
193
+ const docs = await getDb();
194
+ const normalized = normalizeName(name);
195
+ const all = await docs.find(COLLECTIONS.people, {});
196
+ return all.filter((p) => normalizeName(p.name) === normalized);
197
+ }
198
+ async function addPerson(input) {
199
+ const docs = await getDb();
200
+ const possibleDuplicates = await findPossibleDuplicates(input.name);
201
+ const person = await docs.insertOne(COLLECTIONS.people, {
202
+ name: input.name,
203
+ contacts: input.contacts,
204
+ companyId: input.companyId,
205
+ globalTopics: await resolveTopics(input.globalTopics)
206
+ });
207
+ return { person, possibleDuplicates };
208
+ }
209
+ async function updatePerson(input) {
210
+ const docs = await getDb();
211
+ return docs.updateById(COLLECTIONS.people, input.id, {
212
+ $set: {
213
+ ...input.name !== void 0 ? { name: input.name } : {},
214
+ ...input.contacts !== void 0 ? { contacts: input.contacts } : {},
215
+ ...input.companyId !== void 0 ? { companyId: input.companyId } : {},
216
+ ...input.globalTopics !== void 0 ? { globalTopics: await resolveTopics(input.globalTopics) } : {}
217
+ }
218
+ });
219
+ }
220
+ async function getPerson(idOrName) {
221
+ const docs = await getDb();
222
+ const byId = await docs.findById(COLLECTIONS.people, idOrName);
223
+ if (byId) {
224
+ return byId;
225
+ }
226
+ const [byName] = await docs.find(COLLECTIONS.people, { name: { $eq: idOrName } });
227
+ return byName ?? null;
228
+ }
229
+ async function listPeople() {
230
+ const docs = await getDb();
231
+ return docs.find(COLLECTIONS.people, {}, { sort: { name: 1 } });
232
+ }
233
+ async function addCompany(input) {
234
+ const docs = await getDb();
235
+ return docs.insertOne(COLLECTIONS.companies, { name: input.name });
236
+ }
237
+ async function listCompanies() {
238
+ const docs = await getDb();
239
+ return docs.find(COLLECTIONS.companies, {}, { sort: { name: 1 } });
240
+ }
241
+ async function addMember(input) {
242
+ const docs = await getDb();
243
+ return docs.insertOne(COLLECTIONS.projectMembers, {
244
+ personId: input.personId,
245
+ projectId: input.projectId,
246
+ role: input.role,
247
+ topics: input.topics ? await resolveTopics(input.topics) : void 0,
248
+ priority: input.priority
249
+ });
250
+ }
251
+ async function listMembers(projectId) {
252
+ const docs = await getDb();
253
+ return docs.find(
254
+ COLLECTIONS.projectMembers,
255
+ { projectId: { $eq: projectId } },
256
+ { sort: { priority: 1 } }
257
+ );
258
+ }
259
+ async function whoToContact(input) {
260
+ const docs = await getDb();
261
+ const topic = await resolveTopic(input.topic);
262
+ const candidates = [];
263
+ if (input.projectId) {
264
+ const members = await docs.find(COLLECTIONS.projectMembers, {
265
+ projectId: { $eq: input.projectId }
266
+ });
267
+ const matching = members.filter((m) => (m.topics ?? []).some((t) => t.toLowerCase() === topic.toLowerCase())).sort((a, b) => a.priority - b.priority);
268
+ for (const member of matching) {
269
+ const person = await docs.findById(COLLECTIONS.people, member.personId);
270
+ if (person) {
271
+ candidates.push({ person, source: "project", priority: member.priority });
272
+ }
273
+ }
274
+ }
275
+ const allPeople = await docs.find(COLLECTIONS.people, {});
276
+ const globalMatches = allPeople.filter(
277
+ (p) => !candidates.some((c) => c.person.id === p.id) && p.globalTopics.some((t) => t.toLowerCase() === topic.toLowerCase())
278
+ );
279
+ for (const person of globalMatches) {
280
+ candidates.push({ person, source: "global" });
281
+ }
282
+ return candidates;
283
+ }
284
+ function isStale(c, now = Date.now()) {
285
+ if (c.status !== "open") {
286
+ return false;
287
+ }
288
+ if (c.snoozedUntil && c.snoozedUntil > now) {
289
+ return false;
290
+ }
291
+ const dueAt = c.remindAt ?? c.createdAt + c.staleAfterDays * 24 * 60 * 60 * 1e3;
292
+ return dueAt < now;
293
+ }
294
+ async function addCommitment(input) {
295
+ const docs = await getDb();
296
+ const commitment = await docs.insertOne(COLLECTIONS.commitments, {
297
+ text: input.text,
298
+ personId: input.personId,
299
+ projectId: input.projectId,
300
+ status: "open",
301
+ remindAt: input.remindAt,
302
+ staleAfterDays: input.staleAfterDays
303
+ });
304
+ await appendEvent({
305
+ subjectType: "commitment",
306
+ subjectId: commitment.id,
307
+ kind: "commitment.created",
308
+ text: commitment.text
309
+ });
310
+ return commitment;
311
+ }
312
+ async function listCommitments(input) {
313
+ const docs = await getDb();
314
+ const all = await docs.find(
315
+ COLLECTIONS.commitments,
316
+ {
317
+ ...input.status ? { status: input.status } : {},
318
+ ...input.projectId ? { projectId: { $eq: input.projectId } } : {}
319
+ },
320
+ { sort: { createdAt: -1 } }
321
+ );
322
+ return input.staleOnly ? all.filter((c) => isStale(c)) : all;
323
+ }
324
+ async function commitmentDone(input) {
325
+ const docs = await getDb();
326
+ const updated = await docs.updateById(COLLECTIONS.commitments, input.id, {
327
+ $set: { status: "done" }
328
+ });
329
+ if (updated) {
330
+ await appendEvent({ subjectType: "commitment", subjectId: input.id, kind: "commitment.done" });
331
+ }
332
+ return updated;
333
+ }
334
+ async function commitmentDrop(input) {
335
+ const docs = await getDb();
336
+ const updated = await docs.updateById(COLLECTIONS.commitments, input.id, {
337
+ $set: { status: "dropped" }
338
+ });
339
+ if (updated) {
340
+ await appendEvent({
341
+ subjectType: "commitment",
342
+ subjectId: input.id,
343
+ kind: "commitment.dropped",
344
+ reason: input.reason
345
+ });
346
+ }
347
+ return updated;
348
+ }
349
+ async function commitmentSnooze(input) {
350
+ const docs = await getDb();
351
+ const before = await docs.findById(COLLECTIONS.commitments, input.id);
352
+ if (!before) {
353
+ return null;
354
+ }
355
+ const updated = await docs.updateById(COLLECTIONS.commitments, input.id, {
356
+ $set: { snoozedUntil: input.until }
357
+ });
358
+ if (updated) {
359
+ await appendEvent({
360
+ subjectType: "commitment",
361
+ subjectId: input.id,
362
+ kind: "commitment.rescheduled",
363
+ reason: input.reason,
364
+ meta: { from: before.snoozedUntil, to: input.until }
365
+ });
366
+ }
367
+ return updated;
368
+ }
369
+ var DAY_MS = 24 * 60 * 60 * 1e3;
370
+ async function getDailyReview() {
371
+ const docs = await getDb();
372
+ const now = Date.now();
373
+ const [allOpen, activeProjects, exportEvents] = await Promise.all([
374
+ listCommitments({ status: "open", staleOnly: false }),
375
+ docs.find(COLLECTIONS.projects, { status: "active" }),
376
+ listEvents({ kind: "export.completed" })
377
+ ]);
378
+ const staleCommitments = allOpen.filter((c) => isStale(c, now));
379
+ const upcomingCommitments = allOpen.filter((c) => !isStale(c, now) && c.remindAt && c.remindAt - now < 7 * DAY_MS).sort((a, b) => (a.remindAt ?? 0) - (b.remindAt ?? 0));
380
+ const lastBackup = exportEvents[0];
381
+ const lastBackupDaysAgo = lastBackup ? Math.floor((now - lastBackup.at) / DAY_MS) : null;
382
+ const review = {
383
+ generatedAt: now,
384
+ lastBackupDaysAgo,
385
+ staleCommitments,
386
+ upcomingCommitments,
387
+ activeProjects
388
+ };
389
+ await appendEvent({
390
+ subjectType: "project",
391
+ subjectId: "global",
392
+ kind: "review.generated",
393
+ meta: { staleCount: staleCommitments.length, upcomingCount: upcomingCommitments.length }
394
+ });
395
+ return review;
396
+ }
397
+ async function checkIntegrity() {
398
+ const docs = await getDb();
399
+ const collectionNames = Object.values(COLLECTIONS);
400
+ const counts = {};
401
+ for (const name of collectionNames) {
402
+ counts[name] = await docs.count(name, {});
403
+ }
404
+ const [previous] = await listEvents({ kind: "integrity.checked" });
405
+ const previousCounts = previous?.meta?.counts ?? null;
406
+ const suspiciousDrops = [];
407
+ if (previousCounts) {
408
+ for (const name of collectionNames) {
409
+ const prev = previousCounts[name] ?? 0;
410
+ const curr = counts[name] ?? 0;
411
+ if (prev > 0 && curr < prev * 0.8) {
412
+ suspiciousDrops.push(name);
413
+ }
414
+ }
415
+ }
416
+ const report = {
417
+ checkedAt: Date.now(),
418
+ counts,
419
+ previousCounts,
420
+ suspiciousDrops
421
+ };
422
+ await appendEvent({
423
+ subjectType: "project",
424
+ subjectId: "global",
425
+ kind: "integrity.checked",
426
+ meta: { counts }
427
+ });
428
+ return report;
429
+ }
430
+ async function exportSnapshot() {
431
+ const docs = await getDb();
432
+ const collections = {};
433
+ for (const name of Object.values(COLLECTIONS)) {
434
+ collections[name] = await docs.find(name, {});
435
+ }
436
+ const snapshot = { exportedAt: Date.now(), collections };
437
+ await appendEvent({
438
+ subjectType: "project",
439
+ subjectId: "global",
440
+ kind: "export.completed",
441
+ meta: { counts: Object.fromEntries(Object.entries(collections).map(([k, v]) => [k, v.length])) }
442
+ });
443
+ return snapshot;
444
+ }
445
+
446
+ export { addCommitment, addCompany, addEvent, addMember, addPerson, addProject, addResource, addTopic, appendEvent, checkIntegrity, commitmentDone, commitmentDrop, commitmentSnooze, exportSnapshot, findPossibleDuplicates, getDailyReview, getDb, getPerson, getProject, isStale, listCommitments, listCompanies, listEvents, listMembers, listPeople, listProjects, listResources, listTopics, resolveTopic, updatePerson, updateProject, whoToContact };
447
+ //# sourceMappingURL=index.js.map
448
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/db.ts","../src/functions/event.ts","../src/functions/project.ts","../src/functions/topic.ts","../src/functions/person.ts","../src/functions/routing.ts","../src/functions/commitment.ts","../src/functions/review.ts","../src/functions/export.ts"],"names":["COLLECTIONS"],"mappings":"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","file":"index.js","sourcesContent":["import { useDocumentDatabase } from '@kb-labs/sdk';\nimport type { IDocumentDatabase } from '@kb-labs/sdk';\nimport { COLLECTIONS } from '@kb-labs/steward-contracts';\n\n/**\n * Resolves the plugin's document database and ensures collections/indexes\n * exist. Idempotent — safe to call from every command handler.\n */\nexport async function getDb(): Promise<IDocumentDatabase> {\n const docs = useDocumentDatabase();\n if (!docs) {\n throw new Error(\n 'steward requires a documentDatabase adapter — none is configured on this platform',\n );\n }\n await ensureCollections(docs);\n return docs;\n}\n\nlet ensured = false;\n\nasync function ensureCollections(docs: IDocumentDatabase): Promise<void> {\n if (ensured) {return;}\n await Promise.all([\n docs.ensureCollection(COLLECTIONS.projects, {\n indexes: [{ path: 'name' }, { path: 'status' }],\n }),\n docs.ensureCollection(COLLECTIONS.resources, {\n indexes: [{ path: 'projectId' }],\n }),\n docs.ensureCollection(COLLECTIONS.people, {\n indexes: [{ path: 'name' }, { path: 'companyId' }],\n }),\n docs.ensureCollection(COLLECTIONS.companies, {\n indexes: [{ path: 'name' }],\n }),\n docs.ensureCollection(COLLECTIONS.projectMembers, {\n indexes: [{ path: ['personId', 'projectId'] }],\n }),\n docs.ensureCollection(COLLECTIONS.commitments, {\n indexes: [{ path: ['status', 'remindAt'] }, { path: 'projectId' }, { path: 'personId' }],\n }),\n docs.ensureCollection(COLLECTIONS.events, {\n indexes: [{ path: ['subjectType', 'subjectId', 'at'] }, { path: 'kind' }],\n }),\n docs.ensureCollection(COLLECTIONS.topics, {\n indexes: [{ path: 'name' }],\n }),\n ]);\n ensured = true;\n}\n","import { COLLECTIONS, type Event, type EventSubjectType } from '@kb-labs/steward-contracts';\nimport type { AddEventInput, ListEventsInput } from '@kb-labs/steward-contracts';\nimport { getDb } from '../db.js';\n\n/**\n * Append-only write used both by the manual `event add` command and by every\n * other function that records a lifecycle transition (commitment done/drop,\n * project status change, ...). Never updates or deletes.\n */\nexport async function appendEvent(input: {\n subjectType: EventSubjectType;\n subjectId: string;\n kind: string;\n text?: string;\n reason?: string;\n meta?: Record<string, unknown>;\n}): Promise<Event> {\n const docs = await getDb();\n return docs.insertOne<Event>(COLLECTIONS.events, {\n at: Date.now(),\n kind: input.kind as Event['kind'],\n subjectType: input.subjectType,\n subjectId: input.subjectId,\n text: input.text,\n reason: input.reason,\n meta: input.meta,\n });\n}\n\nexport async function addEvent(input: AddEventInput): Promise<Event> {\n return appendEvent({\n subjectType: input.subjectType,\n subjectId: input.subjectId,\n kind: input.kind,\n text: input.text,\n reason: input.reason,\n meta: input.meta,\n });\n}\n\nexport async function listEvents(input: ListEventsInput): Promise<Event[]> {\n const docs = await getDb();\n return docs.find<Event>(\n COLLECTIONS.events,\n {\n ...(input.subjectType ? { subjectType: input.subjectType } : {}),\n ...(input.subjectId ? { subjectId: input.subjectId } : {}),\n ...(input.kind ? { kind: input.kind as Event['kind'] } : {}),\n ...(input.since ? { at: { $gte: input.since } } : {}),\n },\n { sort: { at: -1 } },\n );\n}\n","import {\n COLLECTIONS,\n type Project,\n type Resource,\n type ProjectMember,\n type AddProjectInput,\n type UpdateProjectInput,\n type ListProjectsInput,\n type AddResourceInput,\n} from '@kb-labs/steward-contracts';\nimport { getDb } from '../db.js';\nimport { appendEvent } from './event.js';\n\nexport async function addProject(input: AddProjectInput): Promise<Project> {\n const docs = await getDb();\n const project = await docs.insertOne<Project>(COLLECTIONS.projects, {\n name: input.name,\n status: input.status,\n description: input.description,\n });\n await appendEvent({\n subjectType: 'project',\n subjectId: project.id,\n kind: 'project.created',\n text: project.name,\n });\n return project;\n}\n\nexport async function updateProject(input: UpdateProjectInput): Promise<Project | null> {\n const docs = await getDb();\n const before = await docs.findById<Project>(COLLECTIONS.projects, input.id);\n if (!before) {return null;}\n\n const updated = await docs.updateById<Project>(COLLECTIONS.projects, input.id, {\n $set: {\n ...(input.status !== undefined ? { status: input.status } : {}),\n ...(input.description !== undefined ? { description: input.description } : {}),\n },\n });\n\n if (updated && input.status && input.status !== before.status) {\n await appendEvent({\n subjectType: 'project',\n subjectId: input.id,\n kind: 'project.status_changed',\n meta: { from: before.status, to: input.status },\n });\n }\n return updated;\n}\n\nexport async function listProjects(input: ListProjectsInput): Promise<Project[]> {\n const docs = await getDb();\n return docs.find<Project>(\n COLLECTIONS.projects,\n input.status ? { status: input.status } : {},\n { sort: { name: 1 } },\n );\n}\n\nexport interface ProjectCard {\n project: Project;\n resources: Resource[];\n members: ProjectMember[];\n}\n\n/** Resolves by id first, falling back to an exact name match. */\nexport async function getProject(idOrName: string): Promise<ProjectCard | null> {\n const docs = await getDb();\n let project = await docs.findById<Project>(COLLECTIONS.projects, idOrName);\n if (!project) {\n const [byName] = await docs.find<Project>(COLLECTIONS.projects, { name: { $eq: idOrName } });\n project = byName ?? null;\n }\n if (!project) {return null;}\n\n const [resources, members] = await Promise.all([\n docs.find<Resource>(COLLECTIONS.resources, { projectId: { $eq: project.id } }),\n docs.find<ProjectMember>(COLLECTIONS.projectMembers, { projectId: { $eq: project.id } }, {\n sort: { priority: 1 },\n }),\n ]);\n\n return { project, resources, members };\n}\n\n// ── Resources ────────────────────────────────────────────────────────────\n\nexport async function addResource(input: AddResourceInput): Promise<Resource> {\n const docs = await getDb();\n return docs.insertOne<Resource>(COLLECTIONS.resources, {\n projectId: input.projectId,\n type: input.type,\n label: input.label,\n url: input.url,\n content: input.content,\n });\n}\n\nexport async function listResources(projectId: string): Promise<Resource[]> {\n const docs = await getDb();\n return docs.find<Resource>(COLLECTIONS.resources, { projectId: { $eq: projectId } });\n}\n","import { COLLECTIONS, type Topic, type AddTopicInput } from '@kb-labs/steward-contracts';\nimport { getDb } from '../db.js';\n\nfunction normalize(s: string): string {\n return s.trim().toLowerCase();\n}\n\nexport async function addTopic(input: AddTopicInput): Promise<Topic> {\n const docs = await getDb();\n return docs.insertOne<Topic>(COLLECTIONS.topics, {\n name: input.name,\n aliases: input.aliases,\n });\n}\n\nexport async function listTopics(): Promise<Topic[]> {\n const docs = await getDb();\n return docs.find<Topic>(COLLECTIONS.topics, {}, { sort: { name: 1 } });\n}\n\n/**\n * Resolves a free-text topic to its canonical dictionary name via name or\n * alias match. Falls back to the raw input when nothing is registered —\n * the dictionary is opt-in, not a hard gate (ADR-0001 §Routing).\n *\n * Array-field filtering (`aliases`) isn't part of `DocumentFilter`'s\n * contract (semantics diverge across drivers), so this matches in memory —\n * acceptable at the scale of a personal topic dictionary.\n */\nexport async function resolveTopic(raw: string): Promise<string> {\n const needle = normalize(raw);\n const all = await listTopics();\n const hit = all.find(\n (t) => normalize(t.name) === needle || t.aliases.some((a) => normalize(a) === needle),\n );\n return hit?.name ?? raw;\n}\n","import {\n COLLECTIONS,\n type Person,\n type Company,\n type ProjectMember,\n type AddPersonInput,\n type UpdatePersonInput,\n type AddCompanyInput,\n type AddMemberInput,\n} from '@kb-labs/steward-contracts';\nimport { getDb } from '../db.js';\nimport { resolveTopic } from './topic.js';\n\nfunction normalizeName(name: string): string {\n return name.trim().toLowerCase().replace(/\\s+/g, ' ');\n}\n\n/** Resolves every topic to its dictionary canonical form — normalization happens on write (ADR-0001 §Routing). */\nasync function resolveTopics(topics: string[]): Promise<string[]> {\n return Promise.all(topics.map((t) => resolveTopic(t)));\n}\n\n/**\n * Cheap dedup signal on the write path (ADR-0001 §8): a normalized-name\n * match, not a hard constraint. The caller decides whether to still insert.\n */\nexport async function findPossibleDuplicates(name: string): Promise<Person[]> {\n const docs = await getDb();\n const normalized = normalizeName(name);\n const all = await docs.find<Person>(COLLECTIONS.people, {});\n return all.filter((p) => normalizeName(p.name) === normalized);\n}\n\nexport interface AddPersonResult {\n person: Person;\n possibleDuplicates: Person[];\n}\n\nexport async function addPerson(input: AddPersonInput): Promise<AddPersonResult> {\n const docs = await getDb();\n const possibleDuplicates = await findPossibleDuplicates(input.name);\n const person = await docs.insertOne<Person>(COLLECTIONS.people, {\n name: input.name,\n contacts: input.contacts,\n companyId: input.companyId,\n globalTopics: await resolveTopics(input.globalTopics),\n });\n return { person, possibleDuplicates };\n}\n\nexport async function updatePerson(input: UpdatePersonInput): Promise<Person | null> {\n const docs = await getDb();\n return docs.updateById<Person>(COLLECTIONS.people, input.id, {\n $set: {\n ...(input.name !== undefined ? { name: input.name } : {}),\n ...(input.contacts !== undefined ? { contacts: input.contacts } : {}),\n ...(input.companyId !== undefined ? { companyId: input.companyId } : {}),\n ...(input.globalTopics !== undefined ? { globalTopics: await resolveTopics(input.globalTopics) } : {}),\n },\n });\n}\n\n/** Resolves by id first, falling back to an exact name match. */\nexport async function getPerson(idOrName: string): Promise<Person | null> {\n const docs = await getDb();\n const byId = await docs.findById<Person>(COLLECTIONS.people, idOrName);\n if (byId) {return byId;}\n const [byName] = await docs.find<Person>(COLLECTIONS.people, { name: { $eq: idOrName } });\n return byName ?? null;\n}\n\nexport async function listPeople(): Promise<Person[]> {\n const docs = await getDb();\n return docs.find<Person>(COLLECTIONS.people, {}, { sort: { name: 1 } });\n}\n\n// ── Company (thin — see ADR-0001 §\"Миграционная политика\") ────────────────\n\nexport async function addCompany(input: AddCompanyInput): Promise<Company> {\n const docs = await getDb();\n return docs.insertOne<Company>(COLLECTIONS.companies, { name: input.name });\n}\n\nexport async function listCompanies(): Promise<Company[]> {\n const docs = await getDb();\n return docs.find<Company>(COLLECTIONS.companies, {}, { sort: { name: 1 } });\n}\n\n// ── ProjectMember ────────────────────────────────────────────────────────\n\nexport async function addMember(input: AddMemberInput): Promise<ProjectMember> {\n const docs = await getDb();\n return docs.insertOne<ProjectMember>(COLLECTIONS.projectMembers, {\n personId: input.personId,\n projectId: input.projectId,\n role: input.role,\n topics: input.topics ? await resolveTopics(input.topics) : undefined,\n priority: input.priority,\n });\n}\n\nexport async function listMembers(projectId: string): Promise<ProjectMember[]> {\n const docs = await getDb();\n return docs.find<ProjectMember>(\n COLLECTIONS.projectMembers,\n { projectId: { $eq: projectId } },\n { sort: { priority: 1 } },\n );\n}\n","import {\n COLLECTIONS,\n type Person,\n type ProjectMember,\n type WhoToContactInput,\n} from '@kb-labs/steward-contracts';\nimport { getDb } from '../db.js';\nimport { resolveTopic } from './topic.js';\n\nexport interface ContactCandidate {\n person: Person;\n source: 'project' | 'global';\n /** Fallback order — lower is contacted first. Absent for global matches (unordered). */\n priority?: number;\n}\n\n/**\n * whoToContact(topic, projectId?) — hybrid lookup (ADR-0001 §Routing).\n *\n * With a `projectId`, `ProjectMember.topics` in that project take priority,\n * ordered by `ProjectMember.priority` (the fallback chain). Falls back to\n * `Person.globalTopics` across every contact.\n *\n * Topic array fields aren't filterable at the `DocumentFilter` level\n * (array-operator semantics diverge across drivers), so matching happens\n * in memory — fine at personal scale.\n */\nexport async function whoToContact(input: WhoToContactInput): Promise<ContactCandidate[]> {\n const docs = await getDb();\n const topic = await resolveTopic(input.topic);\n\n const candidates: ContactCandidate[] = [];\n\n if (input.projectId) {\n const members = await docs.find<ProjectMember>(COLLECTIONS.projectMembers, {\n projectId: { $eq: input.projectId },\n });\n const matching = members\n .filter((m) => (m.topics ?? []).some((t) => t.toLowerCase() === topic.toLowerCase()))\n .sort((a, b) => a.priority - b.priority);\n\n for (const member of matching) {\n const person = await docs.findById<Person>(COLLECTIONS.people, member.personId);\n if (person) {candidates.push({ person, source: 'project', priority: member.priority });}\n }\n }\n\n const allPeople = await docs.find<Person>(COLLECTIONS.people, {});\n const globalMatches = allPeople.filter(\n (p) =>\n !candidates.some((c) => c.person.id === p.id) &&\n p.globalTopics.some((t) => t.toLowerCase() === topic.toLowerCase()),\n );\n for (const person of globalMatches) {\n candidates.push({ person, source: 'global' });\n }\n\n return candidates;\n}\n","import {\n COLLECTIONS,\n type Commitment,\n type AddCommitmentInput,\n type ListCommitmentsInput,\n type CommitmentDoneInput,\n type CommitmentDropInput,\n type CommitmentSnoozeInput,\n} from '@kb-labs/steward-contracts';\nimport { getDb } from '../db.js';\nimport { appendEvent } from './event.js';\n\nexport function isStale(c: Commitment, now = Date.now()): boolean {\n if (c.status !== 'open') {return false;}\n if (c.snoozedUntil && c.snoozedUntil > now) {return false;}\n const dueAt = c.remindAt ?? c.createdAt + c.staleAfterDays * 24 * 60 * 60 * 1000;\n return dueAt < now;\n}\n\nexport async function addCommitment(input: AddCommitmentInput): Promise<Commitment> {\n const docs = await getDb();\n const commitment = await docs.insertOne<Commitment>(COLLECTIONS.commitments, {\n text: input.text,\n personId: input.personId,\n projectId: input.projectId,\n status: 'open',\n remindAt: input.remindAt,\n staleAfterDays: input.staleAfterDays,\n });\n await appendEvent({\n subjectType: 'commitment',\n subjectId: commitment.id,\n kind: 'commitment.created',\n text: commitment.text,\n });\n return commitment;\n}\n\nexport async function listCommitments(input: ListCommitmentsInput): Promise<Commitment[]> {\n const docs = await getDb();\n const all = await docs.find<Commitment>(\n COLLECTIONS.commitments,\n {\n ...(input.status ? { status: input.status } : {}),\n ...(input.projectId ? { projectId: { $eq: input.projectId } } : {}),\n },\n { sort: { createdAt: -1 } },\n );\n return input.staleOnly ? all.filter((c) => isStale(c)) : all;\n}\n\nexport async function commitmentDone(input: CommitmentDoneInput): Promise<Commitment | null> {\n const docs = await getDb();\n const updated = await docs.updateById<Commitment>(COLLECTIONS.commitments, input.id, {\n $set: { status: 'done' },\n });\n if (updated) {\n await appendEvent({ subjectType: 'commitment', subjectId: input.id, kind: 'commitment.done' });\n }\n return updated;\n}\n\nexport async function commitmentDrop(input: CommitmentDropInput): Promise<Commitment | null> {\n const docs = await getDb();\n const updated = await docs.updateById<Commitment>(COLLECTIONS.commitments, input.id, {\n $set: { status: 'dropped' },\n });\n if (updated) {\n await appendEvent({\n subjectType: 'commitment',\n subjectId: input.id,\n kind: 'commitment.dropped',\n reason: input.reason,\n });\n }\n return updated;\n}\n\nexport async function commitmentSnooze(input: CommitmentSnoozeInput): Promise<Commitment | null> {\n const docs = await getDb();\n const before = await docs.findById<Commitment>(COLLECTIONS.commitments, input.id);\n if (!before) {return null;}\n\n const updated = await docs.updateById<Commitment>(COLLECTIONS.commitments, input.id, {\n $set: { snoozedUntil: input.until },\n });\n if (updated) {\n await appendEvent({\n subjectType: 'commitment',\n subjectId: input.id,\n kind: 'commitment.rescheduled',\n reason: input.reason,\n meta: { from: before.snoozedUntil, to: input.until },\n });\n }\n return updated;\n}\n","import { COLLECTIONS, type Commitment, type Project } from '@kb-labs/steward-contracts';\nimport { getDb } from '../db.js';\nimport { isStale, listCommitments } from './commitment.js';\nimport { appendEvent, listEvents } from './event.js';\n\nexport interface DailyReview {\n generatedAt: number;\n lastBackupDaysAgo: number | null;\n staleCommitments: Commitment[];\n upcomingCommitments: Commitment[];\n activeProjects: Project[];\n}\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/**\n * Summary for the daily cron artifact and the `steward review` command.\n * First field is `lastBackupDaysAgo` on purpose (ADR-0001 §Артефакты) — a\n * silent backup failure must be the loudest line, not a swallowed log.\n */\nexport async function getDailyReview(): Promise<DailyReview> {\n const docs = await getDb();\n const now = Date.now();\n\n const [allOpen, activeProjects, exportEvents] = await Promise.all([\n listCommitments({ status: 'open', staleOnly: false }),\n docs.find<Project>(COLLECTIONS.projects, { status: 'active' }),\n listEvents({ kind: 'export.completed' }),\n ]);\n\n const staleCommitments = allOpen.filter((c) => isStale(c, now));\n const upcomingCommitments = allOpen\n .filter((c) => !isStale(c, now) && c.remindAt && c.remindAt - now < 7 * DAY_MS)\n .sort((a, b) => (a.remindAt ?? 0) - (b.remindAt ?? 0));\n\n const lastBackup = exportEvents[0];\n const lastBackupDaysAgo = lastBackup ? Math.floor((now - lastBackup.at) / DAY_MS) : null;\n\n const review: DailyReview = {\n generatedAt: now,\n lastBackupDaysAgo,\n staleCommitments,\n upcomingCommitments,\n activeProjects,\n };\n\n await appendEvent({\n subjectType: 'project',\n subjectId: 'global',\n kind: 'review.generated',\n meta: { staleCount: staleCommitments.length, upcomingCount: upcomingCommitments.length },\n });\n\n return review;\n}\n\nexport interface IntegrityReport {\n checkedAt: number;\n counts: Record<string, number>;\n previousCounts: Record<string, number> | null;\n suspiciousDrops: string[];\n}\n\n/**\n * Flags collections whose document count dropped >20% day-over-day without\n * an explicit bulk delete — surfaced in review, not swallowed in a log\n * (ADR-0001 §Бэкапы).\n */\nexport async function checkIntegrity(): Promise<IntegrityReport> {\n const docs = await getDb();\n const collectionNames = Object.values(COLLECTIONS);\n\n const counts: Record<string, number> = {};\n for (const name of collectionNames) {\n counts[name] = await docs.count(name, {});\n }\n\n const [previous] = await listEvents({ kind: 'integrity.checked' });\n const previousCounts = (previous?.meta?.counts as Record<string, number> | undefined) ?? null;\n\n const suspiciousDrops: string[] = [];\n if (previousCounts) {\n for (const name of collectionNames) {\n const prev = previousCounts[name] ?? 0;\n const curr = counts[name] ?? 0;\n if (prev > 0 && curr < prev * 0.8) {suspiciousDrops.push(name);}\n }\n }\n\n const report: IntegrityReport = {\n checkedAt: Date.now(),\n counts,\n previousCounts,\n suspiciousDrops,\n };\n\n await appendEvent({\n subjectType: 'project',\n subjectId: 'global',\n kind: 'integrity.checked',\n meta: { counts },\n });\n\n return report;\n}\n","import { COLLECTIONS } from '@kb-labs/steward-contracts';\nimport { getDb } from '../db.js';\nimport { appendEvent } from './event.js';\n\nexport interface Snapshot {\n exportedAt: number;\n collections: Record<string, unknown[]>;\n}\n\n/**\n * Dumps every collection to plain objects for the daily backup artifact.\n * Writing the result to disk and pushing it to the private repo is a job\n * concern (`entry/src/jobs/export-backup.ts`), not core's — core only\n * produces the data (ADR-0001 §Бэкапы, §\"Разделение core/entry\").\n */\nexport async function exportSnapshot(): Promise<Snapshot> {\n const docs = await getDb();\n const collections: Record<string, unknown[]> = {};\n for (const name of Object.values(COLLECTIONS)) {\n collections[name] = await docs.find(name, {});\n }\n\n const snapshot: Snapshot = { exportedAt: Date.now(), collections };\n\n await appendEvent({\n subjectType: 'project',\n subjectId: 'global',\n kind: 'export.completed',\n meta: { counts: Object.fromEntries(Object.entries(collections).map(([k, v]) => [k, v.length])) },\n });\n\n return snapshot;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@kb-labs/steward-core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Business logic for steward (pure, testable in isolation).",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "sideEffects": false,
18
+ "dependencies": {
19
+ "@kb-labs/sdk": "2.115.4",
20
+ "@kb-labs/steward-contracts": "0.1.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^24.0.0",
24
+ "rimraf": "^6.0.1",
25
+ "tsup": "^8.5.0",
26
+ "typescript": "^5.6.3",
27
+ "vitest": "^3.2.4",
28
+ "@kb-labs/core-runtime": "2.118.2",
29
+ "@kb-labs/devkit": "2.118.2"
30
+ },
31
+ "engines": {
32
+ "node": ">=20.0.0",
33
+ "pnpm": ">=9.0.0"
34
+ },
35
+ "scripts": {
36
+ "clean": "rimraf dist",
37
+ "build": "tsup",
38
+ "dev": "tsup --watch",
39
+ "lint": "eslint src --ext .ts",
40
+ "lint:fix": "eslint . --fix",
41
+ "type-check": "tsc --noEmit",
42
+ "test": "vitest run --passWithNoTests"
43
+ }
44
+ }