@mastra/factory 0.8.0-alpha.9 → 0.8.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 (156) hide show
  1. package/CHANGELOG.md +630 -0
  2. package/dist/factory.d.ts.map +1 -1
  3. package/dist/factory.js +41 -3
  4. package/dist/factory.js.map +1 -1
  5. package/dist/integrations/base.d.ts +8 -0
  6. package/dist/integrations/base.d.ts.map +1 -1
  7. package/dist/integrations/github/app-identity.d.ts +43 -0
  8. package/dist/integrations/github/app-identity.d.ts.map +1 -0
  9. package/dist/integrations/github/app-identity.js +62 -0
  10. package/dist/integrations/github/app-identity.js.map +1 -0
  11. package/dist/integrations/github/integration.d.ts.map +1 -1
  12. package/dist/integrations/github/integration.js +3 -1
  13. package/dist/integrations/github/integration.js.map +1 -1
  14. package/dist/integrations/github/reconcile-worker.d.ts +5 -0
  15. package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
  16. package/dist/integrations/github/reconcile-worker.js +7 -0
  17. package/dist/integrations/github/reconcile-worker.js.map +1 -1
  18. package/dist/integrations/github/routes.d.ts.map +1 -1
  19. package/dist/integrations/github/routes.js +11 -6
  20. package/dist/integrations/github/routes.js.map +1 -1
  21. package/dist/integrations/github/rules.d.ts +8 -0
  22. package/dist/integrations/github/rules.d.ts.map +1 -1
  23. package/dist/integrations/github/rules.js +30 -8
  24. package/dist/integrations/github/rules.js.map +1 -1
  25. package/dist/integrations/github/sandbox.d.ts +39 -6
  26. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  27. package/dist/integrations/github/sandbox.js +48 -14
  28. package/dist/integrations/github/sandbox.js.map +1 -1
  29. package/dist/integrations/github/webhook.d.ts +20 -0
  30. package/dist/integrations/github/webhook.d.ts.map +1 -1
  31. package/dist/integrations/github/webhook.js +32 -5
  32. package/dist/integrations/github/webhook.js.map +1 -1
  33. package/dist/integrations/linear/rules.d.ts.map +1 -1
  34. package/dist/integrations/linear/rules.js +2 -1
  35. package/dist/integrations/linear/rules.js.map +1 -1
  36. package/dist/integrations/platform/github/event-worker.d.ts +11 -0
  37. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
  38. package/dist/integrations/platform/github/event-worker.js +53 -14
  39. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  40. package/dist/integrations/platform/github/integration.d.ts +7 -0
  41. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  42. package/dist/integrations/platform/github/integration.js +40 -4
  43. package/dist/integrations/platform/github/integration.js.map +1 -1
  44. package/dist/integrations/slack/slack.d.ts.map +1 -1
  45. package/dist/integrations/slack/slack.js +2 -1
  46. package/dist/integrations/slack/slack.js.map +1 -1
  47. package/dist/routes/config.d.ts +12 -6
  48. package/dist/routes/config.d.ts.map +1 -1
  49. package/dist/routes/config.js +97 -56
  50. package/dist/routes/config.js.map +1 -1
  51. package/dist/routes/knowledge.d.ts +152 -0
  52. package/dist/routes/knowledge.d.ts.map +1 -0
  53. package/dist/routes/knowledge.js +442 -0
  54. package/dist/routes/knowledge.js.map +1 -0
  55. package/dist/routes/projects.d.ts +9 -1
  56. package/dist/routes/projects.d.ts.map +1 -1
  57. package/dist/routes/projects.js +8 -0
  58. package/dist/routes/projects.js.map +1 -1
  59. package/dist/routes/surface.d.ts +6 -0
  60. package/dist/routes/surface.d.ts.map +1 -1
  61. package/dist/routes/surface.js +9 -0
  62. package/dist/routes/surface.js.map +1 -1
  63. package/dist/routes/work-items.d.ts.map +1 -1
  64. package/dist/routes/work-items.js +3 -1
  65. package/dist/routes/work-items.js.map +1 -1
  66. package/dist/rules/binding-context.d.ts.map +1 -1
  67. package/dist/rules/binding-context.js +4 -1
  68. package/dist/rules/binding-context.js.map +1 -1
  69. package/dist/rules/defaults.d.ts.map +1 -1
  70. package/dist/rules/defaults.js +87 -4
  71. package/dist/rules/defaults.js.map +1 -1
  72. package/dist/rules/dispatcher.d.ts +6 -0
  73. package/dist/rules/dispatcher.d.ts.map +1 -1
  74. package/dist/rules/dispatcher.js +118 -16
  75. package/dist/rules/dispatcher.js.map +1 -1
  76. package/dist/rules/processor.d.ts.map +1 -1
  77. package/dist/rules/processor.js +20 -7
  78. package/dist/rules/processor.js.map +1 -1
  79. package/dist/rules/resolve.d.ts +1 -0
  80. package/dist/rules/resolve.d.ts.map +1 -1
  81. package/dist/rules/resolve.js +3 -2
  82. package/dist/rules/resolve.js.map +1 -1
  83. package/dist/rules/start-coordinator.d.ts +2 -0
  84. package/dist/rules/start-coordinator.d.ts.map +1 -1
  85. package/dist/rules/start-coordinator.js +3 -1
  86. package/dist/rules/start-coordinator.js.map +1 -1
  87. package/dist/rules/terminal-cleanup.d.ts +23 -0
  88. package/dist/rules/terminal-cleanup.d.ts.map +1 -0
  89. package/dist/rules/terminal-cleanup.js +43 -0
  90. package/dist/rules/terminal-cleanup.js.map +1 -0
  91. package/dist/rules/tools.d.ts.map +1 -1
  92. package/dist/rules/tools.js +24 -2
  93. package/dist/rules/tools.js.map +1 -1
  94. package/dist/rules/transition-service.d.ts +3 -0
  95. package/dist/rules/transition-service.d.ts.map +1 -1
  96. package/dist/rules/transition-service.js +10 -6
  97. package/dist/rules/transition-service.js.map +1 -1
  98. package/dist/rules/types.d.ts +32 -3
  99. package/dist/rules/types.d.ts.map +1 -1
  100. package/dist/rules/types.js +2 -0
  101. package/dist/rules/types.js.map +1 -1
  102. package/dist/rules/validation.d.ts.map +1 -1
  103. package/dist/rules/validation.js +8 -3
  104. package/dist/rules/validation.js.map +1 -1
  105. package/dist/sandbox/base-checkpoint-triggers.d.ts +77 -0
  106. package/dist/sandbox/base-checkpoint-triggers.d.ts.map +1 -0
  107. package/dist/sandbox/base-checkpoint-triggers.js +137 -0
  108. package/dist/sandbox/base-checkpoint-triggers.js.map +1 -0
  109. package/dist/sandbox/base-checkpoint.d.ts +59 -0
  110. package/dist/sandbox/base-checkpoint.d.ts.map +1 -0
  111. package/dist/sandbox/base-checkpoint.js +145 -0
  112. package/dist/sandbox/base-checkpoint.js.map +1 -0
  113. package/dist/sandbox/fleet.d.ts +32 -0
  114. package/dist/sandbox/fleet.d.ts.map +1 -1
  115. package/dist/sandbox/fleet.js +39 -10
  116. package/dist/sandbox/fleet.js.map +1 -1
  117. package/dist/session/checkpoint-capture.d.ts +3 -2
  118. package/dist/session/checkpoint-capture.d.ts.map +1 -1
  119. package/dist/session/checkpoint-capture.js +3 -3
  120. package/dist/session/checkpoint-capture.js.map +1 -1
  121. package/dist/session/factory-session.d.ts.map +1 -1
  122. package/dist/session/factory-session.js +2 -1
  123. package/dist/session/factory-session.js.map +1 -1
  124. package/dist/session/model-pack-hydration.d.ts +55 -0
  125. package/dist/session/model-pack-hydration.d.ts.map +1 -0
  126. package/dist/session/model-pack-hydration.js +56 -0
  127. package/dist/session/model-pack-hydration.js.map +1 -0
  128. package/dist/skills/service.d.ts +9 -0
  129. package/dist/skills/service.d.ts.map +1 -1
  130. package/dist/skills/service.js +10 -1
  131. package/dist/skills/service.js.map +1 -1
  132. package/dist/storage/domains/model-packs/base.d.ts +26 -0
  133. package/dist/storage/domains/model-packs/base.d.ts.map +1 -1
  134. package/dist/storage/domains/model-packs/base.js +102 -13
  135. package/dist/storage/domains/model-packs/base.js.map +1 -1
  136. package/dist/storage/domains/source-control/base.d.ts +47 -1
  137. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  138. package/dist/storage/domains/source-control/base.js +78 -19
  139. package/dist/storage/domains/source-control/base.js.map +1 -1
  140. package/dist/storage/domains/source-control/inmemory.d.ts +11 -3
  141. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  142. package/dist/storage/domains/source-control/inmemory.js +10 -1
  143. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  144. package/dist/storage/domains/work-items/base.d.ts +67 -1
  145. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  146. package/dist/storage/domains/work-items/base.js +140 -12
  147. package/dist/storage/domains/work-items/base.js.map +1 -1
  148. package/dist/workspace.d.ts +31 -2
  149. package/dist/workspace.d.ts.map +1 -1
  150. package/dist/workspace.js +225 -54
  151. package/dist/workspace.js.map +1 -1
  152. package/factory-skills/factory-plan/SKILL.md +1 -1
  153. package/factory-skills/factory-rereview/SKILL.md +3 -3
  154. package/factory-skills/factory-review/SKILL.md +3 -3
  155. package/factory-skills/factory-triage/SKILL.md +2 -0
  156. package/package.json +9 -9
