@ai-matrx/associations 0.4.0 → 0.5.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.
@@ -1,4 +1,4 @@
1
- import { EntityOverlayMap, EntityTypeToken, EntityOverlayEntry, ErrorSink, AssociationsRpcError, AssociationsDataSource, AssociationsIdentity, AssociationTargetType, AssociationsRpcResult, AssociationEdge, AssociationTargetEdge, AssociationSourceEdge, CategoryDimension, PlatformCategory, UserStateKind, UserEntityState, AssociationsEntry, CategoriesEntry, AssociationsConfig } from '../index.cjs';
1
+ import { EntityOverlayMap, EntityTypeToken, EntityOverlayEntry, ErrorSink, AssociationsRpcError, AssociationsDataSource, AssociationsIdentity, AssociationTargetType, AssociationsRpcResult, AssociationEdge, AssociationTargetEdge, AssociationSourceEdge, CategoryDimension, PlatformCategory, UserStateKind, UserEntityState, PlatformComment, AssociationsEntry, CategoriesEntry, CommentsEntry, AssociationsConfig } from '../index.cjs';
2
2
  import 'react';
3
3
 
4
4
  /** The universal ownership column post-2026-reorg. */
@@ -474,6 +474,28 @@ interface AssociationHelpersApi {
474
474
  }
475
475
  declare function createAssociationHelpers(service: AssociationsServiceApi): AssociationHelpersApi;
476
476
 
477
+ interface AddCommentArgs {
478
+ /** Registered entity-type token the comment hangs on (writes-strict, C18). */
479
+ entityType: string;
480
+ entityId: string;
481
+ body: string;
482
+ /** Reply target — an existing comment's id. Top-level when omitted. */
483
+ parentId?: string | null;
484
+ /** Org override; the RPC resolves task-org/personal-org when omitted. */
485
+ orgId?: string | null;
486
+ }
487
+ interface CommentsServiceApi {
488
+ listForEntity(entityType: string, entityId: string): Promise<AssociationsRpcResult<{
489
+ comments: PlatformComment[];
490
+ }>>;
491
+ add(args: AddCommentArgs): Promise<AssociationsRpcResult<{
492
+ id: string;
493
+ }>>;
494
+ edit(id: string, body: string): Promise<AssociationsRpcResult<null>>;
495
+ remove(id: string): Promise<AssociationsRpcResult<null>>;
496
+ }
497
+ declare function createCommentsService(deps: CoreDeps): CommentsServiceApi;
498
+
477
499
  /** Cache key for one association endpoint. */
478
500
  declare function associationsKey(type: string, id: string): string;
