@ai-matrx/associations 0.10.7 → 0.11.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.
@@ -46,6 +46,7 @@ __export(react_exports, {
46
46
  UnresolvedRef: () => UnresolvedRef,
47
47
  attachedKey: () => attachedKey,
48
48
  buildCommentTree: () => buildCommentTree,
49
+ containerLinksArgs: () => containerLinksArgs,
49
50
  getContentRoleMeta: () => getContentRoleMeta,
50
51
  lazyPickerOverride: () => lazyPickerOverride,
51
52
  lazyWindowShell: () => lazyWindowShell,
@@ -1254,6 +1255,73 @@ var useEntityRelationships = useAssociations;
1254
1255
 
1255
1256
  // src/react/hooks/useContainerLinks.ts
1256
1257
  var import_react3 = require("react");
1258
+
1259
+ // src/edges.ts
1260
+ var ASSOCIATION_TARGET_TYPES = [
1261
+ "organization",
1262
+ // an org — the top-level container resources attach into
1263
+ "scope",
1264
+ "scope_type",
1265
+ "project",
1266
+ "task",
1267
+ "context_item",
1268
+ "thread",
1269
+ "war_room",
1270
+ "category",
1271
+ "conversation",
1272
+ // a chat conversation a working_document is attached to
1273
+ "fc_set",
1274
+ // a flashcard set (card→set membership)
1275
+ "fc_card",
1276
+ // a flashcard (card→card hierarchy, quiz→card)
1277
+ "study_media",
1278
+ // a study-media artifact (summary/mind_map) — converter lineage target
1279
+ "assessment",
1280
+ // a quiz / practice test — a converter lineage target + origin (assessment→artifact 'source')
1281
+ "note",
1282
+ // a note — the origin a converted study artifact links back to (artifact→note 'source' lineage, P4)
1283
+ "file",
1284
+ // a file (card→file media + source lineage)
1285
+ "quiz_session",
1286
+ // a quiz a card is used in
1287
+ "agent",
1288
+ // an orchestrator agent — the container an Orchestra's member agents attach into (role 'member'); an 'orchestra' self-edge marks the orchestrator
1289
+ "agent_term_list",
1290
+ // a term list — durable file attachments target its vocabulary resource
1291
+ "research_tag",
1292
+ // a research tag a source is filed under (rs_source_tag M2M collapsed into associations)
1293
+ "skill",
1294
+ // a skill — projects link to it (skill→project, role 'member'); code_files attach as resources (code_file→skill, role 'resource')
1295
+ "surface",
1296
+ // a UI surface — agents bind here (agent→surface); context menu reads via agent.menu_surface
1297
+ "plan_entity",
1298
+ // a Content Planning source/media citation — plan_node role edges (about/cites/embeds) target it
1299
+ "plan_node",
1300
+ // a planned page — research topics/tags attach here and follow realization into web_page
1301
+ "web_site",
1302
+ // a web.site — parties attach onto a site's roster (party→web_site role 'writes_for')
1303
+ "seo_topic",
1304
+ // a taxonomy topic — plan_node/plan_entity tag into it (role 'topic')
1305
+ "seo_keyword",
1306
+ // a keyword — plan_node attaches secondaries (role 'secondary_keyword'; the primary is an FK, never an edge)
1307
+ "web_page",
1308
+ // a canonical page — the page workspace attaches notes/tasks/files/keywords onto it (keyword edges: role 'primary'|'supporting')
1309
+ "web_screenshot",
1310
+ // a page capture — per-image attachments on the page workspace
1311
+ "party",
1312
+ // a CRM person/company (crm.party) — the record page attaches tasks/files/notes onto it
1313
+ "crm_outreach_list",
1314
+ // a CRM outreach list (a worked send/dial queue) — parties and resources attach into it
1315
+ "crm_deal",
1316
+ // a CRM deal (crm.deal) — people on it (role 'deal_contact'), tasks/files attach onto it
1317
+ "rulebook"
1318
+ // a Masterwork Rulebook — its Scout interviews attach here (conversation→rulebook, role 'interview')
1319
+ ];
1320
+ function isAssociationTargetType(value) {
1321
+ return typeof value === "string" && ASSOCIATION_TARGET_TYPES.includes(value);
1322
+ }
1323
+
1324
+ // src/react/hooks/useContainerLinks.ts
1257
1325
  function metadataRecord(metadata) {
1258
1326
  if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) {
1259
1327
  return metadata == null ? {} : null;
@@ -1262,6 +1330,7 @@ function metadataRecord(metadata) {
1262
1330
  }
1263
1331
  function useContainerLinks(args) {
1264
1332
  const { containerType, containerId, orgId } = args;
1333
+ const outgoing = args.direction === "outgoing";
1265
1334
  const store = useAssociationsStore();
1266
1335
  const {
1267
1336
  edges,
@@ -1272,7 +1341,7 @@ function useContainerLinks(args) {
1272
1341
  type: containerType,
1273
1342
  id: containerId
1274
1343
  });
1275
- const conversationKey = containerType === "conversation" && containerId ? `conversation:${containerId}` : null;
1344
+ const conversationKey = !outgoing && containerType === "conversation" && containerId ? `conversation:${containerId}` : null;
1276
1345
  const activeConversationKey = (0, import_react3.useRef)(conversationKey);
1277
1346
  const conversationGeneration = (0, import_react3.useRef)(0);
1278
1347
  const [conversationResult, setConversationResult] = (0, import_react3.useState)(null);
@@ -1328,9 +1397,9 @@ function useContainerLinks(args) {
1328
1397
  }, [loadConversationFiles]);
1329
1398
  const conversationFiles = conversationResult?.key === conversationKey ? conversationResult.files : [];
1330
1399
  const conversationFileError = conversationResult?.key === conversationKey ? conversationResult.error : null;
1331
- const conversationFileStatus = containerType !== "conversation" ? "idle" : conversationResult?.key !== conversationKey ? "loading" : conversationResult.loading ? "loading" : conversationFileError ? "error" : "ready";
1400
+ const conversationFileStatus = outgoing || containerType !== "conversation" ? "idle" : conversationResult?.key !== conversationKey ? "loading" : conversationResult.loading ? "loading" : conversationFileError ? "error" : "ready";
1332
1401
  const incomingGeneric = edges.filter(
1333
- (edge) => edge.direction === "incoming" && !(containerType === "conversation" && edge.otherType === "file")
1402
+ (edge) => outgoing ? edge.direction === "outgoing" : edge.direction === "incoming" && !(containerType === "conversation" && edge.otherType === "file")
1334
1403
  ).map((edge) => ({
1335
1404
  edgeId: edge.id,
1336
1405
  resourceId: edge.otherId,
@@ -1339,7 +1408,29 @@ function useContainerLinks(args) {
1339
1408
  label: edge.label ?? null,
1340
1409
  metadata: edge.metadata ?? {}
1341
1410
  }));
1342
- const incoming = containerType === "conversation" ? [...incomingGeneric, ...conversationFiles] : incomingGeneric;
1411
+ const incoming = !outgoing && containerType === "conversation" ? [...incomingGeneric, ...conversationFiles] : incomingGeneric;
1412
+ const endpoints = (token, resourceId) => {
1413
+ if (!containerId) return { error: "Missing container id" };
1414
+ if (outgoing) {
1415
+ if (!isAssociationTargetType(token)) {
1416
+ return {
1417
+ error: `A ${token.replace(/_/g, " ")} cannot be filed under: it is not a container type.`
1418
+ };
1419
+ }
1420
+ return {
1421
+ sourceType: containerType,
1422
+ sourceId: containerId,
1423
+ targetType: token,
1424
+ targetId: resourceId
1425
+ };
1426
+ }
1427
+ return {
1428
+ sourceType: token,
1429
+ sourceId: resourceId,
1430
+ targetType: containerType,
1431
+ targetId: containerId
1432
+ };
1433
+ };
1343
1434
  const linksFor = (token) => incoming.filter((link) => link.token === token);
1344
1435
  const countFor = (token) => incoming.reduce((n, link) => link.token === token ? n + 1 : n, 0);
1345
1436
  const attachedIdsFor = (token) => {
@@ -1350,39 +1441,36 @@ function useContainerLinks(args) {
1350
1441
  return set;
1351
1442
  };
1352
1443
  const attach = async (token, resourceId, label, metadata, options) => {
1353
- if (!containerId) return { ok: false, error: "Missing container id" };
1444
+ const ends = endpoints(token, resourceId);
1445
+ if ("error" in ends) return { ok: false, error: ends.error };
1354
1446
  const result = await store.add({
1355
- sourceType: token,
1356
- sourceId: resourceId,
1357
- targetType: containerType,
1358
- targetId: containerId,
1447
+ ...ends,
1359
1448
  ...orgId != null ? { orgId } : {},
1360
1449
  ...label !== void 0 ? { label } : {},
1361
1450
  ...metadata !== void 0 ? { metadata } : {},
1362
1451
  ...options?.role !== void 0 ? { role: options.role } : {},
1363
1452
  ...options?.replaceMetadata !== void 0 ? { replaceMetadata: options.replaceMetadata } : {}
1364
1453
  });
1365
- if (result.ok && containerType === "conversation" && token === "file") {
1454
+ if (result.ok && !outgoing && containerType === "conversation" && token === "file") {
1366
1455
  await loadConversationFiles();
1367
1456
  }
1368
1457
  return result;
1369
1458
  };
1370
1459
  const detach = async (token, resourceId, role) => {
1371
- if (!containerId) return { ok: false, error: "Missing container id" };
1460
+ const ends = endpoints(token, resourceId);
1461
+ if ("error" in ends) return { ok: false, error: ends.error };
1372
1462
  const result = await store.remove({
1373
- sourceType: token,
1374
- sourceId: resourceId,
1375
- targetType: containerType,
1376
- targetId: containerId,
1463
+ ...ends,
1377
1464
  ...role !== void 0 ? { role } : {}
1378
1465
  });
1379
- if (result.ok && containerType === "conversation" && token === "file") {
1466
+ if (result.ok && !outgoing && containerType === "conversation" && token === "file") {
1380
1467
  await loadConversationFiles();
1381
1468
  }
1382
1469
  return result;
1383
1470
  };
1384
1471
  const setPinned = async (token, resourceId, pinned, role) => {
1385
- if (!containerId) return { ok: false, error: "Missing container id" };
1472
+ const ends = endpoints(token, resourceId);
1473
+ if ("error" in ends) return { ok: false, error: ends.error };
1386
1474
  const exactRole = role ?? null;
1387
1475
  const link = incoming.find(
1388
1476
  (candidate) => candidate.token === token && candidate.resourceId === resourceId && candidate.role === exactRole
@@ -1396,10 +1484,7 @@ function useContainerLinks(args) {
1396
1484
  };
1397
1485
  }
1398
1486
  return store.add({
1399
- sourceType: token,
1400
- sourceId: resourceId,
1401
- targetType: containerType,
1402
- targetId: containerId,
1487
+ ...ends,
1403
1488
  ...orgId != null ? { orgId } : {},
1404
1489
  ...link.label != null ? { label: link.label } : {},
1405
1490
  ...role !== void 0 ? { role } : {},
@@ -1409,10 +1494,10 @@ function useContainerLinks(args) {
1409
1494
  const reload = async () => {
1410
1495
  await Promise.all([
1411
1496
  reloadGeneric(),
1412
- containerType === "conversation" ? loadConversationFiles() : Promise.resolve()
1497
+ !outgoing && containerType === "conversation" ? loadConversationFiles() : Promise.resolve()
1413
1498
  ]);
1414
1499
  };
1415
- const status = !containerId ? genericStatus : conversationFileStatus === "error" ? "error" : containerType === "conversation" && (conversationFileStatus === "idle" || conversationFileStatus === "loading") ? "loading" : genericStatus;
1500
+ const status = !containerId ? genericStatus : conversationFileStatus === "error" ? "error" : !outgoing && containerType === "conversation" && (conversationFileStatus === "idle" || conversationFileStatus === "loading") ? "loading" : genericStatus;
1416
1501
  const error = conversationFileError ?? genericError;
1417
1502
  return {
1418
1503
  status,
@@ -1427,6 +1512,21 @@ function useContainerLinks(args) {
1427
1512
  setPinned
1428
1513
  };
1429
1514
  }
1515
+ function containerLinksArgs(container) {
1516
+ if (container?.direction === "outgoing") {
1517
+ return {
1518
+ containerType: container.type,
1519
+ containerId: container.id,
1520
+ orgId: container.orgId ?? null,
1521
+ direction: "outgoing"
1522
+ };
1523
+ }
1524
+ return {
1525
+ containerType: container?.type ?? "organization",
1526
+ containerId: container?.id ?? null,
1527
+ orgId: container?.orgId ?? null
1528
+ };
1529
+ }
1430
1530
 
1431
1531
  // src/react/hooks/useEntityTitles.ts
1432
1532
  var import_react4 = require("react");
@@ -2261,7 +2361,7 @@ function AssociationPicker(props) {
2261
2361
  const Override = pickerOverrides?.[props.token];
2262
2362
  if (Override) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Override, { ...props });
2263
2363
  const title = `Add ${info.labelPlural}`;
2264
- const subtitle = props.containerLabel ? `Attach to ${props.containerLabel}` : "Click an item to attach or detach it";
2364
+ const subtitle = props.filing ? `File ${props.containerLabel ?? "this"} under a ${info.label.toLowerCase()}` : props.containerLabel ? `Attach to ${props.containerLabel}` : "Click an item to attach or detach it";
2265
2365
  if (!props.open) return null;
2266
2366
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2267
2367
  AssociationWindow,
@@ -2878,8 +2978,9 @@ function AttachedItemsSheet(props) {
2878
2978
  const Icon = info.Icon ?? DefaultEntityIcon;
2879
2979
  const title = info.labelPlural;
2880
2980
  const containerName = props.container?.label ?? props.containerLabel;
2981
+ const filing = props.container?.direction === "outgoing";
2881
2982
  const subtitle = props.container ? /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "inline-flex min-w-0 max-w-full items-center gap-1", children: [
2882
- "Attached to",
2983
+ filing ? "What this is filed under:" : "Attached to",
2883
2984
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2884
2985
  DoorRef,
2885
2986
  {
@@ -3155,11 +3256,7 @@ function AssociationCard({
3155
3256
  attach,
3156
3257
  detach,
3157
3258
  setPinned
3158
- } = useContainerLinks({
3159
- containerType: container?.type ?? "organization",
3160
- containerId: container?.id ?? null,
3161
- orgId: container?.orgId ?? null
3162
- });
3259
+ } = useContainerLinks(containerLinksArgs(container));
3163
3260
  if (!container) {
3164
3261
  store.errorSink({
3165
3262
  code: "association_card_no_container",
@@ -3170,7 +3267,8 @@ function AssociationCard({
3170
3267
  }
3171
3268
  const count = countFor(token);
3172
3269
  const loading = status === "loading" || status === "idle";
3173
- const canAttach = info.canListCandidates;
3270
+ const filing = container.direction === "outgoing";
3271
+ const canAttach = info.canListCandidates && (!filing || isAssociationTargetType(token));
3174
3272
  const canDrillIn = count > 0;
3175
3273
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
3176
3274
  /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
@@ -3218,7 +3316,7 @@ function AssociationCard({
3218
3316
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "block text-[11px] text-muted-foreground tabular-nums", children: loading ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "inline-flex items-center gap-1", children: [
3219
3317
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(SpinnerIcon, { className: "h-3 w-3 animate-spin" }),
3220
3318
  "Loading\u2026"
3221
- ] }) : `${count} attached` })
3319
+ ] }) : `${count} ${container.direction === "outgoing" ? "filed under" : "attached"}` })
3222
3320
  ] })
3223
3321
  ]
3224
3322
  }
@@ -3264,6 +3362,7 @@ function AssociationCard({
3264
3362
  token,
3265
3363
  ...container.label !== void 0 ? { containerLabel: container.label } : {},
3266
3364
  orgId: container.orgId ?? null,
3365
+ ...filing ? { filing: true } : {},
3267
3366
  attachedIds: attachedIdsFor(token),
3268
3367
  onAttach: (resourceId, title) => attach(token, resourceId, title),
3269
3368
  onDetach: (resourceId) => detach(token, resourceId)
@@ -3281,9 +3380,11 @@ function AssociationCardGrid({
3281
3380
  className
3282
3381
  }) {
3283
3382
  const store = useAssociationsStore();
3383
+ const primary = usePrimaryEntity();
3284
3384
  const excluded = new Set(excludeTokens ?? []);
3385
+ const filing = primary?.direction === "outgoing";
3285
3386
  const list = (tokens ?? store.registry.curatedTokens()).filter(
3286
- (token) => !excluded.has(token)
3387
+ (token) => !excluded.has(token) && (!filing || isAssociationTargetType(token))
3287
3388
  );
3288
3389
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: (0, import_design_system7.cn)("space-y-5", className), children: CONTENT_ROLES.map((role) => {
3289
3390
  const inRole = list.filter(
@@ -3317,11 +3418,7 @@ function metadataIsPinned2(metadata) {
3317
3418
  }
3318
3419
  function useContainerLinksAdapter(container, tokens) {
3319
3420
  const store = useAssociationsStore();
3320
- const links = useContainerLinks({
3321
- containerType: container?.type ?? "organization",
3322
- containerId: container?.id ?? null,
3323
- orgId: container?.orgId ?? null
3324
- });
3421
+ const links = useContainerLinks(containerLinksArgs(container));
3325
3422
  const rows = container ? (tokens ?? store.registry.curatedTokens()).flatMap(
3326
3423
  (token) => links.linksFor(token).map((l) => ({
3327
3424
  key: l.edgeId,