@@ -0,0 +1,442 @@
1
+ import { Route } from "./route.js";
2
+ import { registerApiRoute } from "@mastra/core/server";
3
+ import { canonicalizeKnowledgeScope, isKnowledgeScopeVisible, knowledgeScopeKey, parseKnowledgeWikilinks } from "@mastra/core/storage";
4
+ //#region src/routes/knowledge.ts
5
+ /** Reserved node that anchors pinned records (see subconscious/pinned.ts). */
6
+ const PINNED_NODE_NAME = "pinned";
7
+ /** Hover-card budget for record text shipped in the graph payload. */
8
+ const RECORD_TEXT_LIMIT = 240;
9
+ function truncateRecordText(text) {
10
+ return text.length > RECORD_TEXT_LIMIT ? `${text.slice(0, RECORD_TEXT_LIMIT - 1)}…` : text;
11
+ }
12
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
13
+ const DEFAULT_LIMITS = {
14
+ maxNodes: 500,
15
+ maxRecords: 2e3,
16
+ maxFallbackLookups: 100
17
+ };
18
+ function loose(c) {
19
+ return c;
20
+ }
21
+ function deepestRung(scope) {
22
+ let rung = "org";
23
+ for (const entry of scope) {
24
+ const ns = entry.slice(0, entry.indexOf(":"));
25
+ if (ns === "thread") return "thread";
26
+ if (ns === "resource") rung = "resource";
27
+ }
28
+ return rung;
29
+ }
30
+ function boundedThreadId(raw) {
31
+ if (!raw) return void 0;
32
+ const trimmed = raw.trim();
33
+ return trimmed.length > 0 && trimmed.length <= 512 ? trimmed : void 0;
34
+ }
35
+ /**
36
+ * In-record + capped-fallback wikilink resolver, shared by both endpoints.
37
+ * Resolution uses the store's own algorithm: a descending-prefix walk over the
38
+ * record's canonical scope matching canonical name + exact scope key at each
39
+ * prefix, so an edge never depends on whether the target landed in the window.
40
+ */
41
+ var WikilinkResolver = class {
42
+ /** exact `${scopeKey}\u0000${lowerName}` → node, from the fetched window. */
43
+ #inWindow = /* @__PURE__ */ new Map();
44
+ #windowIds = /* @__PURE__ */ new Set();
45
+ /** `${recordScopeKey}\u0000${lowerName}` → fallback result (null = dangling). */
46
+ #fallbackCache = /* @__PURE__ */ new Map();
47
+ #fallbackLookups = 0;
48
+ #store;
49
+ #maxFallbackLookups;
50
+ outOfWindow = /* @__PURE__ */ new Map();
51
+ cappedNames = [];
52
+ #cappedSeen = /* @__PURE__ */ new Set();
53
+ constructor(store, nodes, maxFallbackLookups) {
54
+ this.#store = store;
55
+ this.#maxFallbackLookups = maxFallbackLookups;
56
+ for (const node of nodes) {
57
+ this.#inWindow.set(`${knowledgeScopeKey(node.scope)}\u0000${node.name.trim().toLocaleLowerCase()}`, node);
58
+ this.#windowIds.add(node.id);
59
+ }
60
+ }
61
+ inWindowId(id) {
62
+ return this.#windowIds.has(id);
63
+ }
64
+ /** Resolve a wikilink name from a knowledge record's scope. Returns the node or null (dangling/capped). */
65
+ async resolve(name, recordScope) {
66
+ const canonical = canonicalizeKnowledgeScope(recordScope);
67
+ const lower = name.trim().toLocaleLowerCase();
68
+ for (let length = canonical.length; length > 0; length--) {
69
+ const hit = this.#inWindow.get(`${knowledgeScopeKey(canonical.slice(0, length))}\u0000${lower}`);
70
+ if (hit) return hit;
71
+ }
72
+ const cacheKey = `${knowledgeScopeKey(canonical)}\u0000${lower}`;
73
+ if (this.#fallbackCache.has(cacheKey)) return this.#trackOutOfWindow(this.#fallbackCache.get(cacheKey) ?? null);
74
+ if (this.#fallbackLookups >= this.#maxFallbackLookups) {
75
+ if (!this.#cappedSeen.has(lower) && this.cappedNames.length < 100) {
76
+ this.#cappedSeen.add(lower);
77
+ this.cappedNames.push(name.trim());
78
+ } else if (!this.#cappedSeen.has(lower)) this.#cappedSeen.add(lower);
79
+ return null;
80
+ }
81
+ this.#fallbackLookups += 1;
82
+ let resolved = null;
83
+ try {
84
+ resolved = await this.#store.resolveNode({
85
+ name,
86
+ scope: canonical
87
+ });
88
+ } catch {
89
+ resolved = null;
90
+ }
91
+ this.#fallbackCache.set(cacheKey, resolved);
92
+ return this.#trackOutOfWindow(resolved);
93
+ }
94
+ #trackOutOfWindow(node) {
95
+ if (node && !this.#windowIds.has(node.id)) this.outOfWindow.set(node.id, {
96
+ id: node.id,
97
+ name: node.name
98
+ });
99
+ return node;
100
+ }
101
+ get cappedCount() {
102
+ return this.#cappedSeen.size;
103
+ }
104
+ };
105
+ var KnowledgeRoutes = class extends Route {
106
+ #limits;
107
+ constructor(deps) {
108
+ super(deps);
109
+ this.#limits = {
110
+ ...DEFAULT_LIMITS,
111
+ ...deps.limits
112
+ };
113
+ }
114
+ /** Resolve the `(orgId, userId)` tenant or a ready-to-return error response. */
115
+ async #resolveTenant(c) {
116
+ await this.deps.auth.ensureUser(c);
117
+ const tenant = this.deps.auth.tenant(c);
118
+ if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
119
+ if (!tenant.orgId) return { response: c.json({
120
+ error: "organization_required",
121
+ message: "The knowledge graph requires an organization."
122
+ }, 403) };
123
+ return {
124
+ orgId: tenant.orgId,
125
+ userId: tenant.userId
126
+ };
127
+ }
128
+ /**
129
+ * Resolve tenant + org-owned project + knowledge store + the active view's
130
+ * query scope. The ONE seam both endpoints share, so the default/thread view
131
+ * scope and the pin rungs cannot drift between them.
132
+ *
133
+ * threadId validation runs a single `listRecordsBySource` lookup with
134
+ * `limit: 1` AT THE CANDIDATE SCOPE `[org, resource, thread:<id>]` — the
135
+ * store's own visibility predicate is the authorization: the thread's own
136
+ * records (equal scope key) and its project/org captures (prefix) match, while
137
+ * a cross-org thread's records match nothing → zero rows → 404.
138
+ */
139
+ async #resolveView(c) {
140
+ const tenant = await this.#resolveTenant(c);
141
+ if ("response" in tenant) return tenant;
142
+ const projectId = c.req.param("id");
143
+ if (!projectId || !UUID_RE.test(projectId)) return { response: c.json({ error: "Project not found" }, 404) };
144
+ const { projects } = this.deps;
145
+ await projects.ensureReady();
146
+ if (!await projects.get({
147
+ orgId: tenant.orgId,
148
+ id: projectId
149
+ })) return { response: c.json({ error: "Project not found" }, 404) };
150
+ const store = await this.deps.knowledge();
151
+ if (!store) return { response: c.json({
152
+ error: "knowledge_unavailable",
153
+ message: "The knowledge storage domain is not configured."
154
+ }, 503) };
155
+ const defaultScope = [`org:${tenant.orgId}`, `resource:${projectId}`];
156
+ const resourceRungScope = defaultScope;
157
+ const threadId = boundedThreadId(c.req.query("threadId"));
158
+ if (c.req.query("threadId") !== void 0 && !threadId) return { response: c.json({ error: "thread_not_found" }, 404) };
159
+ if (!threadId) return {
160
+ ...tenant,
161
+ factoryProjectId: projectId,
162
+ store,
163
+ view: "project",
164
+ scope: defaultScope,
165
+ pinRungs: [{
166
+ rung: "resource",
167
+ scope: resourceRungScope
168
+ }]
169
+ };
170
+ const candidateScope = [...defaultScope, `thread:${threadId}`];
171
+ if ((await store.knowledgeBySource({
172
+ sourceThreadId: threadId,
173
+ scope: candidateScope,
174
+ limit: 1
175
+ })).records.length === 0) return { response: c.json({ error: "thread_not_found" }, 404) };
176
+ return {
177
+ ...tenant,
178
+ factoryProjectId: projectId,
179
+ store,
180
+ view: "thread",
181
+ threadId,
182
+ scope: candidateScope,
183
+ pinRungs: [{
184
+ rung: "resource",
185
+ scope: resourceRungScope
186
+ }, {
187
+ rung: "thread",
188
+ scope: candidateScope
189
+ }]
190
+ };
191
+ }
192
+ /** Reserved `pinned` node ids at the active view's rungs (one exact-scope lookup per rung). */
193
+ async #pinnedNodeIds(view) {
194
+ const out = [];
195
+ for (const { rung, scope } of view.pinRungs) {
196
+ const node = await view.store.getNodeByName({
197
+ name: PINNED_NODE_NAME,
198
+ scope
199
+ });
200
+ if (node && !node.mergedInto) out.push({
201
+ rung,
202
+ id: node.id
203
+ });
204
+ }
205
+ return out;
206
+ }
207
+ /** Non-deleted pinned records for the given pinned-node ids, visible in the view. */
208
+ async #pinnedRecords(view, pinnedNodeIds) {
209
+ const out = [];
210
+ for (const { rung, id } of pinnedNodeIds) {
211
+ const { records } = await view.store.listKnowledgeAbout({
212
+ node: id,
213
+ scope: view.scope,
214
+ limit: 200
215
+ });
216
+ for (const record of records) out.push({
217
+ rung,
218
+ record
219
+ });
220
+ }
221
+ return out;
222
+ }
223
+ routes() {
224
+ return [
225
+ registerApiRoute("/web/factory/projects/:id/knowledge/graph", {
226
+ method: "GET",
227
+ requiresAuth: false,
228
+ handler: async (c) => {
229
+ const view = await this.#resolveView(loose(c));
230
+ if ("response" in view) return view.response;
231
+ const { store, scope } = view;
232
+ const limits = this.#limits;
233
+ const fetched = await store.listNodes({
234
+ scope,
235
+ limit: limits.maxNodes + 1
236
+ });
237
+ let truncated = fetched.length > limits.maxNodes;
238
+ const pinnedNodeIds = await this.#pinnedNodeIds(view);
239
+ const pinnedNodeIdSet = new Set(pinnedNodeIds.map((p) => p.id));
240
+ const nodes = fetched.slice(0, limits.maxNodes).filter((node) => !pinnedNodeIdSet.has(node.id));
241
+ const recordWindow = [];
242
+ for (const node of nodes) {
243
+ if (recordWindow.length > limits.maxRecords) break;
244
+ const { records } = await store.listKnowledgeAbout({
245
+ node: node.id,
246
+ scope,
247
+ limit: limits.maxRecords + 1 - recordWindow.length
248
+ });
249
+ recordWindow.push(...records);
250
+ }
251
+ recordWindow.sort((a, b) => b.id.localeCompare(a.id));
252
+ if (recordWindow.length > limits.maxRecords) {
253
+ truncated = true;
254
+ recordWindow.length = limits.maxRecords;
255
+ }
256
+ const resolver = new WikilinkResolver(store, nodes, limits.maxFallbackLookups);
257
+ const edges = [];
258
+ const graphRecords = [];
259
+ const edgeSeen = /* @__PURE__ */ new Set();
260
+ const recordCounts = /* @__PURE__ */ new Map();
261
+ for (const record of recordWindow) {
262
+ recordCounts.set(record.node, (recordCounts.get(record.node) ?? 0) + 1);
263
+ const nodeIds = [record.node];
264
+ for (const name of parseKnowledgeWikilinks(record.text)) {
265
+ const target = await resolver.resolve(name, record.scope);
266
+ if (!target) continue;
267
+ if (target.id === record.node) continue;
268
+ if (!resolver.inWindowId(target.id)) continue;
269
+ if (!nodeIds.includes(target.id)) nodeIds.push(target.id);
270
+ const key = `${record.node}\u0000${target.id}`;
271
+ if (edgeSeen.has(key)) continue;
272
+ edgeSeen.add(key);
273
+ edges.push({
274
+ id: `wikilink:${record.node}:${target.id}`,
275
+ source: record.node,
276
+ target: target.id,
277
+ type: "wikilink",
278
+ recordId: record.id
279
+ });
280
+ }
281
+ graphRecords.push({
282
+ id: record.id,
283
+ nodeIds,
284
+ pinned: false,
285
+ text: truncateRecordText(record.text)
286
+ });
287
+ }
288
+ const pinnedRecords = await this.#pinnedRecords(view, pinnedNodeIds);
289
+ const accented = /* @__PURE__ */ new Set();
290
+ for (const { record } of pinnedRecords) {
291
+ const targets = [];
292
+ for (const name of parseKnowledgeWikilinks(record.text)) {
293
+ const target = await resolver.resolve(name, record.scope);
294
+ if (target && resolver.inWindowId(target.id) && !targets.includes(target.id)) targets.push(target.id);
295
+ }
296
+ graphRecords.push({
297
+ id: record.id,
298
+ nodeIds: targets,
299
+ pinned: true,
300
+ text: truncateRecordText(record.text)
301
+ });
302
+ if (targets.length === 1) {
303
+ accented.add(targets[0]);
304
+ continue;
305
+ }
306
+ for (let a = 0; a < targets.length; a += 1) for (let b = a + 1; b < targets.length; b += 1) {
307
+ const key = `${targets[a]}\u0000${targets[b]}\u0000pin`;
308
+ if (edgeSeen.has(key)) continue;
309
+ edgeSeen.add(key);
310
+ edges.push({
311
+ id: `pin:${record.id}:${targets[a]}:${targets[b]}`,
312
+ source: targets[a],
313
+ target: targets[b],
314
+ type: "wikilink",
315
+ recordId: record.id,
316
+ pinned: true
317
+ });
318
+ }
319
+ }
320
+ const pinCensus = {
321
+ resource: pinnedRecords.filter((p) => p.rung === "resource").length,
322
+ thread: view.view === "thread" ? pinnedRecords.filter((p) => p.rung === "thread").length : null
323
+ };
324
+ const activity = await store.listActivity({
325
+ scope,
326
+ limit: 1
327
+ });
328
+ const payload = {
329
+ view: view.view,
330
+ ...view.threadId ? { threadId: view.threadId } : {},
331
+ nodes: nodes.map((node) => ({
332
+ id: node.id,
333
+ name: node.name,
334
+ kind: node.kind,
335
+ scope: node.scope,
336
+ rung: deepestRung(node.scope),
337
+ pinned: accented.has(node.id),
338
+ recordCount: recordCounts.get(node.id) ?? 0,
339
+ createdAt: node.createdAt.toISOString(),
340
+ updatedAt: node.updatedAt.toISOString()
341
+ })),
342
+ edges,
343
+ records: graphRecords,
344
+ truncated,
345
+ outOfWindow: [...resolver.outOfWindow.values()],
346
+ unresolvedCapped: {
347
+ count: resolver.cappedCount,
348
+ names: resolver.cappedNames
349
+ },
350
+ pinCensus,
351
+ version: activity[0]?.id ?? null
352
+ };
353
+ return c.json(payload);
354
+ }
355
+ }),
356
+ registerApiRoute("/web/factory/projects/:id/knowledge/nodes/:nodeId", {
357
+ method: "GET",
358
+ requiresAuth: false,
359
+ handler: async (c) => {
360
+ const view = await this.#resolveView(loose(c));
361
+ if ("response" in view) return view.response;
362
+ const { store, scope } = view;
363
+ const nodeId = loose(c).req.param("nodeId");
364
+ if (!nodeId || nodeId.length > 512) return c.json({ error: "node_not_found" }, 404);
365
+ const node = await store.getNode(nodeId);
366
+ if (!node || !isKnowledgeScopeVisible(node.scope, scope)) return c.json({ error: "node_not_found" }, 404);
367
+ const pinnedNodeIds = await this.#pinnedNodeIds(view);
368
+ const pinnedNodeIdSet = new Set(pinnedNodeIds.map((p) => p.id));
369
+ const [owned, mentioning] = await Promise.all([store.listKnowledgeAbout({
370
+ node: node.id,
371
+ scope,
372
+ limit: 200
373
+ }), store.listKnowledgeMentioning({
374
+ node: node.id,
375
+ scope,
376
+ limit: 200
377
+ })]);
378
+ const seen = /* @__PURE__ */ new Set();
379
+ const records = [];
380
+ const push = (record, relation) => {
381
+ if (seen.has(record.id)) return;
382
+ seen.add(record.id);
383
+ records.push({
384
+ id: record.id,
385
+ node: record.node,
386
+ relation,
387
+ text: record.text,
388
+ scope: record.scope,
389
+ rung: deepestRung(record.scope),
390
+ sourceThreadId: record.sourceThreadId,
391
+ capturedAt: record.capturedAt.toISOString(),
392
+ ...record.when ? { when: record.when.toISOString() } : {},
393
+ pinned: pinnedNodeIdSet.has(record.node),
394
+ ...record.metadata ? { metadata: record.metadata } : {}
395
+ });
396
+ };
397
+ for (const record of [...owned.records].sort((a, b) => b.id.localeCompare(a.id))) push(record, "owned");
398
+ for (const record of [...mentioning.records].sort((a, b) => b.id.localeCompare(a.id))) push(record, "mentions");
399
+ const payload = {
400
+ node: {
401
+ id: node.id,
402
+ name: node.name,
403
+ kind: node.kind,
404
+ content: node.content ?? "",
405
+ scope: node.scope,
406
+ rung: deepestRung(node.scope),
407
+ createdAt: node.createdAt.toISOString(),
408
+ updatedAt: node.updatedAt.toISOString()
409
+ },
410
+ records
411
+ };
412
+ return c.json(payload);
413
+ }
414
+ }),
415
+ registerApiRoute("/web/factory/projects/:id/knowledge/activity", {
416
+ method: "GET",
417
+ requiresAuth: false,
418
+ handler: async (c) => {
419
+ const view = await this.#resolveView(loose(c));
420
+ if ("response" in view) return view.response;
421
+ const events = await view.store.listActivity({
422
+ scope: view.scope,
423
+ limit: 100
424
+ });
425
+ return c.json({ events: events.map((event) => ({
426
+ id: event.id,
427
+ action: event.action,
428
+ recordType: event.recordType,
429
+ recordId: event.recordId,
430
+ scope: event.scope,
431
+ ...event.sourceThreadId ? { sourceThreadId: event.sourceThreadId } : {},
432
+ createdAt: event.createdAt.toISOString()
433
+ })) });
434
+ }
435
+ })
436
+ ];
437
+ }
438
+ };
439
+ //#endregion
440
+ export { KnowledgeRoutes };
441
+
442
+ //# sourceMappingURL=knowledge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knowledge.js","names":["#inWindow","#windowIds","#fallbackCache","#store","#maxFallbackLookups","#trackOutOfWindow","#fallbackLookups","#cappedSeen","#limits","#resolveTenant","#resolveView","#pinnedNodeIds","#pinnedRecords"],"sources":["../../src/routes/knowledge.ts"],"sourcesContent":["/**\n * Read-only Mastra `apiRoutes` exposing the factory project's knowledge graph.\n *\n * Serves the Knowledge page in factory-ui: a polling graph snapshot (nodes\n * as nodes, wikilink edges derived from record text), a node flyout payload\n * with per-record provenance, and the recent activity feed. Every endpoint is a\n * GET — this module never writes knowledge.\n *\n * Scoping is fail-closed: the org and resource rungs are derived server-side\n * from the authenticated caller and the validated `:id` project. The DEFAULT\n * view queries `[org:<orgId>, resource:<projectId>]` (org + project records).\n * Thread-scoped records are reachable ONLY via an explicit, server-validated\n * `threadId` query parameter (the drill-down view), which appends the thread\n * rung to the query scope. A thread is drillable iff it produced knowledge\n * visible under the caller's org/project prefix; unknown or cross-org threads\n * 404 — never a silent fallback to the default view.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { KnowledgeNode, KnowledgeRecord, KnowledgeScope, KnowledgeStorage } from '@mastra/core/storage';\nimport {\n canonicalizeKnowledgeScope,\n isKnowledgeScopeVisible,\n knowledgeScopeKey,\n parseKnowledgeWikilinks,\n} from '@mastra/core/storage';\nimport type { Context } from 'hono';\n\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { RouteDependencies } from './route.js';\nimport { Route } from './route.js';\n\n/** Reserved node that anchors pinned records (see subconscious/pinned.ts). */\nconst PINNED_NODE_NAME = 'pinned';\n\n/** Hover-card budget for record text shipped in the graph payload. */\nconst RECORD_TEXT_LIMIT = 240;\n\nfunction truncateRecordText(text: string): string {\n return text.length > RECORD_TEXT_LIMIT ? `${text.slice(0, RECORD_TEXT_LIMIT - 1)}…` : text;\n}\n\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/** Window caps. Injectable at construction only — never per-request. */\nexport interface KnowledgeRouteLimits {\n /** Max nodes in a graph snapshot (newest-first). */\n maxNodes: number;\n /** Max records parsed for edges per snapshot (newest-first). */\n maxRecords: number;\n /** Max fallback `resolveNode` store lookups per request (deduped per unique name+scope). */\n maxFallbackLookups: number;\n}\n\nconst DEFAULT_LIMITS: KnowledgeRouteLimits = { maxNodes: 500, maxRecords: 2000, maxFallbackLookups: 100 };\n\nexport interface KnowledgeRoutesDeps extends RouteDependencies {\n /** Factory projects domain — validates the `:id` project belongs to the caller's org. */\n projects: FactoryProjectsStorage;\n /** Lazy handle to the knowledge storage domain; endpoints 503 when absent. */\n knowledge: () => Promise<KnowledgeStorage | undefined>;\n limits?: Partial<KnowledgeRouteLimits>;\n}\n\n/** A graph node. `recordCount` is window-derived (records inside the snapshot window only). */\nexport interface KnowledgeGraphNode {\n id: string;\n name: string;\n kind: string;\n scope: KnowledgeScope;\n /** Deepest rung of the record's scope: org | resource | thread. */\n rung: 'org' | 'resource' | 'thread';\n /**\n * True when a non-deleted pinned record's wikilinks reference ONLY this\n * node (A9: multi-target pins mark their edges instead — the pin is\n * about the relationship; a single-target pin has no edge to carry it).\n */\n pinned: boolean;\n /** Records owned by this node INSIDE the snapshot window (not a total). */\n recordCount: number;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface KnowledgeGraphEdge {\n id: string;\n /** The owning node of the record (its `node`). */\n source: string;\n /** The wikilink-resolved node. */\n target: string;\n /**\n * Always 'wikilink': the record's `node` is the edge SOURCE, so the\n * plan's \"parent link\" collapses into the wikilink edge — nodes carry no\n * separate parent field to derive a second edge type from.\n */\n type: 'wikilink';\n /** The record whose text produced the edge. */\n recordId: string;\n /**\n * True when the edge is derived from a PINNED record linking two nodes —\n * the pin marks the relationship, so the accent lives on the edge (A9).\n */\n pinned?: boolean;\n}\n\n/**\n * A knowledge record as a first-class graph element (A11): every record in the window,\n * with the in-window nodes it touches. The client renders by arity —\n * 1 node: a small dot linked to it; 2: the connecting line; 3+: a midpoint\n * junction splitting to each node. Pin records have their hidden reserved\n * owner omitted, so their arity comes purely from wikilink targets.\n */\nexport interface KnowledgeGraphRecord {\n /** The record id. */\n id: string;\n /** Owner node first (omitted for pins), then resolved wikilink targets. */\n nodeIds: string[];\n pinned: boolean;\n /** Record text, truncated for hover cards. */\n text: string;\n}\n\nexport interface KnowledgeGraphPayload {\n view: 'project' | 'thread';\n threadId?: string;\n nodes: KnowledgeGraphNode[];\n edges: KnowledgeGraphEdge[];\n records: KnowledgeGraphRecord[];\n /** True when the node or record window cap was hit (newest-first window). */\n truncated: boolean;\n /** Wikilink targets that resolved in the store but fell outside the node window. */\n outOfWindow: Array<{ id: string; name: string }>;\n /** Unique unknown names skipped once the fallback-lookup cap was hit. */\n unresolvedCapped: { count: number; names: string[] };\n /** Pin counts per rung of the active view (thread is null in the default view). */\n pinCensus: { resource: number; thread: number | null };\n /** Change hint: newest knowledge activity id (per-process monotonic — hint only). */\n version: string | null;\n}\n\nexport interface KnowledgeNodeRecordPayload {\n id: string;\n node: string;\n /** 'owned' when the node is the record's parent, 'mentions' when it only wikilinks it. */\n relation: 'owned' | 'mentions';\n text: string;\n scope: KnowledgeScope;\n rung: 'org' | 'resource' | 'thread';\n sourceThreadId: string;\n capturedAt: string;\n when?: string;\n pinned: boolean;\n metadata?: Record<string, unknown>;\n}\n\nexport interface KnowledgeNodePayload {\n node: {\n id: string;\n name: string;\n kind: string;\n content: string;\n scope: KnowledgeScope;\n rung: 'org' | 'resource' | 'thread';\n createdAt: string;\n updatedAt: string;\n };\n records: KnowledgeNodeRecordPayload[];\n}\n\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\nfunction deepestRung(scope: KnowledgeScope): 'org' | 'resource' | 'thread' {\n let rung: 'org' | 'resource' | 'thread' = 'org';\n for (const entry of scope) {\n const ns = entry.slice(0, entry.indexOf(':'));\n if (ns === 'thread') return 'thread';\n if (ns === 'resource') rung = 'resource';\n }\n return rung;\n}\n\nfunction boundedThreadId(raw: string | undefined): string | undefined {\n if (!raw) return undefined;\n const trimmed = raw.trim();\n return trimmed.length > 0 && trimmed.length <= 512 ? trimmed : undefined;\n}\n\ninterface ResolvedView {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n store: KnowledgeStorage;\n view: 'project' | 'thread';\n threadId?: string;\n /** The query scope for the active view. */\n scope: KnowledgeScope;\n /** Exact scopes where a reserved `pinned` node may live for this view. */\n pinRungs: Array<{ rung: 'resource' | 'thread'; scope: KnowledgeScope }>;\n}\n\n/**\n * In-record + capped-fallback wikilink resolver, shared by both endpoints.\n * Resolution uses the store's own algorithm: a descending-prefix walk over the\n * record's canonical scope matching canonical name + exact scope key at each\n * prefix, so an edge never depends on whether the target landed in the window.\n */\nclass WikilinkResolver {\n /** exact `${scopeKey}\\u0000${lowerName}` → node, from the fetched window. */\n readonly #inWindow = new Map<string, KnowledgeNode>();\n readonly #windowIds = new Set<string>();\n /** `${recordScopeKey}\\u0000${lowerName}` → fallback result (null = dangling). */\n readonly #fallbackCache = new Map<string, KnowledgeNode | null>();\n #fallbackLookups = 0;\n readonly #store: KnowledgeStorage;\n readonly #maxFallbackLookups: number;\n readonly outOfWindow = new Map<string, { id: string; name: string }>();\n readonly cappedNames: string[] = [];\n #cappedSeen = new Set<string>();\n\n constructor(store: KnowledgeStorage, nodes: KnowledgeNode[], maxFallbackLookups: number) {\n this.#store = store;\n this.#maxFallbackLookups = maxFallbackLookups;\n for (const node of nodes) {\n this.#inWindow.set(`${knowledgeScopeKey(node.scope)}\\u0000${node.name.trim().toLocaleLowerCase()}`, node);\n this.#windowIds.add(node.id);\n }\n }\n\n inWindowId(id: string): boolean {\n return this.#windowIds.has(id);\n }\n\n /** Resolve a wikilink name from a knowledge record's scope. Returns the node or null (dangling/capped). */\n async resolve(name: string, recordScope: KnowledgeScope): Promise<KnowledgeNode | null> {\n const canonical = canonicalizeKnowledgeScope(recordScope);\n const lower = name.trim().toLocaleLowerCase();\n for (let length = canonical.length; length > 0; length--) {\n const hit = this.#inWindow.get(`${knowledgeScopeKey(canonical.slice(0, length))}\\u0000${lower}`);\n if (hit) return hit;\n }\n const cacheKey = `${knowledgeScopeKey(canonical)}\\u0000${lower}`;\n if (this.#fallbackCache.has(cacheKey)) {\n return this.#trackOutOfWindow(this.#fallbackCache.get(cacheKey) ?? null);\n }\n if (this.#fallbackLookups >= this.#maxFallbackLookups) {\n if (!this.#cappedSeen.has(lower) && this.cappedNames.length < 100) {\n this.#cappedSeen.add(lower);\n this.cappedNames.push(name.trim());\n } else if (!this.#cappedSeen.has(lower)) {\n this.#cappedSeen.add(lower);\n }\n return null;\n }\n this.#fallbackLookups += 1;\n let resolved: KnowledgeNode | null = null;\n try {\n resolved = await this.#store.resolveNode({ name, scope: canonical });\n } catch {\n resolved = null;\n }\n this.#fallbackCache.set(cacheKey, resolved);\n return this.#trackOutOfWindow(resolved);\n }\n\n #trackOutOfWindow(node: KnowledgeNode | null): KnowledgeNode | null {\n if (node && !this.#windowIds.has(node.id)) {\n this.outOfWindow.set(node.id, { id: node.id, name: node.name });\n }\n return node;\n }\n\n get cappedCount(): number {\n return this.#cappedSeen.size;\n }\n}\n\nexport class KnowledgeRoutes extends Route<KnowledgeRoutesDeps> {\n readonly #limits: KnowledgeRouteLimits;\n\n constructor(deps: KnowledgeRoutesDeps) {\n super(deps);\n this.#limits = { ...DEFAULT_LIMITS, ...deps.limits };\n }\n\n /** Resolve the `(orgId, userId)` tenant or a ready-to-return error response. */\n async #resolveTenant(c: Context): Promise<{ orgId: string; userId: string } | { response: Response }> {\n await this.deps.auth.ensureUser(c);\n const tenant = this.deps.auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n { error: 'organization_required', message: 'The knowledge graph requires an organization.' },\n 403,\n ),\n };\n }\n return { orgId: tenant.orgId, userId: tenant.userId };\n }\n\n /**\n * Resolve tenant + org-owned project + knowledge store + the active view's\n * query scope. The ONE seam both endpoints share, so the default/thread view\n * scope and the pin rungs cannot drift between them.\n *\n * threadId validation runs a single `listRecordsBySource` lookup with\n * `limit: 1` AT THE CANDIDATE SCOPE `[org, resource, thread:<id>]` — the\n * store's own visibility predicate is the authorization: the thread's own\n * records (equal scope key) and its project/org captures (prefix) match, while\n * a cross-org thread's records match nothing → zero rows → 404.\n */\n async #resolveView(c: Context): Promise<ResolvedView | { response: Response }> {\n const tenant = await this.#resolveTenant(c);\n if ('response' in tenant) return tenant;\n\n const projectId = c.req.param('id');\n if (!projectId || !UUID_RE.test(projectId)) {\n return { response: c.json({ error: 'Project not found' }, 404) };\n }\n const { projects } = this.deps;\n await projects.ensureReady();\n const project = await projects.get({ orgId: tenant.orgId, id: projectId });\n if (!project) {\n return { response: c.json({ error: 'Project not found' }, 404) };\n }\n\n const store = await this.deps.knowledge();\n if (!store) {\n return {\n response: c.json(\n { error: 'knowledge_unavailable', message: 'The knowledge storage domain is not configured.' },\n 503,\n ),\n };\n }\n\n const defaultScope: KnowledgeScope = [`org:${tenant.orgId}`, `resource:${projectId}`];\n const resourceRungScope = defaultScope;\n\n const threadId = boundedThreadId(c.req.query('threadId'));\n if (c.req.query('threadId') !== undefined && !threadId) {\n return { response: c.json({ error: 'thread_not_found' }, 404) };\n }\n if (!threadId) {\n return {\n ...tenant,\n factoryProjectId: projectId,\n store,\n view: 'project',\n scope: defaultScope,\n pinRungs: [{ rung: 'resource', scope: resourceRungScope }],\n };\n }\n\n const candidateScope: KnowledgeScope = [...defaultScope, `thread:${threadId}`];\n const probe = await store.knowledgeBySource({ sourceThreadId: threadId, scope: candidateScope, limit: 1 });\n if (probe.records.length === 0) {\n return { response: c.json({ error: 'thread_not_found' }, 404) };\n }\n return {\n ...tenant,\n factoryProjectId: projectId,\n store,\n view: 'thread',\n threadId,\n scope: candidateScope,\n pinRungs: [\n { rung: 'resource', scope: resourceRungScope },\n { rung: 'thread', scope: candidateScope },\n ],\n };\n }\n\n /** Reserved `pinned` node ids at the active view's rungs (one exact-scope lookup per rung). */\n async #pinnedNodeIds(view: ResolvedView): Promise<Array<{ rung: 'resource' | 'thread'; id: string }>> {\n const out: Array<{ rung: 'resource' | 'thread'; id: string }> = [];\n for (const { rung, scope } of view.pinRungs) {\n const node = await view.store.getNodeByName({ name: PINNED_NODE_NAME, scope });\n if (node && !node.mergedInto) out.push({ rung, id: node.id });\n }\n return out;\n }\n\n /** Non-deleted pinned records for the given pinned-node ids, visible in the view. */\n async #pinnedRecords(\n view: ResolvedView,\n pinnedNodeIds: Array<{ rung: 'resource' | 'thread'; id: string }>,\n ): Promise<Array<{ rung: 'resource' | 'thread'; record: KnowledgeRecord }>> {\n const out: Array<{ rung: 'resource' | 'thread'; record: KnowledgeRecord }> = [];\n for (const { rung, id } of pinnedNodeIds) {\n const { records } = await view.store.listKnowledgeAbout({ node: id, scope: view.scope, limit: 200 });\n for (const record of records) out.push({ rung, record });\n }\n return out;\n }\n\n routes(): ApiRoute[] {\n return [\n // ── Graph snapshot: nodes + derived edges, polled by the page ──────────\n registerApiRoute('/web/factory/projects/:id/knowledge/graph', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const view = await this.#resolveView(loose(c));\n if ('response' in view) return view.response;\n const { store, scope } = view;\n const limits = this.#limits;\n\n // Nodes, newest-first; +1 to detect truncation.\n const fetched = await store.listNodes({ scope, limit: limits.maxNodes + 1 });\n let truncated = fetched.length > limits.maxNodes;\n const pinnedNodeIds = await this.#pinnedNodeIds(view);\n const pinnedNodeIdSet = new Set(pinnedNodeIds.map(p => p.id));\n const nodes = fetched.slice(0, limits.maxNodes).filter(node => !pinnedNodeIdSet.has(node.id));\n\n // Record window: per-node owned records, then newest-first overall.\n const recordWindow: KnowledgeRecord[] = [];\n for (const node of nodes) {\n if (recordWindow.length > limits.maxRecords) break;\n const { records } = await store.listKnowledgeAbout({\n node: node.id,\n scope,\n limit: limits.maxRecords + 1 - recordWindow.length,\n });\n recordWindow.push(...records);\n }\n // Record ids are ULIDs — descending id = newest-first.\n recordWindow.sort((a, b) => b.id.localeCompare(a.id));\n if (recordWindow.length > limits.maxRecords) {\n truncated = true;\n recordWindow.length = limits.maxRecords;\n }\n\n const resolver = new WikilinkResolver(store, nodes, limits.maxFallbackLookups);\n\n // Edges: owner node (the record's parent link) → wikilinked node.\n // Graph records: every windowed record with its in-window node set,\n // owner first. The client renders dots, lines, or junctions by arity.\n const edges: KnowledgeGraphEdge[] = [];\n const graphRecords: KnowledgeGraphRecord[] = [];\n const edgeSeen = new Set<string>();\n const recordCounts = new Map<string, number>();\n for (const record of recordWindow) {\n recordCounts.set(record.node, (recordCounts.get(record.node) ?? 0) + 1);\n const nodeIds = [record.node];\n for (const name of parseKnowledgeWikilinks(record.text)) {\n const target = await resolver.resolve(name, record.scope);\n if (!target) continue;\n if (target.id === record.node) continue;\n if (!resolver.inWindowId(target.id)) continue;\n if (!nodeIds.includes(target.id)) nodeIds.push(target.id);\n const key = `${record.node}\\u0000${target.id}`;\n if (edgeSeen.has(key)) continue;\n edgeSeen.add(key);\n edges.push({\n id: `wikilink:${record.node}:${target.id}`,\n source: record.node,\n target: target.id,\n type: 'wikilink',\n recordId: record.id,\n });\n }\n graphRecords.push({ id: record.id, nodeIds, pinned: false, text: truncateRecordText(record.text) });\n }\n\n // Pins mark relationships. The reserved owner node is omitted, so\n // arity comes purely from wikilink targets.\n const pinnedRecords = await this.#pinnedRecords(view, pinnedNodeIds);\n const accented = new Set<string>();\n for (const { record } of pinnedRecords) {\n const targets: string[] = [];\n for (const name of parseKnowledgeWikilinks(record.text)) {\n const target = await resolver.resolve(name, record.scope);\n if (target && resolver.inWindowId(target.id) && !targets.includes(target.id)) {\n targets.push(target.id);\n }\n }\n graphRecords.push({ id: record.id, nodeIds: targets, pinned: true, text: truncateRecordText(record.text) });\n if (targets.length === 1) {\n accented.add(targets[0]!);\n continue;\n }\n for (let a = 0; a < targets.length; a += 1) {\n for (let b = a + 1; b < targets.length; b += 1) {\n const key = `${targets[a]}\\u0000${targets[b]}\\u0000pin`;\n if (edgeSeen.has(key)) continue;\n edgeSeen.add(key);\n edges.push({\n id: `pin:${record.id}:${targets[a]}:${targets[b]}`,\n source: targets[a]!,\n target: targets[b]!,\n type: 'wikilink',\n recordId: record.id,\n pinned: true,\n });\n }\n }\n }\n const pinCensus = {\n resource: pinnedRecords.filter(p => p.rung === 'resource').length,\n thread: view.view === 'thread' ? pinnedRecords.filter(p => p.rung === 'thread').length : null,\n };\n\n const activity = await store.listActivity({ scope, limit: 1 });\n\n const payload: KnowledgeGraphPayload = {\n view: view.view,\n ...(view.threadId ? { threadId: view.threadId } : {}),\n nodes: nodes.map(node => ({\n id: node.id,\n name: node.name,\n kind: node.kind,\n scope: node.scope,\n rung: deepestRung(node.scope),\n pinned: accented.has(node.id),\n recordCount: recordCounts.get(node.id) ?? 0,\n createdAt: node.createdAt.toISOString(),\n updatedAt: node.updatedAt.toISOString(),\n })),\n edges,\n records: graphRecords,\n truncated,\n outOfWindow: [...resolver.outOfWindow.values()],\n unresolvedCapped: { count: resolver.cappedCount, names: resolver.cappedNames },\n pinCensus,\n version: activity[0]?.id ?? null,\n };\n return c.json(payload);\n },\n }),\n\n // ── Node flyout payload: details + provenance-rich records ───────────────\n registerApiRoute('/web/factory/projects/:id/knowledge/nodes/:nodeId', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const view = await this.#resolveView(loose(c));\n if ('response' in view) return view.response;\n const { store, scope } = view;\n const nodeId = loose(c).req.param('nodeId');\n if (!nodeId || nodeId.length > 512) return c.json({ error: 'node_not_found' }, 404);\n\n const node = await store.getNode(nodeId);\n // getNode is a bare id lookup with no scope predicate. This explicit\n // visibility check prevents an IDOR.\n if (!node || !isKnowledgeScopeVisible(node.scope, scope)) {\n return c.json({ error: 'node_not_found' }, 404);\n }\n\n const pinnedNodeIds = await this.#pinnedNodeIds(view);\n const pinnedNodeIdSet = new Set(pinnedNodeIds.map(p => p.id));\n\n const [owned, mentioning] = await Promise.all([\n store.listKnowledgeAbout({ node: node.id, scope, limit: 200 }),\n store.listKnowledgeMentioning({ node: node.id, scope, limit: 200 }),\n ]);\n const seen = new Set<string>();\n const records: KnowledgeNodeRecordPayload[] = [];\n const push = (record: KnowledgeRecord, relation: 'owned' | 'mentions') => {\n if (seen.has(record.id)) return;\n seen.add(record.id);\n records.push({\n id: record.id,\n node: record.node,\n relation,\n text: record.text,\n scope: record.scope,\n rung: deepestRung(record.scope),\n sourceThreadId: record.sourceThreadId,\n capturedAt: record.capturedAt.toISOString(),\n ...(record.when ? { when: record.when.toISOString() } : {}),\n pinned: pinnedNodeIdSet.has(record.node),\n ...(record.metadata ? { metadata: record.metadata } : {}),\n });\n };\n // Owned first, newest-first within each group. Record ids are ULIDs.\n for (const record of [...owned.records].sort((a, b) => b.id.localeCompare(a.id))) push(record, 'owned');\n for (const record of [...mentioning.records].sort((a, b) => b.id.localeCompare(a.id))) {\n push(record, 'mentions');\n }\n\n const payload: KnowledgeNodePayload = {\n node: {\n id: node.id,\n name: node.name,\n kind: node.kind,\n content: node.content ?? '',\n scope: node.scope,\n rung: deepestRung(node.scope),\n createdAt: node.createdAt.toISOString(),\n updatedAt: node.updatedAt.toISOString(),\n },\n records,\n };\n return c.json(payload);\n },\n }),\n\n // ── Recent activity feed for the live-arrival affordance ───────────────\n registerApiRoute('/web/factory/projects/:id/knowledge/activity', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const view = await this.#resolveView(loose(c));\n if ('response' in view) return view.response;\n const events = await view.store.listActivity({ scope: view.scope, limit: 100 });\n return c.json({\n events: events.map(event => ({\n id: event.id,\n action: event.action,\n recordType: event.recordType,\n recordId: event.recordId,\n scope: event.scope,\n ...(event.sourceThreadId ? { sourceThreadId: event.sourceThreadId } : {}),\n createdAt: event.createdAt.toISOString(),\n })),\n });\n },\n }),\n ];\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import type { ApiRoute } from '@mastra/core/server';
2
2
  import type { SessionRetirementCoordinator } from '../sandbox/session-retirement.js';