479
501
  interface AssociationWriteResult {
@@ -488,6 +510,12 @@ interface CategoryMutationResult {
488
510
  id?: string;
489
511
  error?: string;
490
512
  }
513
+ interface CommentMutationResult {
514
+ ok: boolean;
515
+ /** Set on a successful `addComment`. */
516
+ id?: string;
517
+ error?: string;
518
+ }
491
519
  interface AssociationsStore {
492
520
  /** Stable, sync read of one endpoint's entry (idle default when uncached). */
493
521
  getEdges(type: string, id: string): AssociationsEntry;
@@ -531,7 +559,7 @@ interface AssociationsStore {
531
559
  }): Promise<AssociationWriteResult>;
532
560
  /** Reset one endpoint to idle and notify its subscribers. */
533
561
  invalidate(type: string, id: string): void;
534
- /** Reset every association endpoint AND every category facet. */
562
+ /** Reset every association endpoint, category facet, and comment thread. */
535
563
  invalidateAll(): void;
536
564
  getCategories(dimension: CategoryDimension): CategoriesEntry;
537
565
  loadCategories(dimension: CategoryDimension, opts?: {
@@ -566,6 +594,41 @@ interface AssociationsStore {
566
594
  dimension: CategoryDimension;
567
595
  id: string;
568
596
  }): Promise<CategoryMutationResult>;
597
+ /** Stable, sync read of one entity's comment thread (idle when uncached). */
598
+ getComments(type: string, id: string): CommentsEntry;
599
+ /**
600
+ * Lazy load of every comment on `${type}:${id}` (oldest→newest; thread by
601
+ * `parentId`). Deduped per key; `status === "ready"` short-circuits unless
602
+ * `force`.
603
+ */
604
+ loadComments(type: string, id: string, opts?: {
605
+ force?: boolean;
606
+ }): Promise<void>;
607
+ /** External-store contract for one entity's comment thread. */
608
+ subscribeComments(key: string, cb: () => void): () => void;
609
+ /** Post a comment (or a reply); on success force-reloads the thread. */
610
+ addComment(args: {
611
+ entityType: string;
612
+ entityId: string;
613
+ body: string;
614
+ parentId?: string | null;
615
+ orgId?: string | null;
616
+ }): Promise<CommentMutationResult>;
617
+ /** Edit a comment body (author-only in the RPC); reloads the thread. */
618
+ editComment(args: {
619
+ entityType: string;
620
+ entityId: string;
621
+ id: string;
622
+ body: string;
623
+ }): Promise<CommentMutationResult>;
624
+ /** Soft-delete a comment (author/org member in the RPC); reloads the thread. */
625
+ deleteComment(args: {
626
+ entityType: string;
627
+ entityId: string;
628
+ id: string;
629
+ }): Promise<CommentMutationResult>;
630
+ /** Reset one entity's comment thread to idle and notify its subscribers. */
631
+ invalidateComments(type: string, id: string): void;
569
632
  /** Batched title resolution + the session cache + prime. */
570
633
  titles: TitlesServiceApi;
571
634
  /** Per-user favorite/pinned/hidden state (`ues_*`). */
@@ -586,6 +649,7 @@ interface AssociationsStore {
586
649
  services: {
587
650
  associations: AssociationsServiceApi;
588
651
  categories: CategoriesServiceApi;
652
+ comments: CommentsServiceApi;
589
653
  };
590
654
  /** Merge more host overlay entries in (icons/routes/candidate loaders). */
591
655
  registerEntityOverlay: EntityRegistry["registerEntityOverlay"];
@@ -676,4 +740,4 @@ interface CategoryHierarchy {
676
740
  */
677
741
  declare function buildCategoryHierarchy(categories: PlatformCategory[]): CategoryHierarchy;
678
742
 
679
- export { type AddAssociationArgs, type AssertDemandedSchemaOptions, type AssociationGuards, type AssociationHelpersApi, type AssociationWriteResult, type AssociationsServiceApi, type AssociationsStore, type CandidateRecord, type CandidatesResult, type CandidatesServiceApi, type CategoriesServiceApi, type CategoryHierarchy, type CategoryHierarchyItem, type CategoryMutationResult, type ContainerRef, type ContentRole, type ConversationFileLink, type CoreDeps, type CreateEntityRowArgs, DEFAULT_ORG_COLUMN, DEFAULT_OWNER_COLUMN, type DemandedSchemaReport, type EdgeAttrs, type EdgeSpec, type EntityInfo, type EntityRef, type EntityRegistry, type EntityRowResult, type EntityRowsServiceApi, type FavoritesServiceApi, type ListCandidatesArgs, NON_CONTENT_SOURCE_TYPES, type PgErrorMapper, type RpcResultHelpers, SELF_TEST_MISSING_RPC, type SearchAcrossTokensArgs, type TitlesServiceApi, type UniversalCandidate, assertDemandedSchema, associationsKey, buildCategoryHierarchy, createAssociationGuards, createAssociationHelpers, createAssociationsService, createAssociationsStore, createCandidatesService, createCategoriesService, createEntityRegistry, createEntityRowsService, createFavoritesService, createRpcResultHelpers, createTitlesService, entityTitleCacheKey, err, firstError, isContentRole, isContentSourceEdge, isMembershipMetadata, isTransportFailure, isUuid, ok, resolveEntityToken };
743
+ export { type AddAssociationArgs, type AddCommentArgs, type AssertDemandedSchemaOptions, type AssociationGuards, type AssociationHelpersApi, type AssociationWriteResult, type AssociationsServiceApi, type AssociationsStore, type CandidateRecord, type CandidatesResult, type CandidatesServiceApi, type CategoriesServiceApi, type CategoryHierarchy, type CategoryHierarchyItem, type CategoryMutationResult, type CommentMutationResult, type CommentsServiceApi, type ContainerRef, type ContentRole, type ConversationFileLink, type CoreDeps, type CreateEntityRowArgs, DEFAULT_ORG_COLUMN, DEFAULT_OWNER_COLUMN, type DemandedSchemaReport, type EdgeAttrs, type EdgeSpec, type EntityInfo, type EntityRef, type EntityRegistry, type EntityRowResult, type EntityRowsServiceApi, type FavoritesServiceApi, type ListCandidatesArgs, NON_CONTENT_SOURCE_TYPES, type PgErrorMapper, type RpcResultHelpers, SELF_TEST_MISSING_RPC, type SearchAcrossTokensArgs, type TitlesServiceApi, type UniversalCandidate, assertDemandedSchema, associationsKey, buildCategoryHierarchy, createAssociationGuards, createAssociationHelpers, createAssociationsService, createAssociationsStore, createCandidatesService, createCategoriesService, createCommentsService, createEntityRegistry, createEntityRowsService, createFavoritesService, createRpcResultHelpers, createTitlesService, entityTitleCacheKey, err, firstError, isContentRole, isContentSourceEdge, isMembershipMetadata, isTransportFailure, isUuid, ok, resolveEntityToken };
@@ -1,4 +1,4 @@
1
- import { EntityOverlayMap, EntityTypeToken, EntityOverlayEntry, ErrorSink, AssociationsRpcError, AssociationsDataSource, AssociationsIdentity, AssociationTargetType, AssociationsRpcResult, AssociationEdge, AssociationTargetEdge, AssociationSourceEdge, CategoryDimension, PlatformCategory, UserStateKind, UserEntityState, AssociationsEntry, CategoriesEntry, AssociationsConfig } from '../index.js';
1
+ import { EntityOverlayMap, EntityTypeToken, EntityOverlayEntry, ErrorSink, AssociationsRpcError, AssociationsDataSource, AssociationsIdentity, AssociationTargetType, AssociationsRpcResult, AssociationEdge, AssociationTargetEdge, AssociationSourceEdge, CategoryDimension, PlatformCategory, UserStateKind, UserEntityState, PlatformComment, AssociationsEntry, CategoriesEntry, CommentsEntry, AssociationsConfig } from '../index.js';
2
2
  import 'react';
3
3
 
4
4
  /** The universal ownership column post-2026-reorg. */
@@ -474,6 +474,28 @@ interface AssociationHelpersApi {
474
474
  }
475
475
  declare function createAssociationHelpers(service: AssociationsServiceApi): AssociationHelpersApi;
476
476
 
477
+ interface AddCommentArgs {
478
+ /** Registered entity-type token the comment hangs on (writes-strict, C18). */
479
+ entityType: string;
480
+ entityId: string;
481
+ body: string;
482
+ /** Reply target — an existing comment's id. Top-level when omitted. */
483
+ parentId?: string | null;
484
+ /** Org override; the RPC resolves task-org/personal-org when omitted. */
485
+ orgId?: string | null;
486
+ }
487
+ interface CommentsServiceApi {
488
+ listForEntity(entityType: string, entityId: string): Promise<AssociationsRpcResult<{
489
+ comments: PlatformComment[];
490
+ }>>;
491
+ add(args: AddCommentArgs): Promise<AssociationsRpcResult<{
492
+ id: string;
493
+ }>>;
494
+ edit(id: string, body: string): Promise<AssociationsRpcResult<null>>;
495
+ remove(id: string): Promise<AssociationsRpcResult<null>>;
496
+ }
497
+ declare function createCommentsService(deps: CoreDeps): CommentsServiceApi;
498
+
477
499
  /** Cache key for one association endpoint. */
478
500
  declare function associationsKey(type: string, id: string): string;
479
501
  interface AssociationWriteResult {
@@ -488,6 +510,12 @@ interface CategoryMutationResult {
488
510
  id?: string;
489
511
  error?: string;
490
512
  }
513
+ interface CommentMutationResult {
514
+ ok: boolean;
515
+ /** Set on a successful `addComment`. */
516
+ id?: string;
517
+ error?: string;
518
+ }
491
519
  interface AssociationsStore {
492
520
  /** Stable, sync read of one endpoint's entry (idle default when uncached). */
493
521
  getEdges(type: string, id: string): AssociationsEntry;
@@ -531,7 +559,7 @@ interface AssociationsStore {
531
559
  }): Promise<AssociationWriteResult>;
532
560
  /** Reset one endpoint to idle and notify its subscribers. */
533
561
  invalidate(type: string, id: string): void;
534
- /** Reset every association endpoint AND every category facet. */
562
+ /** Reset every association endpoint, category facet, and comment thread. */
535
563
  invalidateAll(): void;
536
564
  getCategories(dimension: CategoryDimension): CategoriesEntry;
537
565
  loadCategories(dimension: CategoryDimension, opts?: {
@@ -566,6 +594,41 @@ interface AssociationsStore {
566
594
  dimension: CategoryDimension;
567
595
  id: string;
568
596
  }): Promise<CategoryMutationResult>;
597
+ /** Stable, sync read of one entity's comment thread (idle when uncached). */
598
+ getComments(type: string, id: string): CommentsEntry;
599
+ /**
600
+ * Lazy load of every comment on `${type}:${id}` (oldest→newest; thread by
601
+ * `parentId`). Deduped per key; `status === "ready"` short-circuits unless
602
+ * `force`.
603
+ */
604
+ loadComments(type: string, id: string, opts?: {
605
+ force?: boolean;
606
+ }): Promise<void>;
607
+ /** External-store contract for one entity's comment thread. */
608
+ subscribeComments(key: string, cb: () => void): () => void;
609
+ /** Post a comment (or a reply); on success force-reloads the thread. */
610
+ addComment(args: {
611
+ entityType: string;
612
+ entityId: string;
613
+ body: string;
614
+ parentId?: string | null;
615
+ orgId?: string | null;
616
+ }): Promise<CommentMutationResult>;
617
+ /** Edit a comment body (author-only in the RPC); reloads the thread. */
618
+ editComment(args: {
619
+ entityType: string;
620
+ entityId: string;
621
+ id: string;
622
+ body: string;
623
+ }): Promise<CommentMutationResult>;
624
+ /** Soft-delete a comment (author/org member in the RPC); reloads the thread. */
625
+ deleteComment(args: {
626
+ entityType: string;
627
+ entityId: string;
628
+ id: string;
629
+ }): Promise<CommentMutationResult>;
630
+ /** Reset one entity's comment thread to idle and notify its subscribers. */
631
+ invalidateComments(type: string, id: string): void;
569
632
  /** Batched title resolution + the session cache + prime. */
570
633
  titles: TitlesServiceApi;
571
634
  /** Per-user favorite/pinned/hidden state (`ues_*`). */
@@ -586,6 +649,7 @@ interface AssociationsStore {
586
649
  services: {
587
650
  associations: AssociationsServiceApi;
588
651
  categories: CategoriesServiceApi;
652
+ comments: CommentsServiceApi;
589
653
  };
590
654
  /** Merge more host overlay entries in (icons/routes/candidate loaders). */
591
655
  registerEntityOverlay: EntityRegistry["registerEntityOverlay"];
@@ -676,4 +740,4 @@ interface CategoryHierarchy {
676
740
  */
677
741
  declare function buildCategoryHierarchy(categories: PlatformCategory[]): CategoryHierarchy;
678
742
 
679
- export { type AddAssociationArgs, type AssertDemandedSchemaOptions, type AssociationGuards, type AssociationHelpersApi, type AssociationWriteResult, type AssociationsServiceApi, type AssociationsStore, type CandidateRecord, type CandidatesResult, type CandidatesServiceApi, type CategoriesServiceApi, type CategoryHierarchy, type CategoryHierarchyItem, type CategoryMutationResult, type ContainerRef, type ContentRole, type ConversationFileLink, type CoreDeps, type CreateEntityRowArgs, DEFAULT_ORG_COLUMN, DEFAULT_OWNER_COLUMN, type DemandedSchemaReport, type EdgeAttrs, type EdgeSpec, type EntityInfo, type EntityRef, type EntityRegistry, type EntityRowResult, type EntityRowsServiceApi, type FavoritesServiceApi, type ListCandidatesArgs, NON_CONTENT_SOURCE_TYPES, type PgErrorMapper, type RpcResultHelpers, SELF_TEST_MISSING_RPC, type SearchAcrossTokensArgs, type TitlesServiceApi, type UniversalCandidate, assertDemandedSchema, associationsKey, buildCategoryHierarchy, createAssociationGuards, createAssociationHelpers, createAssociationsService, createAssociationsStore, createCandidatesService, createCategoriesService, createEntityRegistry, createEntityRowsService, createFavoritesService, createRpcResultHelpers, createTitlesService, entityTitleCacheKey, err, firstError, isContentRole, isContentSourceEdge, isMembershipMetadata, isTransportFailure, isUuid, ok, resolveEntityToken };
743
+ export { type AddAssociationArgs, type AddCommentArgs, type AssertDemandedSchemaOptions, type AssociationGuards, type AssociationHelpersApi, type AssociationWriteResult, type AssociationsServiceApi, type AssociationsStore, type CandidateRecord, type CandidatesResult, type CandidatesServiceApi, type CategoriesServiceApi, type CategoryHierarchy, type CategoryHierarchyItem, type CategoryMutationResult, type CommentMutationResult, type CommentsServiceApi, type ContainerRef, type ContentRole, type ConversationFileLink, type CoreDeps, type CreateEntityRowArgs, DEFAULT_ORG_COLUMN, DEFAULT_OWNER_COLUMN, type DemandedSchemaReport, type EdgeAttrs, type EdgeSpec, type EntityInfo, type EntityRef, type EntityRegistry, type EntityRowResult, type EntityRowsServiceApi, type FavoritesServiceApi, type ListCandidatesArgs, NON_CONTENT_SOURCE_TYPES, type PgErrorMapper, type RpcResultHelpers, SELF_TEST_MISSING_RPC, type SearchAcrossTokensArgs, type TitlesServiceApi, type UniversalCandidate, assertDemandedSchema, associationsKey, buildCategoryHierarchy, createAssociationGuards, createAssociationHelpers, createAssociationsService, createAssociationsStore, createCandidatesService, createCategoriesService, createCommentsService, createEntityRegistry, createEntityRowsService, createFavoritesService, createRpcResultHelpers, createTitlesService, entityTitleCacheKey, err, firstError, isContentRole, isContentSourceEdge, isMembershipMetadata, isTransportFailure, isUuid, ok, resolveEntityToken };
@@ -2661,6 +2661,122 @@ function createAssociationHelpers(service) {
2661
2661
  };
2662
2662
  }
2663
2663
 
2664
+ // src/core/commentsService.ts
2665
+ function toComment(row) {
2666
+ return {
2667
+ id: row.id,
2668
+ orgId: row.organization_id ?? null,
2669
+ entityType: row.entity_type,
2670
+ entityId: row.entity_id,
2671
+ parentId: row.parent_id ?? null,
2672
+ body: row.body,
2673
+ createdAt: row.created_at,
2674
+ updatedAt: row.updated_at,
2675
+ createdBy: row.created_by ?? null,
2676
+ author: {
2677
+ email: row.author_email ?? null,
2678
+ displayName: row.author_display_name ?? null,
2679
+ avatarUrl: row.author_avatar_url ?? null
2680
+ }
2681
+ };
2682
+ }
2683
+ function createCommentsService(deps) {
2684
+ const { dataSource, identity, guards } = deps;
2685
+ const { ok: ok2, err: err2, mapPgError, mapPgErrorPair } = deps.rpc;
2686
+ return {
2687
+ /**
2688
+ * All comments on `${entityType}:${entityId}`, org-filtered by RLS
2689
+ * inside the RPC, ordered oldest→newest. Build the thread from each
2690
+ * row's `parentId` (top-level comments have `parentId === null`).
2691
+ */
2692
+ async listForEntity(entityType, entityId) {
2693
+ try {
2694
+ identity.requireUserId();
2695
+ const token = guards.normalizeEntityToken(entityType);
2696
+ const invalid = firstError(
2697
+ guards.checkToken("entityType", token),
2698
+ guards.checkUuid("entityId", entityId)
2699
+ );
2700
+ if (invalid) return { ok: false, error: invalid };
2701
+ const { data, error } = await dataSource.rpc("cmt_list", {
2702
+ p_entity_type: token,
2703
+ p_entity_id: entityId
2704
+ });
2705
+ if (error) return err2(...mapPgErrorPair(error));
2706
+ const rows = Array.isArray(data) ? data : [];
2707
+ return ok2({ comments: rows.map(toComment) });
2708
+ } catch (e) {
2709
+ return { ok: false, error: mapPgError(e) };
2710
+ }
2711
+ },
2712
+ /**
2713
+ * Post `body` on `${entityType}:${entityId}`; pass `parentId` to reply.
2714
+ * Returns the new comment id.
2715
+ */
2716
+ async add(args) {
2717
+ try {
2718
+ identity.requireUserId();
2719
+ const token = guards.normalizeEntityToken(args.entityType);
2720
+ const invalid = firstError(
2721
+ guards.checkToken("entityType", token),
2722
+ guards.checkUuid("entityId", args.entityId),
2723
+ args.parentId != null ? guards.checkUuid("parentId", args.parentId) : null
2724
+ );
2725
+ if (invalid) return { ok: false, error: invalid };
2726
+ if (!args.body || args.body.trim().length === 0) {
2727
+ return err2("invalid_argument", "comment body must be non-empty");
2728
+ }
2729
+ const { data, error } = await dataSource.rpc("cmt_add", {
2730
+ p_entity_type: token,
2731
+ p_entity_id: args.entityId,
2732
+ p_body: args.body,
2733
+ p_parent_id: args.parentId ?? void 0,
2734
+ p_org_id: args.orgId ?? void 0
2735
+ });
2736
+ if (error) return err2(...mapPgErrorPair(error));
2737
+ if (!data || typeof data !== "string") {
2738
+ return err2("internal", "cmt_add returned no comment id");
2739
+ }
2740
+ return ok2({ id: data });
2741
+ } catch (e) {
2742
+ return { ok: false, error: mapPgError(e) };
2743
+ }
2744
+ },
2745
+ /** Replace a comment's body. Author-only; the RPC rejects everyone else. */
2746
+ async edit(id, body) {
2747
+ try {
2748
+ identity.requireUserId();
2749
+ const invalid = guards.checkUuid("commentId", id);
2750
+ if (invalid) return { ok: false, error: invalid };
2751
+ if (!body || body.trim().length === 0) {
2752
+ return err2("invalid_argument", "comment body must be non-empty");
2753
+ }
2754
+ const { error } = await dataSource.rpc("cmt_edit", {
2755
+ p_id: id,
2756
+ p_body: body
2757
+ });
2758
+ if (error) return err2(...mapPgErrorPair(error));
2759
+ return ok2(null);
2760
+ } catch (e) {
2761
+ return { ok: false, error: mapPgError(e) };
2762
+ }
2763
+ },
2764
+ /** Soft-delete a comment. Author or org member; enforced server-side. */
2765
+ async remove(id) {
2766
+ try {
2767
+ identity.requireUserId();
2768
+ const invalid = guards.checkUuid("commentId", id);
2769
+ if (invalid) return { ok: false, error: invalid };
2770
+ const { error } = await dataSource.rpc("cmt_delete", { p_id: id });
2771
+ if (error) return err2(...mapPgErrorPair(error));
2772
+ return ok2(null);
2773
+ } catch (e) {
2774
+ return { ok: false, error: mapPgError(e) };
2775
+ }
2776
+ }
2777
+ };
2778
+ }
2779
+
2664
2780
  // src/core/store.ts
2665
2781
  function associationsKey(type, id) {
2666
2782
  return `${type}:${id}`;
@@ -2677,6 +2793,12 @@ var IDLE_CATEGORIES = Object.freeze({
2677
2793
  fetchedAt: null,
2678
2794
  error: null
2679
2795
  });
2796
+ var IDLE_COMMENTS = Object.freeze({
2797
+ status: "idle",
2798
+ comments: Object.freeze([]),
2799
+ fetchedAt: null,
2800
+ error: null
2801
+ });
2680
2802
  function createAssociationsStore(config) {
2681
2803
  if (!config || typeof config.dataSource?.rpc !== "function") {
2682
2804
  throw new Error(
@@ -2710,12 +2832,16 @@ function createAssociationsStore(config) {
2710
2832
  const titles = createTitlesService(deps, candidates);
2711
2833
  const entityRows = createEntityRowsService(deps, titles);
2712
2834
  const helpers = createAssociationHelpers(associations);
2835
+ const comments = createCommentsService(deps);
2713
2836
  const assocByKey = /* @__PURE__ */ new Map();
2714
2837
  const assocSubs = /* @__PURE__ */ new Map();
2715
2838
  const assocInFlight = /* @__PURE__ */ new Map();
2716
2839
  const catByDim = /* @__PURE__ */ new Map();
2717
2840
  const catSubs = /* @__PURE__ */ new Map();
2718
2841
  const catInFlight = /* @__PURE__ */ new Map();
2842
+ const cmtByKey = /* @__PURE__ */ new Map();
2843
+ const cmtSubs = /* @__PURE__ */ new Map();
2844
+ const cmtInFlight = /* @__PURE__ */ new Map();
2719
2845
  function notifyAssoc(key) {
2720
2846
  const subs = assocSubs.get(key);
2721
2847
  if (subs) for (const cb of [...subs]) cb();
@@ -2732,6 +2858,14 @@ function createAssociationsStore(config) {
2732
2858
  catByDim.set(dimension, entry);
2733
2859
  notifyCat(dimension);
2734
2860
  }
2861
+ function notifyCmt(key) {
2862
+ const subs = cmtSubs.get(key);
2863
+ if (subs) for (const cb of [...subs]) cb();
2864
+ }
2865
+ function setCmt(key, entry) {
2866
+ cmtByKey.set(key, entry);
2867
+ notifyCmt(key);
2868
+ }
2735
2869
  async function load(type, id, opts = {}) {
2736
2870
  const force = opts.force ?? false;
2737
2871
  if (!type || !id) return;
@@ -2821,6 +2955,49 @@ function createAssociationsStore(config) {
2821
2955
  catInFlight.set(dimension, promise);
2822
2956
  return promise;
2823
2957
  }
2958
+ async function loadComments(type, id, opts = {}) {
2959
+ const force = opts.force ?? false;
2960
+ if (!type || !id) return;
2961
+ const key = associationsKey(type, id);
2962
+ const entry = cmtByKey.get(key);
2963
+ if (!force && entry?.status === "ready") return;
2964
+ const pending = cmtInFlight.get(key);
2965
+ if (!force && entry?.status === "loading" && pending) return pending;
2966
+ const prev = cmtByKey.get(key);
2967
+ setCmt(key, {
2968
+ status: "loading",
2969
+ comments: prev?.comments ?? [],
2970
+ fetchedAt: prev?.fetchedAt ?? null,
2971
+ // Retain the visible failure while a retry is pending (same contract
2972
+ // as the association cache).
2973
+ error: prev?.error ?? null
2974
+ });
2975
+ const promise = (async () => {
2976
+ try {
2977
+ const res = await comments.listForEntity(type, id);
2978
+ if (isAssociationsRpcErr(res)) {
2979
+ const before = cmtByKey.get(key);
2980
+ setCmt(key, {
2981
+ status: "error",
2982
+ comments: before?.comments ?? [],
2983
+ fetchedAt: before?.fetchedAt ?? null,
2984
+ error: res.error.message
2985
+ });
2986
+ } else {
2987
+ setCmt(key, {
2988
+ status: "ready",
2989
+ comments: res.data.comments,
2990
+ fetchedAt: Date.now(),
2991
+ error: null
2992
+ });
2993
+ }
2994
+ } finally {
2995
+ cmtInFlight.delete(key);
2996
+ }
2997
+ })();
2998
+ cmtInFlight.set(key, promise);
2999
+ return promise;
3000
+ }
2824
3001
  return {
2825
3002
  // ── associations ─────────────────────────────────────────────────────
2826
3003
  getEdges(type, id) {
@@ -2891,6 +3068,9 @@ function createAssociationsStore(config) {
2891
3068
  const dims = [...catByDim.keys()];
2892
3069
  catByDim.clear();
2893
3070
  for (const dim of dims) notifyCat(dim);
3071
+ const cmtKeys = [...cmtByKey.keys()];
3072
+ cmtByKey.clear();
3073
+ for (const key of cmtKeys) notifyCmt(key);
2894
3074
  },
2895
3075
  // ── categories ───────────────────────────────────────────────────────
2896
3076
  getCategories(dimension) {
@@ -2965,6 +3145,52 @@ function createAssociationsStore(config) {
2965
3145
  await loadCategories(args.dimension, { force: true });
2966
3146
  return { ok: true, id: res.data.id };
2967
3147
  },
3148
+ // ── comments (W6) ────────────────────────────────────────────────────
3149
+ getComments(type, id) {
3150
+ if (!type || !id) return IDLE_COMMENTS;
3151
+ return cmtByKey.get(associationsKey(type, id)) ?? IDLE_COMMENTS;
3152
+ },
3153
+ loadComments,
3154
+ subscribeComments(key, cb) {
3155
+ let subs = cmtSubs.get(key);
3156
+ if (!subs) {
3157
+ subs = /* @__PURE__ */ new Set();
3158
+ cmtSubs.set(key, subs);
3159
+ }
3160
+ subs.add(cb);
3161
+ return () => {
3162
+ subs.delete(cb);
3163
+ if (subs.size === 0) cmtSubs.delete(key);
3164
+ };
3165
+ },
3166
+ async addComment(args) {
3167
+ const res = await comments.add(args);
3168
+ if (isAssociationsRpcErr(res)) {
3169
+ return { ok: false, error: res.error.message };
3170
+ }
3171
+ await loadComments(args.entityType, args.entityId, { force: true });
3172
+ return { ok: true, id: res.data.id };
3173
+ },
3174
+ async editComment(args) {
3175
+ const res = await comments.edit(args.id, args.body);
3176
+ if (isAssociationsRpcErr(res)) {
3177
+ return { ok: false, error: res.error.message };
3178
+ }
3179
+ await loadComments(args.entityType, args.entityId, { force: true });
3180
+ return { ok: true, id: args.id };
3181
+ },
3182
+ async deleteComment(args) {
3183
+ const res = await comments.remove(args.id);
3184
+ if (isAssociationsRpcErr(res)) {
3185
+ return { ok: false, error: res.error.message };
3186
+ }
3187
+ await loadComments(args.entityType, args.entityId, { force: true });
3188
+ return { ok: true, id: args.id };
3189
+ },
3190
+ invalidateComments(type, id) {
3191
+ const key = associationsKey(type, id);
3192
+ if (cmtByKey.delete(key)) notifyCmt(key);
3193
+ },
2968
3194
  // ── seams ────────────────────────────────────────────────────────────
2969
3195
  titles,
2970
3196
  favorites,
@@ -2972,7 +3198,7 @@ function createAssociationsStore(config) {
2972
3198
  entityRows,
2973
3199
  helpers,
2974
3200
  registry,
2975
- services: { associations, categories },
3201
+ services: { associations, categories, comments },
2976
3202
  registerEntityOverlay: registry.registerEntityOverlay,
2977
3203
  errorSink,
2978
3204
  identity: config.identity
@@ -3003,7 +3229,11 @@ var DEMANDED_RPC_NAMES = [
3003
3229
  "ues_list",
3004
3230
  "ues_get_bulk",
3005
3231
  "ues_touch",
3006
- "reference_search_candidates"
3232
+ "reference_search_candidates",
3233
+ "cmt_list",
3234
+ "cmt_add",
3235
+ "cmt_edit",
3236
+ "cmt_delete"
3007
3237
  ];
3008
3238
 
3009
3239
  // src/core/assertDemandedSchema.ts
@@ -3068,7 +3298,17 @@ var PROBE_ARGS = {
3068
3298
  ues_get_bulk: { p_entity_type: "__probe__", p_entity_ids: [BAD_UUID] },
3069
3299
  ues_touch: { p_entity_type: "__probe__", p_entity_id: BAD_UUID },
3070
3300
  // candidates
3071
- reference_search_candidates: { p_token: "__probe__", p_limit: 1 }
3301
+ reference_search_candidates: { p_token: "__probe__", p_limit: 1 },
3302
+ // comments (W6) — every fn takes a uuid arg, so the unparseable sentinel
3303
+ // guarantees 22P02 before any SECURITY DEFINER body runs (no write occurs).
3304
+ cmt_list: { p_entity_type: "__probe__", p_entity_id: BAD_UUID },
3305
+ cmt_add: {
3306
+ p_entity_type: "__probe__",
3307
+ p_entity_id: BAD_UUID,
3308
+ p_body: "__probe__"
3309
+ },
3310
+ cmt_edit: { p_id: BAD_UUID, p_body: "__probe__" },
3311
+ cmt_delete: { p_id: BAD_UUID }
3072
3312
  };
3073
3313
  function isMissingFunctionError(error) {
3074
3314
  if (!error || typeof error !== "object") return false;
@@ -3222,6 +3462,7 @@ export {
3222
3462
  createAssociationsStore,
3223
3463
  createCandidatesService,
3224
3464
  createCategoriesService,
3465
+ createCommentsService,
3225
3466
  createEntityRegistry,
3226
3467
  createEntityRowsService,
3227
3468
  createFavoritesService,