3
3
  import type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';
4
- import type { SourceControlStorage } from '../storage/domains/source-control/base.js';
4
+ import type { ProjectRepository, SourceControlStorage } from '../storage/domains/source-control/base.js';
5
5
  import type { RouteDependencies } from './route.js';
6
6
  import { Route } from './route.js';
7
7
  export interface ProjectRoutesDeps extends RouteDependencies {
@@ -11,6 +11,14 @@ export interface ProjectRoutesDeps extends RouteDependencies {
11
11
  sourceControl: SourceControlStorage;
12
12
  /** Integration ids allowed as source-control connection targets. */
13
13
  versionControlIntegrationIds?: string[];
14
+ /**
15
+ * Fire-and-forget hook invoked after a repository is linked to a project —
16
+ * kicks the initial base-checkpoint build. Must never throw.
17
+ */
18
+ onProjectRepositoryLinked?: (args: {
19
+ orgId: string;
20
+ projectRepository: ProjectRepository;
21
+ }) => void;
14
22
  /** Shared lifecycle for retiring sessions before their owning records are deleted. */
15
23
  sessionRetirement?: SessionRetirementCoordinator;
16
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/routes/projects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,kCAAkC,CAAC;AACrF,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EAEV,oBAAoB,EAGrB,MAAM,2CAA2C,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAkKnC,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,wDAAwD;IACxD,QAAQ,EAAE,sBAAsB,CAAC;IACjC,2EAA2E;IAC3E,aAAa,EAAE,oBAAoB,CAAC;IACpC,oEAAoE;IACpE,4BAA4B,CAAC,EAAE,MAAM,EAAE,CAAC;IACxC,sFAAsF;IACtF,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;CAClD;AAED,qBAAa,aAAc,SAAQ,KAAK,CAAC,iBAAiB,CAAC;;gBAG7C,IAAI,EAAE,iBAAiB;IA8EnC,MAAM,IAAI,QAAQ,EAAE;CAuPrB"}
1
+ {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/routes/projects.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,kCAAkC,CAAC;AACrF,OAAO,KAAK,EAEV,sBAAsB,EAEvB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,KAAK,EACV,iBAAiB,EACjB,oBAAoB,EAGrB,MAAM,2CAA2C,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAkKnC,MAAM,WAAW,iBAAkB,SAAQ,iBAAiB;IAC1D,wDAAwD;IACxD,QAAQ,EAAE,sBAAsB,CAAC;IACjC,2EAA2E;IAC3E,aAAa,EAAE,oBAAoB,CAAC;IACpC,oEAAoE;IACpE,4BAA4B,CAAC,EAAE,MAAM,EAAE,CAAC;IACxC;;;OAGG;IACH,yBAAyB,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,iBAAiB,CAAA;KAAE,KAAK,IAAI,CAAC;IACpG,sFAAsF;IACtF,iBAAiB,CAAC,EAAE,4BAA4B,CAAC;CAClD;AAED,qBAAa,aAAc,SAAQ,KAAK,CAAC,iBAAiB,CAAC;;gBAG7C,IAAI,EAAE,iBAAiB;IA8EnC,MAAM,IAAI,QAAQ,EAAE;CA4PrB"}
@@ -434,6 +434,14 @@ var ProjectRoutes = class extends Route {
434
434
  createdByUserId: tenant.userId,
435
435
  ...input
436
436
  });
437
+ try {
438
+ this.deps.onProjectRepositoryLinked?.({
439
+ orgId: tenant.orgId,
440
+ projectRepository
441
+ });
442
+ } catch (error) {
443
+ console.warn("[factory] onProjectRepositoryLinked failed after a successful repository link:", error);
444
+ }
437
445
  return context.json({ projectRepository: await this.#repositoryPayload(found.handle, tenant.orgId, projectRepository) }, 201);
438
446
  }
439
447
  }),