@mastra/mongodb 1.25.0 → 1.26.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,15 +1,16 @@
1
1
  import { v4 } from "@lukeed/uuid";
2
2
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
3
- import { AgentsStorage, BackgroundTasksStorage, BlobStore, DatasetsStorage, ExperimentsStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, MastraCompositeStore, MemoryStorage, NotificationsStorage, ObservabilityStorage, PromptBlocksStorage, RunFenceConflictError, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_KNOWLEDGE_ACTIVITY, TABLE_KNOWLEDGE_MENTIONS, TABLE_KNOWLEDGE_NODES, TABLE_KNOWLEDGE_RECORDS, TABLE_KNOWLEDGE_SEMANTIC_OUTBOX, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, TraceStatus, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, calculatePagination, canonicalizeKnowledgeScope, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, hasErrorCode, isKnowledgeScopeVisible, isRunFenceConflictError, knowledgeScopeKey, knowledgeSemanticDocumentId, knowledgeSemanticIdempotencyKey, listTracesArgsSchema, normalizePerPage, normalizeScheduleTarget, parseDuration, parseKnowledgeNodeCursor, parseKnowledgeWikilinks, resolveListOrderBy, resolveRunFence, safelyParseJSON, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
3
+ import { AgentsStorage, BackgroundTasksStorage, BlobStore, DatasetsStorage, ExperimentsStorage, KnowledgeConflictError, KnowledgeNotFoundError, KnowledgeStorage, MCPClientsStorage, MCPServersStorage, MastraCompositeStore, MemoryStorage, NotificationsStorage, ObservabilityStorage, PromptBlocksStorage, RunFenceConflictError, SchedulesStorage, ScorerDefinitionsStorage, ScoresStorage, SkillsStorage, TABLE_AGENTS, TABLE_AGENT_VERSIONS, TABLE_BACKGROUND_TASKS, TABLE_DATASETS, TABLE_DATASET_ITEMS, TABLE_DATASET_VERSIONS, TABLE_EXPERIMENTS, TABLE_EXPERIMENT_RESULTS, TABLE_MCP_CLIENTS, TABLE_MCP_CLIENT_VERSIONS, TABLE_MCP_SERVERS, TABLE_MCP_SERVER_VERSIONS, TABLE_MEMORY_RUN_FENCES, TABLE_MESSAGES, TABLE_NOTIFICATIONS, TABLE_PROMPT_BLOCKS, TABLE_PROMPT_BLOCK_VERSIONS, TABLE_RESOURCES, TABLE_SCHEDULES, TABLE_SCHEDULE_TRIGGERS, TABLE_SCORERS, TABLE_SCORER_DEFINITIONS, TABLE_SCORER_DEFINITION_VERSIONS, TABLE_SKILLS, TABLE_SKILL_BLOBS, TABLE_SKILL_VERSIONS, TABLE_SPANS, TABLE_THREADS, TABLE_WORKFLOW_DEFINITIONS, TABLE_WORKFLOW_RUN_OWNERS, TABLE_WORKFLOW_SNAPSHOT, TABLE_WORKSPACES, TABLE_WORKSPACE_VERSIONS, TraceStatus, WorkflowDefinitionsStorage, WorkflowsStorage, WorkspacesStorage, calculatePagination, createKnowledgeUlid, createStorageErrorId, createVectorErrorId, ensureDate, hasErrorCode, isRunFenceConflictError, listTracesArgsSchema, normalizePerPage, normalizeScheduleTarget, parseDuration, resolveListOrderBy, resolveRunFence, safelyParseJSON, storageMessageMatchesMetadataFilter, toTraceSpans, transformScoreRow, validateStorageMetadataFilter } from "@mastra/core/storage";
4
4
  import { MastraVector, validateUpsertInput, validateVectorValues } from "@mastra/core/vector";
5
5
  import { MongoBulkWriteError, MongoClient, ObjectId } from "mongodb";
6
6
  import { BaseFilterTranslator } from "@mastra/core/vector/filter";
7
+ import { coreFeatures } from "@mastra/core/features";
7
8
  import { createHash } from "crypto";
8
9
  import { MessageList } from "@mastra/core/agent";
9
10
  import { saveScorePayloadSchema } from "@mastra/core/evals";
10
11
  import { skillSnapshotFieldValuesEqual } from "@mastra/core/storage/domains/skills";
11
12
  // package.json
12
- var version = "1.25.0";
13
+ var version = "1.26.0-alpha.0";
13
14
  // src/vector/filter.ts
14
15
  /**
15
16
  * Translator for MongoDB filter queries.
@@ -4768,845 +4769,1953 @@ var MongoDBExperimentsStorage = class MongoDBExperimentsStorage extends Experime
4768
4769
  } catch {}
4769
4770
  }
4770
4771
  };
4771
- // src/storage/domains/knowledge/index.ts
4772
- let assertDescriptionWithinBound;
4773
- async function assertKnowledgeDescriptionWithinBoundCompat(description) {
4774
- let assertWithinBound = assertDescriptionWithinBound;
4775
- if (!assertWithinBound) {
4776
- const resolvedAssert = (await import("@mastra/core/storage")).assertKnowledgeDescriptionWithinBound ?? ((value) => {
4777
- if (value !== void 0 && value.length > 400) throw new Error("Knowledge node description exceeds the 400 UTF-16 code unit limit");
4772
+ const KNOWLEDGE_TABLE_NAMES = [
4773
+ "mastra_knowledge_nodes",
4774
+ "mastra_knowledge_records",
4775
+ "mastra_knowledge_mentions",
4776
+ "mastra_knowledge_activity",
4777
+ "mastra_knowledge_semantic_outbox",
4778
+ "mastra_knowledge_node_scopes",
4779
+ "mastra_knowledge_record_scopes",
4780
+ "mastra_knowledge_scope_grants",
4781
+ "mastra_knowledge_access_state",
4782
+ "mastra_knowledge_scope_addresses",
4783
+ "mastra_knowledge_node_addresses",
4784
+ "mastra_knowledge_import_state",
4785
+ "mastra_knowledge_import_runs",
4786
+ "mastra_knowledge_proposals"
4787
+ ];
4788
+ /** Knowledge v1 tables with no v2 equivalent; an explicit schema reset drops them. */
4789
+ const RETIRED_KNOWLEDGE_TABLE_NAMES = ["mastra_knowledge_cursors"];
4790
+ const KNOWLEDGE_PAGES_CORE_FEATURE = "knowledge-pages";
4791
+ const KNOWLEDGE_PAGES_TABLE_NAMES = [
4792
+ "mastra_knowledge_pages",
4793
+ "mastra_knowledge_notes",
4794
+ "mastra_knowledge_note_stamps",
4795
+ "mastra_knowledge_mentions",
4796
+ "mastra_knowledge_pins",
4797
+ "mastra_knowledge_activity",
4798
+ "mastra_knowledge_access_state"
4799
+ ];
4800
+ /** Every Knowledge table any schema version created. Reset drops these and nothing else. */
4801
+ const KNOWLEDGE_RESETTABLE_TABLE_NAMES = [.../* @__PURE__ */ new Set([
4802
+ ...KNOWLEDGE_PAGES_TABLE_NAMES,
4803
+ ...KNOWLEDGE_TABLE_NAMES,
4804
+ ...RETIRED_KNOWLEDGE_TABLE_NAMES
4805
+ ])];
4806
+ /** Schema-1/2 tables whose presence means the store needs a reset before schema 4 can run. */
4807
+ const LEGACY_KNOWLEDGE_TABLE_NAMES = new Set([...KNOWLEDGE_TABLE_NAMES, ...RETIRED_KNOWLEDGE_TABLE_NAMES].filter((name) => !KNOWLEDGE_PAGES_TABLE_NAMES.includes(name)));
4808
+ /**
4809
+ * Classifies what an adapter found before creating schema-4 tables. `tableNames` is every
4810
+ * `mastra_knowledge_*` table present; `schemaVersion` is the state row's version, if any.
4811
+ */
4812
+ function inspectKnowledgePagesSchema(snapshot) {
4813
+ const legacy = snapshot.tableNames.filter((name) => LEGACY_KNOWLEDGE_TABLE_NAMES.has(name));
4814
+ if (legacy.length > 0) return {
4815
+ status: "incompatible-reset-required",
4816
+ schemaVersion: snapshot.schemaVersion ?? null,
4817
+ reason: `found Knowledge tables from an earlier schema (${legacy.join(", ")})`
4818
+ };
4819
+ if (snapshot.schemaVersion === 4) return {
4820
+ status: "compatible",
4821
+ schemaVersion: 4
4822
+ };
4823
+ if (snapshot.schemaVersion === void 0) {
4824
+ const present = snapshot.tableNames.filter((name) => KNOWLEDGE_PAGES_TABLE_NAMES.includes(name));
4825
+ if (present.length === 0) return {
4826
+ status: "uninitialized",
4827
+ schemaVersion: null
4828
+ };
4829
+ return {
4830
+ status: "incompatible-reset-required",
4831
+ schemaVersion: null,
4832
+ reason: `found Knowledge tables (${present.join(", ")}) without a schema version`
4833
+ };
4834
+ }
4835
+ return {
4836
+ status: "incompatible-reset-required",
4837
+ schemaVersion: snapshot.schemaVersion,
4838
+ reason: `Knowledge schema version ${snapshot.schemaVersion} is not 4`
4839
+ };
4840
+ }
4841
+ /** Schema-2 table names schema 4 reuses with a different shape. */
4842
+ const REUSED_KNOWLEDGE_TABLE_NAMES = KNOWLEDGE_TABLE_NAMES.filter((name) => KNOWLEDGE_PAGES_TABLE_NAMES.includes(name));
4843
+ /**
4844
+ * The old Knowledge tables an adapter drops on init, rows included: Knowledge before schema 4 was an
4845
+ * experimental store with no migration. Every schema-1/2-only table present is dropped. The table
4846
+ * names schema 4 reuses (mentions, activity, the state table) are dropped too, but only when an old
4847
+ * table is present and no schema-4 state row exists, so they are old-shaped. Schema-4-only tables
4848
+ * are never listed. On a schema-4 store this lists only tables an older process recreated during a
4849
+ * rolling deploy. Drop order is safe for adapters that enforce references.
4850
+ */
4851
+ function legacyKnowledgeTablesToDrop(snapshot) {
4852
+ const present = new Set(snapshot.tableNames);
4853
+ const legacy = [...LEGACY_KNOWLEDGE_TABLE_NAMES].filter((name) => present.has(name));
4854
+ if (legacy.length === 0) return [];
4855
+ const reused = snapshot.schemaVersion === 4 ? [] : REUSED_KNOWLEDGE_TABLE_NAMES.filter((name) => present.has(name));
4856
+ const drop = /* @__PURE__ */ new Set([...legacy, ...reused]);
4857
+ return [...RETIRED_KNOWLEDGE_TABLE_NAMES, ...[...KNOWLEDGE_TABLE_NAMES].reverse()].filter((name) => drop.has(name));
4858
+ }
4859
+ /** Message for the error an adapter throws on `incompatible-reset-required`. */
4860
+ function knowledgePagesResetMessage(reason) {
4861
+ return `Knowledge storage needs a reset before this version can use it: ${reason}. Knowledge data has no migration; run \`await storage.stores?.knowledge?.dangerouslyReset()\` to drop the Knowledge tables (Memory tables are untouched).`;
4862
+ }
4863
+ const KNOWLEDGE_PAGES_CORE_FUNCTIONS = [
4864
+ "assertKnowledgePageDescriptionWithinBound",
4865
+ "assertKnowledgeNoteTextWithinBound",
4866
+ "canonicalKnowledgeName",
4867
+ "knowledgePositionBetween",
4868
+ "KnowledgeMoveTooLargeError",
4869
+ "KnowledgePageDepthError",
4870
+ "KnowledgePageHasChildrenError",
4871
+ "KnowledgePageKeyConflictError"
4872
+ ];
4873
+ const KNOWLEDGE_PAGES_CORE_NUMBERS = [
4874
+ "MAX_KNOWLEDGE_MOVE_PAGES",
4875
+ "MAX_KNOWLEDGE_PAGE_DEPTH",
4876
+ "MAX_KNOWLEDGE_PAGE_LIST_LIMIT"
4877
+ ];
4878
+ function resolveKnowledgePagesCore(storageModule) {
4879
+ const module = typeof storageModule === "object" && storageModule !== null ? storageModule : {};
4880
+ const missing = [...KNOWLEDGE_PAGES_CORE_FUNCTIONS.filter((name) => typeof module[name] !== "function"), ...KNOWLEDGE_PAGES_CORE_NUMBERS.filter((name) => typeof module[name] !== "number")];
4881
+ if (missing.length > 0) throw new Error(`@mastra/core advertises Knowledge pages without the required storage API (missing ${missing.join(", ")})`);
4882
+ return module;
4883
+ }
4884
+ /** Loads core's page-store runtime once, after checking core advertises `knowledge-pages`. */
4885
+ function createKnowledgePagesCoreLoader(features, loadStorageModule) {
4886
+ let core;
4887
+ return async () => {
4888
+ if (!features.has("knowledge-pages")) throw new Error(`Knowledge requires a @mastra/core release with the "${KNOWLEDGE_PAGES_CORE_FEATURE}" feature. Upgrade @mastra/core to use Knowledge; other storage domains keep working on this version.`);
4889
+ core ??= loadStorageModule().then(resolveKnowledgePagesCore).catch((error) => {
4890
+ core = void 0;
4891
+ throw error;
4778
4892
  });
4779
- assertDescriptionWithinBound = resolvedAssert;
4780
- assertWithinBound = resolvedAssert;
4893
+ return core;
4894
+ };
4895
+ }
4896
+ /**
4897
+ * Keyword-search words: the query split on whitespace, lowercased, deduplicated. Every word must
4898
+ * appear (as a case-insensitive substring) in a hit; an empty list means the search returns nothing.
4899
+ */
4900
+ function knowledgeSearchWords(query) {
4901
+ return [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
4902
+ }
4903
+ /**
4904
+ * Why `key` can't be a Knowledge page key, or undefined when it can. Keys are one path segment:
4905
+ * non-empty, with no `/` (path separator), `#` (scope/path separator) or `%` (escape character).
4906
+ */
4907
+ function knowledgePageKeyProblem(key) {
4908
+ if (!key) return "Knowledge page key must not be empty";
4909
+ if (/[/#%]/.test(key)) return `Knowledge page key "${key}" must not contain "/", "#" or "%"`;
4910
+ }
4911
+ // src/storage/domains/knowledge/pages.ts
4912
+ const loadPagesCore = createKnowledgePagesCoreLoader(coreFeatures, () => import("@mastra/core/storage"));
4913
+ const DEFAULT_LIST_LIMIT = 50;
4914
+ const MONGO_DUPLICATE_KEY = 11e3;
4915
+ const MONGO_KNOWLEDGE_PAGES_COLLECTIONS = {
4916
+ pages: "mastra_knowledge_pages",
4917
+ notes: "mastra_knowledge_notes",
4918
+ pins: "mastra_knowledge_pins",
4919
+ activity: "mastra_knowledge_activity",
4920
+ state: "mastra_knowledge_access_state"
4921
+ };
4922
+ /** Thrown when existing Knowledge collections can't be used as schema 4 without a reset. */
4923
+ var MongoKnowledgeResetRequiredError = class extends Error {
4924
+ inspection;
4925
+ constructor(inspection) {
4926
+ super(knowledgePagesResetMessage(inspection.reason));
4927
+ this.name = "KnowledgeSchemaResetRequiredError";
4928
+ this.inspection = inspection;
4781
4929
  }
4782
- assertWithinBound(description);
4930
+ };
4931
+ function escapeRegex(value) {
4932
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4783
4933
  }
4784
- let assertRecordTextWithinBound;
4785
- async function assertKnowledgeRecordTextWithinBoundCompat(text) {
4786
- if (!assertRecordTextWithinBound) assertRecordTextWithinBound = (await import("@mastra/core/storage")).assertKnowledgeRecordTextWithinBound ?? ((value) => {
4787
- if (value.length > 1e3) throw new Error("Knowledge record text exceeds the 1000 UTF-16 code unit limit; split it into separate facts or summarize it");
4788
- });
4789
- assertRecordTextWithinBound(text);
4934
+ function isDuplicateKey(error) {
4935
+ return typeof error === "object" && error !== null && error.code === MONGO_DUPLICATE_KEY;
4936
+ }
4937
+ function encodeCursor(parts) {
4938
+ return Buffer.from(JSON.stringify(parts)).toString("base64url");
4790
4939
  }
4791
- const cloneScope = (scope) => [...scope];
4792
- const canonicalName = (name) => name.trim().toLocaleLowerCase();
4793
- const nodeReferenceId = (node) => typeof node === "string" ? node : node.id;
4794
- const sessionOptions = (session) => session ? { session } : {};
4795
- function visibleScopeKeys(scope) {
4796
- const canonical = canonicalizeKnowledgeScope(scope);
4797
- const subsets = [[]];
4798
- for (const entry of canonical) subsets.push(...subsets.map((subset) => [...subset, entry]));
4799
- const keys = /* @__PURE__ */ new Set();
4800
- for (const subset of subsets.slice(1)) try {
4801
- keys.add(knowledgeScopeKey(subset));
4940
+ function decodeCursor(cursor) {
4941
+ try {
4942
+ const parts = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
4943
+ if (Array.isArray(parts) && parts.length === 2 && parts.every((part) => typeof part === "string")) return parts;
4802
4944
  } catch {}
4803
- return [...keys];
4945
+ throw new Error("Invalid Knowledge cursor");
4946
+ }
4947
+ function clampLimit(limit, max) {
4948
+ const value = limit ?? DEFAULT_LIST_LIMIT;
4949
+ if (!Number.isInteger(value) || value < 1) throw new Error("Knowledge list limit must be a positive integer");
4950
+ return Math.min(value, max);
4804
4951
  }
4805
- function recordCursorFilter(cursor, expected) {
4806
- const parsed = parseKnowledgeNodeCursor(cursor, expected);
4807
- return { $or: [{ updatedAt: { $lt: parsed.updatedAt } }, {
4808
- updatedAt: parsed.updatedAt,
4809
- $or: [{ name: { $gt: parsed.name } }, {
4810
- name: parsed.name,
4811
- id: { $gt: parsed.id }
4812
- }]
4813
- }] };
4952
+ const live = { deletedAt: { $exists: false } };
4953
+ const notScope = { isScope: { $ne: true } };
4954
+ /** Unique-while-live page key per parent; unset on delete so the key can be reused. */
4955
+ function activeKeyOf(parentId, key) {
4956
+ return `${parentId}\u0000${key}`;
4814
4957
  }
4815
- function nodeFromDocument(row) {
4958
+ /** Pipeline stage recomputing `activeKey` for pages that have a live key after their parent changed. */
4959
+ function recomputeActiveKey(parentId) {
4960
+ return { $set: { activeKey: { $cond: [
4961
+ { $and: [{ $eq: [{ $type: "$key" }, "string"] }, { $eq: [{ $type: "$deletedAt" }, "missing"] }] },
4962
+ { $concat: [
4963
+ parentId,
4964
+ "\0",
4965
+ "$key"
4966
+ ] },
4967
+ "$$REMOVE"
4968
+ ] } } };
4969
+ }
4970
+ function toPage(row) {
4816
4971
  return {
4817
4972
  id: String(row.id),
4818
- type: "node",
4973
+ scopeId: String(row.scopeId),
4974
+ parentId: row.parentId == null ? null : String(row.parentId),
4975
+ isScope: row.isScope === true,
4976
+ ...row.address == null ? {} : { address: String(row.address) },
4977
+ ...row.declaration == null ? {} : { declaration: String(row.declaration) },
4978
+ ...row.key == null ? {} : { key: String(row.key) },
4979
+ position: String(row.position),
4819
4980
  name: String(row.name),
4820
- kind: String(row.kind),
4821
- content: row.content == null ? void 0 : String(row.content),
4822
- description: row.description == null ? void 0 : String(row.description),
4823
- scope: cloneScope(row.scope),
4981
+ ...row.description == null ? {} : { description: String(row.description) },
4982
+ ...row.body == null ? {} : { body: String(row.body) },
4824
4983
  version: Number(row.version),
4825
- mergedInto: row.mergedInto ?? void 0,
4984
+ ...row.mergedInto == null ? {} : { mergedInto: String(row.mergedInto) },
4985
+ createdBy: row.createdBy,
4986
+ updatedBy: row.updatedBy,
4826
4987
  createdAt: new Date(row.createdAt),
4827
- updatedAt: new Date(row.updatedAt)
4988
+ updatedAt: new Date(row.updatedAt),
4989
+ ...row.deletedAt == null ? {} : { deletedAt: new Date(row.deletedAt) },
4990
+ ...row.deletedBy == null ? {} : { deletedBy: String(row.deletedBy) }
4828
4991
  };
4829
4992
  }
4830
- function recordFromDocument(row) {
4993
+ function toNote(row) {
4831
4994
  return {
4832
4995
  id: String(row.id),
4833
- node: String(row.node),
4996
+ pageId: String(row.pageId),
4834
4997
  text: String(row.text),
4835
- scope: cloneScope(row.scope),
4836
- sourceThreadId: String(row.sourceThreadId),
4837
- capturedAt: new Date(row.capturedAt),
4838
- when: row.when ? new Date(row.when) : void 0,
4839
- metadata: row.metadata ?? void 0,
4840
- deletedAt: row.deletedAt ? new Date(row.deletedAt) : void 0,
4841
- deletedBy: row.deletedBy ?? void 0
4998
+ ...row.when == null ? {} : { when: new Date(row.when) },
4999
+ stampScopeIds: [...row.stampScopeIds ?? []].sort(),
5000
+ mentions: [...row.mentions ?? []].sort(),
5001
+ provenance: row.provenance,
5002
+ version: Number(row.version),
5003
+ createdAt: new Date(row.createdAt),
5004
+ updatedAt: new Date(row.updatedAt),
5005
+ ...row.deletedAt == null ? {} : { deletedAt: new Date(row.deletedAt) },
5006
+ ...row.deletedBy == null ? {} : { deletedBy: String(row.deletedBy) }
4842
5007
  };
4843
5008
  }
4844
- function outboxFromDocument(row) {
5009
+ function toListedPin(row) {
5010
+ return {
5011
+ ...toPin(row),
5012
+ pageName: String(row.pageName)
5013
+ };
5014
+ }
5015
+ function toPin(row) {
4845
5016
  return {
4846
5017
  id: String(row.id),
4847
- idempotencyKey: String(row.idempotencyKey),
4848
- documentId: String(row.documentId),
4849
- documentType: row.documentType,
4850
- operation: row.operation,
4851
- scope: cloneScope(row.scope),
4852
- status: row.status,
4853
- attempts: Number(row.attempts),
4854
- availableAt: new Date(row.availableAt),
4855
- claimedAt: row.claimedAt ? new Date(row.claimedAt) : void 0,
4856
- claimedBy: row.claimedBy ?? void 0,
5018
+ pageId: String(row.pageId),
5019
+ audience: [...row.audience ?? []],
5020
+ createdBy: String(row.createdBy),
4857
5021
  createdAt: new Date(row.createdAt),
4858
- completedAt: row.completedAt ? new Date(row.completedAt) : void 0
5022
+ updatedAt: new Date(row.updatedAt)
4859
5023
  };
4860
5024
  }
4861
- const KNOWLEDGE_CURATION_CURSOR_REMOVED_MESSAGE = "Knowledge curation cursors were removed: observation-time curate is the only Knowledge writer and needs no cursor.";
4862
- var KnowledgeMongoDB = class KnowledgeMongoDB extends KnowledgeStorage {
4863
- static MANAGED_COLLECTIONS = [
4864
- TABLE_KNOWLEDGE_NODES,
4865
- TABLE_KNOWLEDGE_RECORDS,
4866
- TABLE_KNOWLEDGE_MENTIONS,
4867
- TABLE_KNOWLEDGE_ACTIVITY,
4868
- TABLE_KNOWLEDGE_SEMANTIC_OUTBOX
4869
- ];
5025
+ const noProjection = { projection: { _id: 0 } };
5026
+ /**
5027
+ * MongoDB implementation of the schema-4 {@link KnowledgePageStore}.
5028
+ *
5029
+ * Notes embed their stamps and mentions and carry two denormalized copies of page scope
5030
+ * (`pageScopeId`, `mentionScopeIds`); activity events carry `pageScopeId`. Moves and merges rewrite
5031
+ * those copies in the same transaction as the page rows, so a read is one indexed filter against the
5032
+ * set of readable scope ids resolved from the read scope (never a per-address query).
5033
+ */
5034
+ var MongoKnowledgePages = class {
4870
5035
  #connector;
4871
- constructor(config) {
4872
- super();
4873
- this.#connector = resolveMongoDBConfig(config);
5036
+ #bounds;
5037
+ constructor(options) {
5038
+ this.#connector = options.connector;
5039
+ this.#bounds = options.bounds;
5040
+ }
5041
+ #c(name) {
5042
+ return this.#connector.getCollection(MONGO_KNOWLEDGE_PAGES_COLLECTIONS[name]);
5043
+ }
5044
+ #tx(operation) {
5045
+ return this.#connector.withTransaction(operation);
5046
+ }
5047
+ async inspectSchema() {
5048
+ return inspectKnowledgePagesSchema(await this.#snapshot());
5049
+ }
5050
+ async #snapshot() {
5051
+ const tableNames = [];
5052
+ for (const name of KNOWLEDGE_RESETTABLE_TABLE_NAMES) if (await this.#connector.collectionExists(name)) tableNames.push(name);
5053
+ let schemaVersion;
5054
+ if (tableNames.includes(MONGO_KNOWLEDGE_PAGES_COLLECTIONS.state)) {
5055
+ const state = await (await this.#c("state")).findOne({ id: "knowledge" });
5056
+ schemaVersion = typeof state?.schemaVersion === "number" ? state.schemaVersion : void 0;
5057
+ }
5058
+ return {
5059
+ tableNames,
5060
+ schemaVersion
5061
+ };
5062
+ }
5063
+ async #drop(name) {
5064
+ const collection = await this.#connector.getCollection(name);
5065
+ try {
5066
+ await collection.drop();
5067
+ } catch (error) {
5068
+ if (error.codeName !== "NamespaceNotFound") throw error;
5069
+ }
4874
5070
  }
4875
5071
  async init() {
4876
- const nodes = await this.#collection(TABLE_KNOWLEDGE_NODES);
4877
- const knowledge = await this.#collection(TABLE_KNOWLEDGE_RECORDS);
4878
- const mentions = await this.#collection(TABLE_KNOWLEDGE_MENTIONS);
4879
- const activity = await this.#collection(TABLE_KNOWLEDGE_ACTIVITY);
4880
- const outbox = await this.#collection(TABLE_KNOWLEDGE_SEMANTIC_OUTBOX);
5072
+ const stateName = MONGO_KNOWLEDGE_PAGES_COLLECTIONS.state;
5073
+ for (const name of legacyKnowledgeTablesToDrop(await this.#snapshot())) if (name === stateName) await (await this.#c("state")).deleteMany({ schemaVersion: { $ne: 4 } });
5074
+ else await this.#drop(name);
5075
+ await (await this.#c("state")).updateOne({ id: "knowledge" }, { $setOnInsert: {
5076
+ id: "knowledge",
5077
+ schemaVersion: 4
5078
+ } }, { upsert: true });
5079
+ const inspection = await this.inspectSchema();
5080
+ if (inspection.status === "incompatible-reset-required") throw new MongoKnowledgeResetRequiredError(inspection);
5081
+ const [pages, notes, pins, activity] = await Promise.all([
5082
+ this.#c("pages"),
5083
+ this.#c("notes"),
5084
+ this.#c("pins"),
5085
+ this.#c("activity")
5086
+ ]);
4881
5087
  await Promise.all([
4882
- nodes.createIndex({
4883
- type: 1,
4884
- scopeKey: 1,
4885
- canonicalName: 1
4886
- }, { unique: true }),
4887
- nodes.createIndex({
4888
- scopeKey: 1,
4889
- type: 1
5088
+ pages.createIndex({ id: 1 }, { unique: true }),
5089
+ pages.createIndex({ address: 1 }, {
5090
+ unique: true,
5091
+ partialFilterExpression: { isScope: true }
4890
5092
  }),
4891
- nodes.createIndex({
4892
- type: 1,
4893
- canonicalName: 1
5093
+ pages.createIndex({ activeKey: 1 }, {
5094
+ unique: true,
5095
+ partialFilterExpression: { activeKey: { $type: "string" } }
4894
5096
  }),
4895
- knowledge.createIndex({
4896
- node: 1,
4897
- id: -1
5097
+ pages.createIndex({
5098
+ parentId: 1,
5099
+ position: 1,
5100
+ id: 1
4898
5101
  }),
4899
- knowledge.createIndex({
4900
- sourceThreadId: 1,
4901
- id: -1
5102
+ pages.createIndex({
5103
+ scopeId: 1,
5104
+ name: 1,
5105
+ id: 1
4902
5106
  }),
4903
- knowledge.createIndex({
4904
- scopeKey: 1,
4905
- id: -1
5107
+ pages.createIndex({
5108
+ canonicalName: 1,
5109
+ scopeId: 1
4906
5110
  }),
4907
- mentions.createIndex({
4908
- sourceType: 1,
4909
- sourceId: 1,
4910
- recordId: 1
4911
- }, { unique: true }),
4912
- mentions.createIndex({
4913
- recordId: 1,
4914
- sourceType: 1,
4915
- sourceId: 1
5111
+ pages.createIndex({
5112
+ isScope: 1,
5113
+ declaration: 1,
5114
+ parentId: 1
4916
5115
  }),
4917
- activity.createIndex({ id: -1 }),
5116
+ notes.createIndex({ id: 1 }, { unique: true }),
5117
+ notes.createIndex({
5118
+ pageId: 1,
5119
+ createdAt: 1,
5120
+ id: 1
5121
+ }),
5122
+ notes.createIndex({ mentions: 1 }),
5123
+ notes.createIndex({ pageScopeId: 1 }),
5124
+ notes.createIndex({ stampScopeIds: 1 }),
5125
+ pins.createIndex({ id: 1 }, { unique: true }),
5126
+ pins.createIndex({ pageId: 1 }, { unique: true }),
5127
+ pins.createIndex({ audience: 1 }),
5128
+ activity.createIndex({ id: 1 }, { unique: true }),
4918
5129
  activity.createIndex({
4919
- scopeKey: 1,
5130
+ pageScopeId: 1,
5131
+ createdAt: -1,
4920
5132
  id: -1
4921
5133
  }),
4922
- outbox.createIndex({ idempotencyKey: 1 }, { unique: true }),
4923
- outbox.createIndex({
4924
- status: 1,
4925
- availableAt: 1,
4926
- createdAt: 1
5134
+ activity.createIndex({
5135
+ pageId: 1,
5136
+ createdAt: -1,
5137
+ id: -1
4927
5138
  })
4928
5139
  ]);
4929
5140
  }
4930
- async dangerouslyClearAll() {
4931
- await this.#connector.withTransaction(async (session) => {
4932
- for (const name of KnowledgeMongoDB.MANAGED_COLLECTIONS) await (await this.#collection(name)).deleteMany({}, sessionOptions(session));
5141
+ /** Drops exactly the Knowledge collections (never Memory's) and recreates schema 4. */
5142
+ async reset() {
5143
+ for (const name of KNOWLEDGE_RESETTABLE_TABLE_NAMES) await this.#drop(name);
5144
+ await this.init();
5145
+ }
5146
+ /** The ids of every scope page the read scope can see; `null` means everything. */
5147
+ async #readable(read, session) {
5148
+ if (read === "all") return null;
5149
+ const any = this.#readableClauses(read);
5150
+ if (!any.length) return [];
5151
+ return (await (await this.#c("pages")).find({
5152
+ isScope: true,
5153
+ $or: any
5154
+ }, {
5155
+ projection: {
5156
+ _id: 0,
5157
+ id: 1
5158
+ },
5159
+ session
5160
+ }).toArray()).map((row) => String(row.id));
5161
+ }
5162
+ #readableClauses(read) {
5163
+ const any = [];
5164
+ if (read.addresses.length) any.push({ address: { $in: read.addresses } });
5165
+ for (const entry of read.prefixes) any.push({
5166
+ declaration: entry.declaration,
5167
+ address: { $regex: `^${escapeRegex(entry.prefix)}` }
4933
5168
  });
5169
+ for (const entry of read.children) any.push({
5170
+ declaration: entry.declaration,
5171
+ parentId: entry.parentId
5172
+ });
5173
+ return any;
4934
5174
  }
4935
- async createNode(input) {
4936
- await assertKnowledgeDescriptionWithinBoundCompat(input.description);
4937
- if (input.scopeAddresses?.length) {
4938
- const error = /* @__PURE__ */ new Error("This Knowledge storage adapter does not expose structural scope placement.");
4939
- error.name = "KnowledgeUnsupportedCapabilityError";
4940
- throw error;
5175
+ /**
5176
+ * Whether one scope page is readable, answered by a single indexed lookup instead of resolving the
5177
+ * whole frontier. Use it when a call targets one scope; the frontier can be thousands of scopes.
5178
+ */
5179
+ async #scopeReadable(read, scopeId) {
5180
+ if (read === "all") return true;
5181
+ const any = this.#readableClauses(read);
5182
+ if (!any.length) return false;
5183
+ return await (await this.#c("pages")).countDocuments({
5184
+ id: scopeId,
5185
+ isScope: true,
5186
+ $or: any
5187
+ }, { limit: 1 }) > 0;
5188
+ }
5189
+ #inScopes(field, ids) {
5190
+ return ids === null ? {} : { [field]: { $in: ids } };
5191
+ }
5192
+ #visibleNotes(ids) {
5193
+ if (ids === null) return {};
5194
+ return {
5195
+ pageScopeId: { $in: ids },
5196
+ stampScopeIds: { $in: ids },
5197
+ mentionScopeIds: { $not: { $elemMatch: { $nin: ids } } }
5198
+ };
5199
+ }
5200
+ async #page(id, session) {
5201
+ const row = await (await this.#c("pages")).findOne({ id }, {
5202
+ ...noProjection,
5203
+ session
5204
+ });
5205
+ return row ? toPage(row) : null;
5206
+ }
5207
+ async #requireLivePage(id, session) {
5208
+ const page = await this.#page(id, session);
5209
+ if (!page || page.deletedAt) throw new KnowledgeNotFoundError("page", id);
5210
+ return page;
5211
+ }
5212
+ async #log(session, event) {
5213
+ await (await this.#c("activity")).insertOne({
5214
+ id: createKnowledgeUlid(),
5215
+ action: event.action,
5216
+ scopeId: event.scopeId,
5217
+ pageScopeId: event.pageScopeId ?? event.scopeId,
5218
+ pageId: event.pageId,
5219
+ ...event.noteId ? { noteId: event.noteId } : {},
5220
+ provenance: event.provenance,
5221
+ createdAt: event.at
5222
+ }, { session });
5223
+ }
5224
+ async #lastPosition(parentId, session) {
5225
+ const row = await (await this.#c("pages")).find({
5226
+ parentId,
5227
+ ...live
5228
+ }, {
5229
+ projection: {
5230
+ _id: 0,
5231
+ position: 1
5232
+ },
5233
+ session
5234
+ }).sort({ position: -1 }).limit(1).next();
5235
+ return row ? String(row.position) : void 0;
5236
+ }
5237
+ /**
5238
+ * Ordinary descendants of `rootId`, level by level. Stops counting once the live total passes the
5239
+ * move cap, so an oversize subtree is reported without being walked to the end.
5240
+ */
5241
+ async #subtree(rootId, session, find) {
5242
+ const core = await loadPagesCore();
5243
+ const pages = await this.#c("pages");
5244
+ const ids = [];
5245
+ let liveCount = 0;
5246
+ let maxDepth = 0;
5247
+ let frontier = [rootId];
5248
+ while (frontier.length) {
5249
+ const rows = await pages.find({
5250
+ parentId: { $in: frontier },
5251
+ ...notScope
5252
+ }, {
5253
+ projection: {
5254
+ _id: 0,
5255
+ id: 1,
5256
+ depth: 1,
5257
+ deletedAt: 1
5258
+ },
5259
+ session
5260
+ }).toArray();
5261
+ frontier = [];
5262
+ for (const row of rows) {
5263
+ const id = String(row.id);
5264
+ if (find !== void 0 && id === find) return {
5265
+ ids,
5266
+ live: liveCount,
5267
+ maxDepth,
5268
+ found: true,
5269
+ truncated: false
5270
+ };
5271
+ ids.push(id);
5272
+ frontier.push(id);
5273
+ if (row.deletedAt == null) {
5274
+ liveCount++;
5275
+ maxDepth = Math.max(maxDepth, Number(row.depth));
5276
+ }
5277
+ }
5278
+ if (liveCount > core.MAX_KNOWLEDGE_MOVE_PAGES) return {
5279
+ ids,
5280
+ live: liveCount,
5281
+ maxDepth,
5282
+ found: false,
5283
+ truncated: true
5284
+ };
4941
5285
  }
4942
- const scope = canonicalizeKnowledgeScope(input.scope);
4943
- return this.#connector.withTransaction(async (session) => {
4944
- const existing = await this.#getNodeByName(input.name, scope, session);
4945
- if (existing) {
4946
- const terminal = await this.#resolveTerminalNode(existing.id, session);
4947
- if (!isKnowledgeScopeVisible(terminal.scope, scope)) throw new Error(`Merged knowledge node is not visible from scope: ${input.name}`);
4948
- return terminal;
5286
+ return {
5287
+ ids,
5288
+ live: liveCount,
5289
+ maxDepth,
5290
+ found: false,
5291
+ truncated: false
5292
+ };
5293
+ }
5294
+ /** Refuses before any write when a live arriving page's key is already live under `parentId`. */
5295
+ async #assertNoKeyConflicts(keys, parentId, session, exceptId) {
5296
+ const core = await loadPagesCore();
5297
+ if (!keys.length) return;
5298
+ const clash = await (await this.#c("pages")).findOne({
5299
+ ...live,
5300
+ parentId,
5301
+ key: { $in: keys },
5302
+ ...exceptId ? { id: { $ne: exceptId } } : {}
5303
+ }, {
5304
+ projection: {
5305
+ _id: 0,
5306
+ key: 1
5307
+ },
5308
+ session
5309
+ });
5310
+ if (clash) throw new core.KnowledgePageKeyConflictError(String(clash.key), parentId);
5311
+ }
5312
+ /** Recomputes the denormalized scope of every page a note mentions, for notes touching `pageIds`. */
5313
+ async #refreshMentionScopes(pageIds, session) {
5314
+ if (!pageIds.length) return;
5315
+ const notes = await this.#c("notes");
5316
+ const pages = await this.#c("pages");
5317
+ const affected = await notes.find({ mentions: { $in: pageIds } }, {
5318
+ projection: {
5319
+ _id: 0,
5320
+ id: 1,
5321
+ mentions: 1
5322
+ },
5323
+ session
5324
+ }).toArray();
5325
+ for (const note of affected) {
5326
+ const mentioned = await pages.find({ id: { $in: note.mentions } }, {
5327
+ projection: {
5328
+ _id: 0,
5329
+ scopeId: 1
5330
+ },
5331
+ session
5332
+ }).toArray();
5333
+ await notes.updateOne({ id: note.id }, { $set: { mentionScopeIds: [...new Set(mentioned.map((page) => String(page.scopeId)))] } }, { session });
5334
+ }
5335
+ }
5336
+ async materializeScope(input) {
5337
+ for (let attempt = 0;; attempt++) try {
5338
+ return await this.#materializeScopeOnce(input);
5339
+ } catch (error) {
5340
+ if (!(error instanceof (await loadPagesCore()).KnowledgePageKeyConflictError || error instanceof KnowledgeConflictError || isDuplicateKey(error) || error?.hasErrorLabel?.("TransientTransactionError")) || attempt >= 4) throw error;
5341
+ }
5342
+ }
5343
+ async #materializeScopeOnce(input) {
5344
+ const bounds = await this.#bounds();
5345
+ bounds.assertKnowledgePageDescriptionWithinBound(input.description);
5346
+ for (const child of input.children ?? []) bounds.assertKnowledgePageDescriptionWithinBound(child.description);
5347
+ const checkTemplates = (templates) => {
5348
+ for (const template of templates) {
5349
+ bounds.assertKnowledgePageDescriptionWithinBound(template.description);
5350
+ checkTemplates(template.pages ?? []);
4949
5351
  }
5352
+ };
5353
+ checkTemplates(input.pages ?? []);
5354
+ return this.#tx(async (session) => {
4950
5355
  const now = /* @__PURE__ */ new Date();
4951
- const node = {
4952
- id: input.id ?? crypto.randomUUID(),
4953
- type: "node",
4954
- name: input.name.trim(),
4955
- kind: input.kind,
4956
- content: input.content,
4957
- description: input.description,
5356
+ let scope = await this.#scopeByAddress(input.address, session);
5357
+ let created = false;
5358
+ let parentConflict;
5359
+ if (!scope) {
5360
+ if (input.parentId) await this.#requireLivePage(input.parentId, session);
5361
+ scope = await this.#insertScope(session, {
5362
+ address: input.address,
5363
+ declaration: input.declaration,
5364
+ name: input.name,
5365
+ description: input.description,
5366
+ parentId: input.parentId ?? null,
5367
+ provenance: input.provenance,
5368
+ at: now
5369
+ });
5370
+ created = true;
5371
+ } else if (input.parentId && scope.parentId === null) {
5372
+ await this.#requireLivePage(input.parentId, session);
5373
+ const linked = await (await this.#c("pages")).findOneAndUpdate({
5374
+ id: scope.id,
5375
+ parentId: null
5376
+ }, { $set: { parentId: input.parentId } }, {
5377
+ returnDocument: "after",
5378
+ projection: { _id: 0 },
5379
+ session
5380
+ });
5381
+ scope = toPage(linked ?? await this.#scopeByAddress(input.address, session));
5382
+ if (!linked && input.parentId !== scope.parentId) parentConflict = {
5383
+ existingParentId: scope.parentId,
5384
+ requestedParentId: input.parentId
5385
+ };
5386
+ } else if (input.parentId !== void 0 && (input.parentId ?? null) !== scope.parentId) parentConflict = {
5387
+ existingParentId: scope.parentId,
5388
+ requestedParentId: input.parentId ?? null
5389
+ };
5390
+ for (const child of input.children ?? []) {
5391
+ if (await this.#scopeByAddress(child.address, session)) continue;
5392
+ await this.#insertScope(session, {
5393
+ address: child.address,
5394
+ declaration: child.declaration,
5395
+ name: child.name,
5396
+ description: child.description,
5397
+ parentId: scope.id,
5398
+ provenance: input.provenance,
5399
+ at: now
5400
+ });
5401
+ }
5402
+ await this.#materializeTemplates(session, scope, input.pages ?? [], input.provenance, now);
5403
+ return {
4958
5404
  scope,
4959
- version: 1,
4960
- createdAt: now,
4961
- updatedAt: now
5405
+ created,
5406
+ ...parentConflict ? { parentConflict } : {}
4962
5407
  };
4963
- try {
4964
- await (await this.#nodes()).insertOne({
4965
- ...node,
4966
- canonicalName: canonicalName(node.name),
4967
- scopeKey: knowledgeScopeKey(scope),
4968
- mergedInto: null
4969
- }, sessionOptions(session));
4970
- } catch (error) {
4971
- if (error.code !== 11e3) throw error;
4972
- const concurrent = await this.#getNodeByName(input.name, scope, session);
4973
- if (!concurrent) throw error;
4974
- return await this.#resolveTerminalNode(concurrent.id, session);
4975
- }
4976
- await this.#replaceMentions("node", node.id, node.content ?? "", input.resolutionScope ?? scope, scope, session);
4977
- await this.#activity("node-created", "node", node.id, scope, void 0, session);
4978
- await this.#outbox("node", node.id, "upsert", 1, scope, session);
4979
- return node;
4980
5408
  });
4981
5409
  }
4982
- async getNode(id) {
4983
- return this.#getNode(id);
5410
+ async #materializeTemplates(session, parent, templates, provenance, at) {
5411
+ const pages = await this.#c("pages");
5412
+ for (const template of templates) {
5413
+ const problem = knowledgePageKeyProblem(template.key);
5414
+ if (problem) throw new Error(problem);
5415
+ const existing = await pages.findOne({
5416
+ parentId: parent.id,
5417
+ key: template.key,
5418
+ ...live
5419
+ }, {
5420
+ ...noProjection,
5421
+ session
5422
+ });
5423
+ const page = existing ? toPage(existing) : await this.#insertPage(session, {
5424
+ parent,
5425
+ key: template.key,
5426
+ name: template.name,
5427
+ description: template.description,
5428
+ provenance,
5429
+ at
5430
+ });
5431
+ if (template.pages?.length) await this.#materializeTemplates(session, page, template.pages, provenance, at);
5432
+ }
5433
+ }
5434
+ async #scopeByAddress(address, session) {
5435
+ const row = await (await this.#c("pages")).findOne({
5436
+ isScope: true,
5437
+ address
5438
+ }, {
5439
+ ...noProjection,
5440
+ session
5441
+ });
5442
+ return row ? toPage(row) : null;
5443
+ }
5444
+ async #insertScope(session, input) {
5445
+ const core = await loadPagesCore();
5446
+ const id = createKnowledgeUlid();
5447
+ const doc = {
5448
+ id,
5449
+ scopeId: id,
5450
+ parentId: input.parentId,
5451
+ isScope: true,
5452
+ address: input.address,
5453
+ declaration: input.declaration,
5454
+ position: core.knowledgePositionBetween(await this.#lastPosition(input.parentId, session)),
5455
+ name: input.name,
5456
+ canonicalName: core.canonicalKnowledgeName(input.name),
5457
+ ...input.description === void 0 ? {} : { description: input.description },
5458
+ depth: 0,
5459
+ version: 1,
5460
+ createdBy: input.provenance,
5461
+ updatedBy: input.provenance,
5462
+ createdAt: input.at,
5463
+ updatedAt: input.at
5464
+ };
5465
+ try {
5466
+ await (await this.#c("pages")).insertOne({ ...doc }, { session });
5467
+ } catch (error) {
5468
+ if (isDuplicateKey(error)) throw new KnowledgeConflictError(input.address);
5469
+ throw error;
5470
+ }
5471
+ await this.#log(session, {
5472
+ action: "scope-created",
5473
+ scopeId: id,
5474
+ pageId: id,
5475
+ provenance: input.provenance,
5476
+ at: input.at
5477
+ });
5478
+ return toPage(doc);
5479
+ }
5480
+ async getScopesByAddress(addresses) {
5481
+ if (addresses.length === 0) return [];
5482
+ const rows = await (await this.#c("pages")).find({
5483
+ isScope: true,
5484
+ address: { $in: addresses }
5485
+ }, noProjection).toArray();
5486
+ const byAddress = new Map(rows.map((row) => [String(row.address), toPage(row)]));
5487
+ return [...new Set(addresses)].flatMap((address) => byAddress.get(address) ?? []);
5488
+ }
5489
+ async listScopeDeclarations() {
5490
+ return (await (await this.#c("pages")).aggregate([{ $match: {
5491
+ isScope: true,
5492
+ declaration: { $type: "string" }
5493
+ } }, { $group: {
5494
+ _id: "$declaration",
5495
+ scopes: { $sum: 1 }
5496
+ } }]).toArray()).map((row) => ({
5497
+ declaration: String(row._id),
5498
+ scopes: Number(row.scopes)
5499
+ })).sort((a, b) => a.declaration < b.declaration ? -1 : a.declaration > b.declaration ? 1 : 0);
5500
+ }
5501
+ async renameScopeDeclaration(input) {
5502
+ return this.#tx(async (session) => {
5503
+ const pages = await this.#c("pages");
5504
+ const rows = await pages.find({
5505
+ isScope: true,
5506
+ $or: [{ declaration: input.from }, { declaration: { $regex: `^${escapeRegex(`${input.from}/`)}` } }]
5507
+ }, {
5508
+ projection: {
5509
+ _id: 0,
5510
+ id: 1,
5511
+ address: 1,
5512
+ declaration: 1
5513
+ },
5514
+ session
5515
+ }).toArray();
5516
+ if (rows.length === 0) return 0;
5517
+ const renames = rows.map((row) => ({
5518
+ address: String(row.address),
5519
+ from: String(row.declaration),
5520
+ to: input.to + String(row.declaration).slice(input.from.length)
5521
+ }));
5522
+ input.check(renames);
5523
+ if ((await pages.bulkWrite(rows.map((row, index) => ({ updateOne: {
5524
+ filter: {
5525
+ id: row.id,
5526
+ isScope: true
5527
+ },
5528
+ update: { $set: { declaration: renames[index].to } }
5529
+ } })), { session })).matchedCount < rows.length) throw new KnowledgeConflictError("Knowledge scope declarations changed while renaming; retry the rename");
5530
+ return rows.length;
5531
+ });
5532
+ }
5533
+ async listChildScopes(input) {
5534
+ const core = await loadPagesCore();
5535
+ const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
5536
+ const filter = {
5537
+ isScope: true,
5538
+ parentId: input.parentId,
5539
+ ...live
5540
+ };
5541
+ if (input.cursor) {
5542
+ const [position, id] = decodeCursor(input.cursor);
5543
+ filter.$or = [{ position: { $gt: position } }, {
5544
+ position,
5545
+ id: { $gt: id }
5546
+ }];
5547
+ }
5548
+ const rows = await (await this.#c("pages")).find(filter, noProjection).sort({
5549
+ position: 1,
5550
+ id: 1
5551
+ }).limit(limit + 1).toArray();
5552
+ return this.#pageOutput(rows, limit, (page) => [page.position, page.id]);
5553
+ }
5554
+ #pageOutput(rows, limit, cursorOf) {
5555
+ const pages = rows.slice(0, limit).map(toPage);
5556
+ const last = pages[pages.length - 1];
5557
+ return {
5558
+ pages,
5559
+ ...rows.length > limit && last ? { nextCursor: encodeCursor(cursorOf(last)) } : {}
5560
+ };
5561
+ }
5562
+ async getPage(input) {
5563
+ const row = await (await this.#c("pages")).findOne({ id: input.id }, noProjection);
5564
+ return row && await this.#scopeReadable(input.read, String(row.scopeId)) ? toPage(row) : null;
5565
+ }
5566
+ async getPageByPath(input) {
5567
+ if (!input.path.length) return null;
5568
+ if (!await this.#scopeReadable(input.read, input.scopeId)) return null;
5569
+ const pages = await this.#c("pages");
5570
+ let page;
5571
+ let parentId = input.scopeId;
5572
+ for (const key of input.path) {
5573
+ const row = await pages.findOne({
5574
+ parentId,
5575
+ key,
5576
+ scopeId: input.scopeId,
5577
+ ...live,
5578
+ ...notScope
5579
+ }, noProjection);
5580
+ if (!row) return null;
5581
+ page = toPage(row);
5582
+ parentId = page.id;
5583
+ }
5584
+ return page ?? null;
5585
+ }
5586
+ async findPagesByName(input) {
5587
+ const core = await loadPagesCore();
5588
+ const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
5589
+ const filter = {
5590
+ canonicalName: core.canonicalKnowledgeName(input.name),
5591
+ ...live
5592
+ };
5593
+ if (!await this.#scopeFilter(filter, input.read, input.scopeId)) return [];
5594
+ return (await (await this.#c("pages")).find(filter, noProjection).sort({
5595
+ createdAt: -1,
5596
+ id: -1
5597
+ }).limit(limit).toArray()).map(toPage);
4984
5598
  }
4985
- async getNodeByName(input) {
4986
- return this.#getNodeByName(input.name, canonicalizeKnowledgeScope(input.scope));
5599
+ /**
5600
+ * Adds the home-scope constraint: the explicit scope when given (one indexed readability check),
5601
+ * otherwise the readable frontier. Returns false when nothing can match.
5602
+ */
5603
+ async #scopeFilter(filter, read, scopeId) {
5604
+ if (scopeId !== void 0) {
5605
+ if (!await this.#scopeReadable(read, scopeId)) return false;
5606
+ filter.scopeId = scopeId;
5607
+ return true;
5608
+ }
5609
+ const ids = await this.#readable(read);
5610
+ if (ids !== null) filter.scopeId = { $in: ids };
5611
+ return true;
4987
5612
  }
4988
- async resolveNode(input) {
4989
- return this.#resolveNode(input.name, canonicalizeKnowledgeScope(input.scope));
5613
+ async listChildPages(input) {
5614
+ const core = await loadPagesCore();
5615
+ const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
5616
+ const ids = await this.#readable(input.read);
5617
+ const filter = {
5618
+ parentId: input.parentId,
5619
+ ...notScope,
5620
+ ...live,
5621
+ ...this.#inScopes("scopeId", ids)
5622
+ };
5623
+ if (input.cursor) {
5624
+ const [position, id] = decodeCursor(input.cursor);
5625
+ filter.$or = [{ position: { $gt: position } }, {
5626
+ position,
5627
+ id: { $gt: id }
5628
+ }];
5629
+ }
5630
+ const rows = await (await this.#c("pages")).find(filter, noProjection).sort({
5631
+ position: 1,
5632
+ id: 1
5633
+ }).limit(limit + 1).toArray();
5634
+ return this.#pageOutput(rows, limit, (page) => [page.position, page.id]);
4990
5635
  }
4991
- async listNodes(input) {
5636
+ async listPages(input) {
5637
+ const core = await loadPagesCore();
5638
+ const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
4992
5639
  const filter = {
4993
- type: "node",
4994
- mergedInto: null,
4995
- scopeKey: { $in: visibleScopeKeys(canonicalizeKnowledgeScope(input.scope)) },
4996
- ...input.namePrefix ? { canonicalName: { $regex: `^${this.#escapeRegex(canonicalName(input.namePrefix))}` } } : {},
4997
- ...input.kind ? { kind: input.kind } : {},
4998
- ...input.hasContent === void 0 ? {} : input.hasContent ? { content: {
4999
- $exists: true,
5000
- $nin: [null, ""]
5001
- } } : { $or: [
5002
- { content: { $exists: false } },
5003
- { content: null },
5004
- { content: "" }
5005
- ] },
5006
- ...input.cursor ? recordCursorFilter(input.cursor, {
5007
- namePrefix: input.namePrefix,
5008
- kind: input.kind,
5009
- hasContent: input.hasContent
5010
- }) : {}
5640
+ ...notScope,
5641
+ ...live
5011
5642
  };
5012
- return (await (await this.#nodes()).find(filter).sort({
5013
- updatedAt: -1,
5643
+ if (!await this.#scopeFilter(filter, input.read, input.scopeId)) return { pages: [] };
5644
+ if (input.namePrefix) filter.canonicalName = { $regex: `^${escapeRegex(core.canonicalKnowledgeName(input.namePrefix))}` };
5645
+ if (input.cursor) {
5646
+ const [name, id] = decodeCursor(input.cursor);
5647
+ filter.$or = [{ name: { $gt: name } }, {
5648
+ name,
5649
+ id: { $gt: id }
5650
+ }];
5651
+ }
5652
+ const rows = await (await this.#c("pages")).find(filter, noProjection).sort({
5014
5653
  name: 1,
5015
5654
  id: 1
5016
- }).limit(input.limit ?? 100).toArray()).map(nodeFromDocument);
5655
+ }).limit(limit + 1).toArray();
5656
+ return this.#pageOutput(rows, limit, (page) => [page.name, page.id]);
5657
+ }
5658
+ async createPage(input) {
5659
+ (await this.#bounds()).assertKnowledgePageDescriptionWithinBound(input.description);
5660
+ if (input.key !== void 0) {
5661
+ const problem = knowledgePageKeyProblem(input.key);
5662
+ if (problem) throw new Error(problem);
5663
+ }
5664
+ return this.#tx(async (session) => {
5665
+ const parent = await this.#requireLivePage(input.parentId, session);
5666
+ return this.#insertPage(session, {
5667
+ id: input.id,
5668
+ parent,
5669
+ key: input.key,
5670
+ name: input.name,
5671
+ description: input.description,
5672
+ body: input.body,
5673
+ position: input.position,
5674
+ provenance: input.provenance,
5675
+ at: /* @__PURE__ */ new Date()
5676
+ });
5677
+ });
5017
5678
  }
5018
- async updateNode(input) {
5019
- await assertKnowledgeDescriptionWithinBoundCompat(input.description);
5020
- return this.#connector.withTransaction(async (session) => {
5021
- const existing = await this.#getNode(input.id, session);
5022
- if (!existing) throw new KnowledgeNotFoundError("node", input.id);
5023
- if (existing.mergedInto) throw new Error(`Cannot update merged knowledge node: ${input.id}`);
5024
- const scope = input.scope ? canonicalizeKnowledgeScope(input.scope) : existing.scope;
5025
- const name = input.name?.trim() ?? existing.name;
5026
- const content = input.content ?? existing.content;
5027
- const description = input.description ?? existing.description;
5679
+ async #insertPage(session, input) {
5680
+ const core = await loadPagesCore();
5681
+ const id = input.id ?? createKnowledgeUlid();
5682
+ const scopeId = input.parent.isScope ? input.parent.id : input.parent.scopeId;
5683
+ const depth = input.parent.isScope ? 1 : await this.#depthOf(input.parent.id, session) + 1;
5684
+ if (depth > core.MAX_KNOWLEDGE_PAGE_DEPTH) throw new core.KnowledgePageDepthError(id);
5685
+ const doc = {
5686
+ id,
5687
+ scopeId,
5688
+ parentId: input.parent.id,
5689
+ isScope: false,
5690
+ ...input.key === void 0 ? {} : {
5691
+ key: input.key,
5692
+ activeKey: activeKeyOf(input.parent.id, input.key)
5693
+ },
5694
+ position: input.position ?? core.knowledgePositionBetween(await this.#lastPosition(input.parent.id, session)),
5695
+ name: input.name,
5696
+ canonicalName: core.canonicalKnowledgeName(input.name),
5697
+ ...input.description === void 0 ? {} : { description: input.description },
5698
+ ...input.body === void 0 ? {} : { body: input.body },
5699
+ depth,
5700
+ version: 1,
5701
+ createdBy: input.provenance,
5702
+ updatedBy: input.provenance,
5703
+ createdAt: input.at,
5704
+ updatedAt: input.at
5705
+ };
5706
+ try {
5707
+ await (await this.#c("pages")).insertOne({ ...doc }, { session });
5708
+ } catch (error) {
5709
+ if (isDuplicateKey(error)) {
5710
+ if (input.key) throw new core.KnowledgePageKeyConflictError(input.key, input.parent.id);
5711
+ throw new KnowledgeConflictError(id);
5712
+ }
5713
+ throw error;
5714
+ }
5715
+ await this.#log(session, {
5716
+ action: "page-created",
5717
+ scopeId,
5718
+ pageId: id,
5719
+ provenance: input.provenance,
5720
+ at: input.at
5721
+ });
5722
+ return toPage(doc);
5723
+ }
5724
+ async #depthOf(id, session) {
5725
+ const row = await (await this.#c("pages")).findOne({ id }, {
5726
+ projection: {
5727
+ _id: 0,
5728
+ depth: 1
5729
+ },
5730
+ session
5731
+ });
5732
+ return Number(row?.depth ?? 0);
5733
+ }
5734
+ async updatePage(input) {
5735
+ const core = await loadPagesCore();
5736
+ (await this.#bounds()).assertKnowledgePageDescriptionWithinBound(input.description);
5737
+ return this.#tx(async (session) => {
5738
+ const page = await this.#requireLivePage(input.id, session);
5739
+ if (page.version !== input.expectedVersion) throw new KnowledgeConflictError(input.id);
5028
5740
  const now = /* @__PURE__ */ new Date();
5029
- const result = await (await this.#nodes()).findOneAndUpdate({
5741
+ const set = {
5742
+ updatedAt: now,
5743
+ updatedBy: input.provenance
5744
+ };
5745
+ const unset = {};
5746
+ if (input.name !== void 0) {
5747
+ set.name = input.name;
5748
+ set.canonicalName = core.canonicalKnowledgeName(input.name);
5749
+ }
5750
+ if (input.description !== void 0) {
5751
+ if (input.description === "") unset.description = "";
5752
+ else set.description = input.description;
5753
+ }
5754
+ if (input.body !== void 0) set.body = input.body;
5755
+ const updated = await (await this.#c("pages")).findOneAndUpdate({
5030
5756
  id: input.id,
5031
- type: "node",
5032
- version: input.version
5757
+ version: input.expectedVersion,
5758
+ ...live
5033
5759
  }, {
5034
- $set: {
5035
- name,
5036
- canonicalName: canonicalName(name),
5037
- kind: input.kind ?? existing.kind,
5038
- content: content ?? null,
5039
- description: description ?? null,
5040
- scope,
5041
- scopeKey: knowledgeScopeKey(scope),
5042
- updatedAt: now
5043
- },
5044
- $inc: { version: 1 }
5760
+ $set: set,
5761
+ $inc: { version: 1 },
5762
+ ...Object.keys(unset).length ? { $unset: unset } : {}
5045
5763
  }, {
5046
- ...sessionOptions(session),
5047
- returnDocument: "after"
5764
+ returnDocument: "after",
5765
+ projection: { _id: 0 },
5766
+ session
5048
5767
  });
5049
- if (!result) throw new KnowledgeConflictError(input.id);
5050
- if (input.content !== void 0 || input.name !== void 0 || input.scope !== void 0) await this.#replaceMentions("node", input.id, content ?? "", input.resolutionScope ?? scope, scope, session);
5051
- if (knowledgeScopeKey(scope) !== knowledgeScopeKey(existing.scope)) {
5052
- await this.#outbox("node", input.id, "delete", createKnowledgeUlid(), existing.scope, session);
5053
- const records = await (await this.#knowledge()).find({ node: input.id }, sessionOptions(session)).toArray();
5054
- for (const record of records) {
5055
- await this.#outbox("record", record.id, "delete", createKnowledgeUlid(), record.scope, session);
5056
- if (!record.deletedAt) await this.#outbox("record", record.id, "upsert", createKnowledgeUlid(), record.scope, session);
5057
- }
5768
+ if (!updated) throw new KnowledgeConflictError(input.id);
5769
+ await this.#log(session, {
5770
+ action: "page-updated",
5771
+ scopeId: page.scopeId,
5772
+ pageId: page.id,
5773
+ provenance: input.provenance,
5774
+ at: now
5775
+ });
5776
+ return toPage(updated);
5777
+ });
5778
+ }
5779
+ /** Rewrites everything that follows a page's home scope to `scopeId`, for the pages in `moved`. */
5780
+ async #rehome(moved, scopeId, depthDelta, session) {
5781
+ const pages = await this.#c("pages");
5782
+ const descendants = moved.slice(1);
5783
+ if (descendants.length) await pages.updateMany({ id: { $in: descendants } }, [{ $set: {
5784
+ scopeId,
5785
+ depth: { $add: ["$depth", depthDelta] }
5786
+ } }], { session });
5787
+ await (await this.#c("notes")).updateMany({ pageId: { $in: moved } }, { $set: { pageScopeId: scopeId } }, { session });
5788
+ await (await this.#c("activity")).updateMany({ pageId: { $in: moved } }, { $set: { pageScopeId: scopeId } }, { session });
5789
+ }
5790
+ async movePage(input) {
5791
+ const core = await loadPagesCore();
5792
+ return this.#tx(async (session) => {
5793
+ const page = await this.#requireLivePage(input.id, session);
5794
+ if (page.version !== input.expectedVersion) throw new KnowledgeConflictError(input.id);
5795
+ if (page.isScope) throw new Error(`Knowledge scope page ${input.id} can't be moved`);
5796
+ if (input.parentId === input.id) throw new Error(`Knowledge page ${input.id} can't move under itself or its own subtree`);
5797
+ const tree = await this.#subtree(input.id, session, input.parentId);
5798
+ if (tree.found) throw new Error(`Knowledge page ${input.id} can't move under itself or its own subtree`);
5799
+ const parent = await this.#requireLivePage(input.parentId, session);
5800
+ if (tree.live + 1 > core.MAX_KNOWLEDGE_MOVE_PAGES) throw new core.KnowledgeMoveTooLargeError(input.id, tree.live + 1);
5801
+ const newScopeId = parent.isScope ? parent.id : parent.scopeId;
5802
+ const newDepth = parent.isScope ? 1 : await this.#depthOf(parent.id, session) + 1;
5803
+ const delta = newDepth - await this.#depthOf(page.id, session);
5804
+ if (page.key) await this.#assertNoKeyConflicts([page.key], parent.id, session, page.id);
5805
+ if (tree.maxDepth + delta > core.MAX_KNOWLEDGE_PAGE_DEPTH || newDepth > core.MAX_KNOWLEDGE_PAGE_DEPTH) throw new core.KnowledgePageDepthError(input.id);
5806
+ const position = input.position ?? core.knowledgePositionBetween(await this.#lastPosition(parent.id, session));
5807
+ const now = /* @__PURE__ */ new Date();
5808
+ let updated;
5809
+ try {
5810
+ await this.#rehome([page.id, ...tree.ids], newScopeId, delta, session);
5811
+ updated = await (await this.#c("pages")).findOneAndUpdate({
5812
+ id: input.id,
5813
+ version: input.expectedVersion
5814
+ }, {
5815
+ $set: {
5816
+ parentId: parent.id,
5817
+ scopeId: newScopeId,
5818
+ ...page.key ? { activeKey: activeKeyOf(parent.id, page.key) } : {},
5819
+ depth: newDepth,
5820
+ position,
5821
+ updatedAt: now,
5822
+ updatedBy: input.provenance
5823
+ },
5824
+ $inc: { version: 1 }
5825
+ }, {
5826
+ returnDocument: "after",
5827
+ projection: { _id: 0 },
5828
+ session
5829
+ });
5830
+ } catch (error) {
5831
+ if (isDuplicateKey(error)) throw new KnowledgeConflictError(page.key ? `${parent.id}#${page.key}` : input.id);
5832
+ throw error;
5058
5833
  }
5059
- await this.#activity("node-updated", "node", input.id, scope, void 0, session);
5060
- await this.#outbox("node", input.id, "upsert", Number(result.version), scope, session);
5061
- return nodeFromDocument(result);
5834
+ if (!updated) throw new KnowledgeConflictError(input.id);
5835
+ await this.#refreshMentionScopes([page.id, ...tree.ids], session);
5836
+ await this.#log(session, {
5837
+ action: "page-moved",
5838
+ scopeId: newScopeId,
5839
+ pageId: input.id,
5840
+ provenance: input.provenance,
5841
+ at: now
5842
+ });
5843
+ return toPage(updated);
5062
5844
  });
5063
5845
  }
5064
- async mergeNodes(input) {
5065
- if (input.sourceId === input.targetId) throw new Error("Cannot merge a knowledge node into itself");
5066
- return this.#connector.withTransaction(async (session) => {
5067
- const source = await this.#getNode(input.sourceId, session);
5068
- if (!source) throw new KnowledgeNotFoundError("node", input.sourceId);
5069
- const target = await this.#resolveTerminalNode(input.targetId, session);
5070
- if (!target) throw new KnowledgeNotFoundError("node", input.targetId);
5071
- if (target.id === source.id) throw new Error("Cannot create a knowledge merge cycle");
5072
- if (!isKnowledgeScopeVisible(target.scope, source.scope)) throw new Error("Cannot merge a knowledge node into a target that is narrower than its source scope");
5073
- const mentions = await this.#mentions();
5074
- const affected = await mentions.find({ recordId: source.id }, sessionOptions(session)).toArray();
5075
- const movedFacts = await (await this.#knowledge()).find({ node: source.id }, sessionOptions(session)).toArray();
5076
- if ((await (await this.#nodes()).updateOne({
5846
+ async mergePages(input) {
5847
+ const core = await loadPagesCore();
5848
+ return this.#tx(async (session) => {
5849
+ if (input.sourceId === input.targetId) throw new Error(`Knowledge page ${input.sourceId} can't merge into itself or its own subtree`);
5850
+ const source = await this.#requireLivePage(input.sourceId, session);
5851
+ const target = await this.#requireLivePage(input.targetId, session);
5852
+ if (source.version !== input.sourceVersion) throw new KnowledgeConflictError(input.sourceId);
5853
+ if (source.isScope || target.isScope) throw new Error("Knowledge scope pages can't be merged");
5854
+ const tree = await this.#subtree(source.id, session, target.id);
5855
+ if (tree.found) throw new Error(`Knowledge page ${source.id} can't merge into itself or its own subtree`);
5856
+ if (tree.live > core.MAX_KNOWLEDGE_MOVE_PAGES) throw new core.KnowledgeMoveTooLargeError(source.id, tree.live);
5857
+ const delta = await this.#depthOf(target.id, session) - await this.#depthOf(source.id, session);
5858
+ const arriving = await (await this.#c("pages")).find({
5859
+ parentId: source.id,
5860
+ ...live,
5861
+ ...notScope,
5862
+ key: { $type: "string" }
5863
+ }, {
5864
+ projection: {
5865
+ _id: 0,
5866
+ key: 1
5867
+ },
5868
+ session
5869
+ }).toArray();
5870
+ await this.#assertNoKeyConflicts(arriving.map((row) => String(row.key)), target.id, session, source.id);
5871
+ if (tree.maxDepth > 0 && tree.maxDepth + delta > core.MAX_KNOWLEDGE_PAGE_DEPTH) throw new core.KnowledgePageDepthError(source.id);
5872
+ const now = /* @__PURE__ */ new Date();
5873
+ const pages = await this.#c("pages");
5874
+ const notes = await this.#c("notes");
5875
+ if ((await pages.updateOne({
5077
5876
  id: source.id,
5078
- type: "node",
5079
- version: input.sourceVersion,
5080
- mergedInto: null
5877
+ version: input.sourceVersion
5081
5878
  }, {
5082
5879
  $set: {
5880
+ deletedAt: now,
5881
+ deletedBy: input.provenance.author,
5083
5882
  mergedInto: target.id,
5084
- updatedAt: /* @__PURE__ */ new Date()
5883
+ scopeId: target.scopeId,
5884
+ updatedAt: now,
5885
+ updatedBy: input.provenance
5085
5886
  },
5887
+ $unset: { activeKey: "" },
5086
5888
  $inc: { version: 1 }
5087
- }, sessionOptions(session))).modifiedCount === 0) throw new KnowledgeConflictError(source.id);
5088
- await (await this.#knowledge()).updateMany({ node: source.id }, { $set: { node: target.id } }, sessionOptions(session));
5089
- for (const mention of affected) if (await mentions.findOne({
5090
- sourceType: mention.sourceType,
5091
- sourceId: mention.sourceId,
5092
- recordId: target.id
5093
- }, sessionOptions(session))) await mentions.deleteOne({ _id: mention._id }, sessionOptions(session));
5094
- else await mentions.updateOne({ _id: mention._id }, { $set: { recordId: target.id } }, sessionOptions(session));
5095
- for (const record of movedFacts) if (!record.deletedAt) await this.#outbox("record", record.id, "upsert", createKnowledgeUlid(), record.scope, session);
5096
- for (const mention of affected) {
5097
- const scope = mention.sourceType === "record" ? (await (await this.#knowledge()).findOne({ id: mention.sourceId }, sessionOptions(session)))?.scope : (await (await this.#nodes()).findOne({
5098
- id: mention.sourceId,
5099
- type: "node"
5100
- }, sessionOptions(session)))?.scope;
5101
- if (scope) await this.#outbox(mention.sourceType, mention.sourceId, "upsert", createKnowledgeUlid(), scope, session);
5889
+ }, { session })).matchedCount === 0) throw new KnowledgeConflictError(input.sourceId);
5890
+ try {
5891
+ if (tree.ids.length) {
5892
+ await pages.updateMany({ id: { $in: tree.ids } }, [{ $set: {
5893
+ scopeId: target.scopeId,
5894
+ depth: { $add: ["$depth", delta] }
5895
+ } }], { session });
5896
+ await notes.updateMany({ pageId: { $in: tree.ids } }, { $set: { pageScopeId: target.scopeId } }, { session });
5897
+ await (await this.#c("activity")).updateMany({ pageId: { $in: tree.ids } }, { $set: { pageScopeId: target.scopeId } }, { session });
5898
+ }
5899
+ await pages.updateMany({
5900
+ parentId: source.id,
5901
+ ...notScope
5902
+ }, [{ $set: { parentId: target.id } }, recomputeActiveKey(target.id)], { session });
5903
+ } catch (error) {
5904
+ if (isDuplicateKey(error)) throw new KnowledgeConflictError(source.id);
5905
+ throw error;
5102
5906
  }
5103
- let mergedTarget = target;
5104
- if (target.description === void 0 && source.description) {
5105
- const adoptedAt = /* @__PURE__ */ new Date();
5106
- if ((await (await this.#nodes()).updateOne({
5107
- id: target.id,
5108
- type: "node",
5109
- version: target.version,
5110
- description: null
5111
- }, {
5112
- $set: {
5113
- description: source.description,
5114
- updatedAt: adoptedAt
5115
- },
5116
- $inc: { version: 1 }
5117
- }, sessionOptions(session))).modifiedCount > 0) {
5118
- mergedTarget = {
5119
- ...target,
5120
- description: source.description,
5121
- version: target.version + 1,
5122
- updatedAt: adoptedAt
5123
- };
5124
- await this.#activity("node-updated", "node", target.id, target.scope, void 0, session);
5125
- }
5907
+ await notes.updateMany({ pageId: source.id }, { $set: {
5908
+ pageId: target.id,
5909
+ pageScopeId: target.scopeId
5910
+ } }, { session });
5911
+ await notes.updateMany({ mentions: source.id }, [{ $set: { mentions: { $setUnion: [{ $map: {
5912
+ input: "$mentions",
5913
+ in: { $cond: [
5914
+ { $eq: ["$$this", source.id] },
5915
+ target.id,
5916
+ "$$this"
5917
+ ] }
5918
+ } }] } } }], { session });
5919
+ await this.#refreshMentionScopes([target.id, ...tree.ids], session);
5920
+ const pins = await this.#c("pins");
5921
+ const sourcePin = await pins.findOne({ pageId: source.id }, {
5922
+ ...noProjection,
5923
+ session
5924
+ });
5925
+ if (sourcePin) {
5926
+ await this.#upsertPinAudience(session, target.id, sourcePin.audience ?? [], String(sourcePin.createdBy), now);
5927
+ await pins.deleteOne({ id: sourcePin.id }, { session });
5126
5928
  }
5127
- await this.#activity("node-merged", "node", source.id, source.scope, void 0, session);
5128
- await this.#outbox("node", source.id, "delete", input.sourceVersion + 1, source.scope, session);
5129
- await this.#outbox("node", target.id, "upsert", createKnowledgeUlid(), mergedTarget.scope, session);
5130
- return mergedTarget;
5929
+ await (await this.#c("activity")).updateMany({ pageId: source.id }, { $set: { pageScopeId: target.scopeId } }, { session });
5930
+ await this.#log(session, {
5931
+ action: "page-merged",
5932
+ scopeId: target.scopeId,
5933
+ pageScopeId: target.scopeId,
5934
+ pageId: source.id,
5935
+ provenance: input.provenance,
5936
+ at: now
5937
+ });
5938
+ return await this.#page(target.id, session);
5131
5939
  });
5132
5940
  }
5133
- async appendKnowledge(input) {
5134
- await assertKnowledgeRecordTextWithinBoundCompat(input.text);
5135
- const scope = canonicalizeKnowledgeScope(input.scope);
5136
- const defaultScope = canonicalizeKnowledgeScope(input.defaultScope);
5137
- return this.#connector.withTransaction(async (session) => {
5138
- const parent = await this.#resolveTerminalNode(nodeReferenceId(input.node), session);
5139
- if (!parent) throw new KnowledgeNotFoundError("node", nodeReferenceId(input.node));
5140
- const id = input.id ?? createKnowledgeUlid();
5141
- const existing = await (await this.#knowledge()).findOne({ id }, sessionOptions(session));
5142
- if (existing) return recordFromDocument(existing);
5143
- const record = {
5144
- id,
5145
- node: parent.id,
5146
- text: input.text,
5147
- scope,
5148
- sourceThreadId: input.sourceThreadId,
5149
- capturedAt: /* @__PURE__ */ new Date(),
5150
- when: input.when,
5151
- metadata: input.metadata
5152
- };
5153
- await (await this.#knowledge()).insertOne({
5154
- ...record,
5155
- scopeKey: knowledgeScopeKey(scope),
5156
- when: record.when ?? null,
5157
- deletedAt: null,
5158
- deletedBy: null
5159
- }, sessionOptions(session));
5160
- await this.#replaceMentions("record", id, record.text, input.resolutionScope, defaultScope, session);
5161
- await this.#activity("record-created", "record", id, scope, input.sourceThreadId, session);
5162
- await this.#outbox("record", id, "upsert", createKnowledgeUlid(), scope, session);
5163
- return record;
5941
+ async deletePage(input) {
5942
+ const core = await loadPagesCore();
5943
+ return this.#tx(async (session) => {
5944
+ const page = await this.#requireLivePage(input.id, session);
5945
+ if (page.version !== input.expectedVersion) throw new KnowledgeConflictError(input.id);
5946
+ if (page.isScope) throw new Error(`Knowledge scope page ${input.id} can't be deleted`);
5947
+ const pages = await this.#c("pages");
5948
+ if (await pages.findOne({
5949
+ parentId: input.id,
5950
+ ...live
5951
+ }, {
5952
+ projection: { id: 1 },
5953
+ session
5954
+ })) throw new core.KnowledgePageHasChildrenError(input.id);
5955
+ const now = /* @__PURE__ */ new Date();
5956
+ const updated = await pages.findOneAndUpdate({
5957
+ id: input.id,
5958
+ version: input.expectedVersion,
5959
+ ...live
5960
+ }, {
5961
+ $set: {
5962
+ deletedAt: now,
5963
+ deletedBy: input.deletedBy,
5964
+ updatedAt: now,
5965
+ updatedBy: { author: input.deletedBy }
5966
+ },
5967
+ $unset: { activeKey: "" },
5968
+ $inc: { version: 1 }
5969
+ }, {
5970
+ returnDocument: "after",
5971
+ projection: { _id: 0 },
5972
+ session
5973
+ });
5974
+ if (!updated) throw new KnowledgeConflictError(input.id);
5975
+ await (await this.#c("pins")).deleteOne({ pageId: input.id }, { session });
5976
+ await this.#log(session, {
5977
+ action: "page-deleted",
5978
+ scopeId: page.scopeId,
5979
+ pageId: page.id,
5980
+ provenance: { author: input.deletedBy },
5981
+ at: now
5982
+ });
5983
+ return toPage(updated);
5164
5984
  });
5165
5985
  }
5166
- async getKnowledge(input) {
5167
- const row = await (await this.#knowledge()).findOne({
5986
+ async getNote(input) {
5987
+ const ids = await this.#readable(input.read);
5988
+ const filter = {
5168
5989
  id: input.id,
5169
- ...input.includeDeleted ? {} : { deletedAt: null }
5170
- });
5171
- return row ? recordFromDocument(row) : null;
5172
- }
5173
- async listKnowledgeAbout(input) {
5174
- return this.#queryKnowledge(input, "about");
5175
- }
5176
- async listKnowledgeMentioning(input) {
5177
- return this.#queryKnowledge(input, "mentioning");
5178
- }
5179
- async listKnowledgeRelatedTo(input) {
5180
- return this.#queryKnowledge(input, "related");
5181
- }
5182
- async knowledgeBySource(input) {
5183
- const scope = canonicalizeKnowledgeScope(input.scope);
5184
- const limit = input.limit ?? 100;
5185
- const rows = await (await this.#knowledge()).find({
5186
- sourceThreadId: input.sourceThreadId,
5187
- scopeKey: { $in: visibleScopeKeys(scope) },
5188
- ...input.includeDeleted ? {} : { deletedAt: null },
5189
- ...input.after ? { id: { $gt: input.after } } : {}
5190
- }).sort({ id: 1 }).limit(limit + 1).toArray();
5990
+ ...this.#visibleNotes(ids)
5991
+ };
5992
+ if (!input.includeDeleted) Object.assign(filter, live);
5993
+ const row = await (await this.#c("notes")).findOne(filter, noProjection);
5994
+ return row ? toNote(row) : null;
5995
+ }
5996
+ async listNotes(input) {
5997
+ const core = await loadPagesCore();
5998
+ const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
5999
+ const ids = await this.#readable(input.read);
6000
+ const filter = {
6001
+ pageId: input.pageId,
6002
+ ...this.#visibleNotes(ids)
6003
+ };
6004
+ if (!input.includeDeleted) Object.assign(filter, live);
6005
+ if (input.cursor) {
6006
+ const [createdAt, id] = decodeCursor(input.cursor);
6007
+ const at = new Date(createdAt);
6008
+ filter.$or = [{ createdAt: { $gt: at } }, {
6009
+ createdAt: at,
6010
+ id: { $gt: id }
6011
+ }];
6012
+ }
6013
+ const rows = await (await this.#c("notes")).find(filter, noProjection).sort({
6014
+ createdAt: 1,
6015
+ id: 1
6016
+ }).limit(limit + 1).toArray();
6017
+ return this.#noteOutput(rows, limit);
6018
+ }
6019
+ async countNotes(input) {
6020
+ const ids = [...new Set(input.pageIds)];
6021
+ const counts = Object.fromEntries(ids.map((id) => [id, 0]));
6022
+ if (!ids.length) return counts;
6023
+ const readable = await this.#readable(input.read);
6024
+ const rows = await (await this.#c("notes")).aggregate([{ $match: {
6025
+ pageId: { $in: ids },
6026
+ ...live,
6027
+ ...this.#visibleNotes(readable)
6028
+ } }, { $group: {
6029
+ _id: "$pageId",
6030
+ count: { $sum: 1 }
6031
+ } }]).toArray();
6032
+ for (const row of rows) counts[row._id] = row.count;
6033
+ return counts;
6034
+ }
6035
+ async listNotesMentioning(input) {
6036
+ const core = await loadPagesCore();
6037
+ const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
6038
+ const ids = await this.#readable(input.read);
6039
+ const match = {
6040
+ mentions: input.pageId,
6041
+ pageId: { $ne: input.pageId },
6042
+ ...live,
6043
+ ...this.#visibleNotes(ids)
6044
+ };
6045
+ if (input.cursor) {
6046
+ const [createdAt, id] = decodeCursor(input.cursor);
6047
+ const at = new Date(createdAt);
6048
+ match.$or = [{ createdAt: { $lt: at } }, {
6049
+ createdAt: at,
6050
+ id: { $lt: id }
6051
+ }];
6052
+ }
6053
+ const rows = await (await this.#c("notes")).aggregate([
6054
+ { $match: match },
6055
+ { $lookup: {
6056
+ from: MONGO_KNOWLEDGE_PAGES_COLLECTIONS.pages,
6057
+ localField: "pageId",
6058
+ foreignField: "id",
6059
+ as: "page"
6060
+ } },
6061
+ { $match: { "page.deletedAt": { $exists: false } } },
6062
+ { $project: {
6063
+ _id: 0,
6064
+ page: 0
6065
+ } },
6066
+ { $sort: {
6067
+ createdAt: -1,
6068
+ id: -1
6069
+ } },
6070
+ { $limit: limit + 1 }
6071
+ ]).toArray();
6072
+ return this.#noteOutput(rows, limit);
6073
+ }
6074
+ #noteOutput(rows, limit) {
6075
+ const notes = rows.slice(0, limit).map(toNote);
6076
+ const last = notes[notes.length - 1];
5191
6077
  return {
5192
- records: rows.slice(0, limit).map(recordFromDocument),
5193
- nextCursor: rows.length > limit ? rows[limit - 1]?.id : void 0
6078
+ notes,
6079
+ ...rows.length > limit && last ? { nextCursor: encodeCursor([last.createdAt.toISOString(), last.id]) } : {}
5194
6080
  };
5195
6081
  }
5196
- async removeKnowledge(input) {
5197
- return this.#connector.withTransaction(async (session) => {
5198
- const record = await this.#getKnowledge(input.id, true, session);
5199
- if (!record) throw new KnowledgeNotFoundError("record", input.id);
5200
- if (record.deletedAt) return record;
5201
- const deletedAt = /* @__PURE__ */ new Date();
5202
- await (await this.#knowledge()).updateOne({
5203
- id: input.id,
5204
- deletedAt: null
5205
- }, { $set: {
5206
- deletedAt,
5207
- deletedBy: input.deletedBy
5208
- } }, sessionOptions(session));
5209
- await this.#activity("record-deleted", "record", input.id, record.scope, record.sourceThreadId, session);
5210
- await this.#outbox("record", input.id, "delete", createKnowledgeUlid(), record.scope, session);
5211
- return {
5212
- ...record,
5213
- deletedAt,
5214
- deletedBy: input.deletedBy
6082
+ async #assertScopePages(ids, session) {
6083
+ const rows = await (await this.#c("pages")).find({
6084
+ id: { $in: ids },
6085
+ isScope: true
6086
+ }, {
6087
+ projection: {
6088
+ _id: 0,
6089
+ id: 1
6090
+ },
6091
+ session
6092
+ }).toArray();
6093
+ const found = new Set(rows.map((row) => String(row.id)));
6094
+ const missing = ids.find((id) => !found.has(id));
6095
+ if (missing) throw new Error(`Knowledge note stamp ${missing} is not a scope page`);
6096
+ }
6097
+ async addNote(input) {
6098
+ (await this.#bounds()).assertKnowledgeNoteTextWithinBound(input.text);
6099
+ if (input.stampScopeIds.length === 0) throw new Error("A Knowledge note needs at least one stamp scope");
6100
+ return this.#tx(async (session) => {
6101
+ const page = await this.#requireLivePage(input.pageId, session);
6102
+ const stampIds = [...new Set(input.stampScopeIds)];
6103
+ await this.#assertScopePages(stampIds, session);
6104
+ const mentionIds = [...new Set(input.mentions)];
6105
+ let mentionScopeIds = [];
6106
+ if (mentionIds.length) {
6107
+ const found = await (await this.#c("pages")).find({ id: { $in: mentionIds } }, {
6108
+ projection: {
6109
+ _id: 0,
6110
+ id: 1,
6111
+ scopeId: 1
6112
+ },
6113
+ session
6114
+ }).toArray();
6115
+ const known = new Set(found.map((row) => String(row.id)));
6116
+ const missing = mentionIds.find((id) => !known.has(id));
6117
+ if (missing) throw new KnowledgeNotFoundError("page", missing);
6118
+ mentionScopeIds = [...new Set(found.map((row) => String(row.scopeId)))];
6119
+ }
6120
+ const id = input.id ?? createKnowledgeUlid();
6121
+ const now = /* @__PURE__ */ new Date();
6122
+ const doc = {
6123
+ id,
6124
+ pageId: page.id,
6125
+ pageScopeId: page.scopeId,
6126
+ text: input.text,
6127
+ ...input.when ? { when: input.when } : {},
6128
+ stampScopeIds: stampIds,
6129
+ mentions: mentionIds,
6130
+ mentionScopeIds,
6131
+ provenance: input.provenance,
6132
+ version: 1,
6133
+ createdAt: now,
6134
+ updatedAt: now
5215
6135
  };
6136
+ try {
6137
+ await (await this.#c("notes")).insertOne({ ...doc }, { session });
6138
+ } catch (error) {
6139
+ if (isDuplicateKey(error)) throw new KnowledgeConflictError(id);
6140
+ throw error;
6141
+ }
6142
+ await this.#log(session, {
6143
+ action: "note-added",
6144
+ scopeId: page.scopeId,
6145
+ pageId: page.id,
6146
+ noteId: id,
6147
+ provenance: input.provenance,
6148
+ at: now
6149
+ });
6150
+ return toNote(doc);
5216
6151
  });
5217
6152
  }
5218
- async restoreKnowledge(input) {
5219
- return this.#connector.withTransaction(async (session) => {
5220
- const record = await this.#getKnowledge(input.id, true, session);
5221
- if (!record) throw new KnowledgeNotFoundError("record", input.id);
5222
- if (!record.deletedAt) return record;
5223
- await (await this.#knowledge()).updateOne({ id: input.id }, { $set: {
5224
- deletedAt: null,
5225
- deletedBy: null
5226
- } }, sessionOptions(session));
5227
- await this.#activity("record-restored", "record", input.id, record.scope, record.sourceThreadId, session);
5228
- await this.#outbox("record", input.id, "upsert", createKnowledgeUlid(), record.scope, session);
5229
- return {
5230
- ...record,
5231
- deletedAt: void 0,
5232
- deletedBy: void 0
5233
- };
6153
+ async #requireNote(id, session) {
6154
+ const row = await (await this.#c("notes")).findOne({ id }, {
6155
+ ...noProjection,
6156
+ session
6157
+ });
6158
+ if (!row) throw new KnowledgeNotFoundError("note", id);
6159
+ return row;
6160
+ }
6161
+ async restampNote(input) {
6162
+ if (input.stampScopeIds.length === 0) throw new Error("A Knowledge note needs at least one stamp scope");
6163
+ return this.#tx(async (session) => {
6164
+ const note = await this.#requireNote(input.id, session);
6165
+ if (note.deletedAt) throw new KnowledgeNotFoundError("note", input.id);
6166
+ if (Number(note.version) !== input.expectedVersion) throw new KnowledgeConflictError(input.id);
6167
+ const stampIds = [...new Set(input.stampScopeIds)];
6168
+ await this.#assertScopePages(stampIds, session);
6169
+ const now = /* @__PURE__ */ new Date();
6170
+ const updated = await (await this.#c("notes")).findOneAndUpdate({
6171
+ id: input.id,
6172
+ version: input.expectedVersion
6173
+ }, {
6174
+ $set: {
6175
+ stampScopeIds: stampIds,
6176
+ updatedAt: now
6177
+ },
6178
+ $inc: { version: 1 }
6179
+ }, {
6180
+ returnDocument: "after",
6181
+ projection: { _id: 0 },
6182
+ session
6183
+ });
6184
+ if (!updated) throw new KnowledgeConflictError(input.id);
6185
+ await this.#log(session, {
6186
+ action: "note-restamped",
6187
+ scopeId: String(note.pageScopeId),
6188
+ pageId: String(note.pageId),
6189
+ noteId: input.id,
6190
+ provenance: input.provenance,
6191
+ at: now
6192
+ });
6193
+ return toNote(updated);
5234
6194
  });
5235
6195
  }
5236
- async rescopeKnowledge(input) {
5237
- const scope = canonicalizeKnowledgeScope(input.scope);
5238
- return this.#connector.withTransaction(async (session) => {
5239
- const record = await this.#getKnowledge(input.id, true, session);
5240
- if (!record) throw new KnowledgeNotFoundError("record", input.id);
5241
- await (await this.#knowledge()).updateOne({ id: input.id }, { $set: {
5242
- scope,
5243
- scopeKey: knowledgeScopeKey(scope)
5244
- } }, sessionOptions(session));
5245
- await this.#activity("record-rescoped", "record", input.id, scope, record.sourceThreadId, session);
5246
- await this.#outbox("record", input.id, "delete", createKnowledgeUlid(), record.scope, session);
5247
- if (!record.deletedAt) await this.#outbox("record", input.id, "upsert", createKnowledgeUlid(), scope, session);
5248
- return {
5249
- ...record,
5250
- scope
5251
- };
6196
+ async removeNote(input) {
6197
+ return this.#tx(async (session) => {
6198
+ const note = await this.#requireNote(input.id, session);
6199
+ if (note.deletedAt) return toNote(note);
6200
+ const now = /* @__PURE__ */ new Date();
6201
+ const updated = await (await this.#c("notes")).findOneAndUpdate({ id: input.id }, {
6202
+ $set: {
6203
+ deletedAt: now,
6204
+ deletedBy: input.deletedBy,
6205
+ updatedAt: now
6206
+ },
6207
+ $inc: { version: 1 }
6208
+ }, {
6209
+ returnDocument: "after",
6210
+ projection: { _id: 0 },
6211
+ session
6212
+ });
6213
+ await this.#log(session, {
6214
+ action: "note-removed",
6215
+ scopeId: String(note.pageScopeId),
6216
+ pageId: String(note.pageId),
6217
+ noteId: input.id,
6218
+ provenance: input.provenance,
6219
+ at: now
6220
+ });
6221
+ return toNote(updated);
5252
6222
  });
5253
6223
  }
5254
- async search(input) {
5255
- const scope = canonicalizeKnowledgeScope(input.scope);
5256
- const query = input.query.trim();
5257
- if (!query) return [];
5258
- const regex = new RegExp(this.#escapeRegex(query), "i");
5259
- const limit = input.limit ?? 20;
5260
- const results = (await (await this.#nodes()).find({
5261
- mergedInto: null,
5262
- scopeKey: { $in: visibleScopeKeys(scope) },
5263
- $or: [
5264
- { name: regex },
5265
- { kind: regex },
5266
- { content: regex },
5267
- { description: regex }
5268
- ]
5269
- }).sort({ updatedAt: -1 }).limit(limit).toArray()).map((row) => ({
5270
- type: "node",
5271
- id: row.id,
5272
- recordId: row.id,
5273
- name: row.name,
5274
- text: [
5275
- row.name,
5276
- ...row.description ? [row.description] : [],
5277
- ...row.content ? [row.content] : []
5278
- ].join("\n"),
5279
- scope: cloneScope(row.scope)
6224
+ async restoreNote(input) {
6225
+ return this.#tx(async (session) => {
6226
+ const note = await this.#requireNote(input.id, session);
6227
+ if (!note.deletedAt) return toNote(note);
6228
+ const now = /* @__PURE__ */ new Date();
6229
+ const updated = await (await this.#c("notes")).findOneAndUpdate({ id: input.id }, {
6230
+ $set: { updatedAt: now },
6231
+ $unset: {
6232
+ deletedAt: "",
6233
+ deletedBy: ""
6234
+ },
6235
+ $inc: { version: 1 }
6236
+ }, {
6237
+ returnDocument: "after",
6238
+ projection: { _id: 0 },
6239
+ session
6240
+ });
6241
+ await this.#log(session, {
6242
+ action: "note-restored",
6243
+ scopeId: String(note.pageScopeId),
6244
+ pageId: String(note.pageId),
6245
+ noteId: input.id,
6246
+ provenance: input.provenance,
6247
+ at: now
6248
+ });
6249
+ return toNote(updated);
6250
+ });
6251
+ }
6252
+ async searchPages(input) {
6253
+ const core = await loadPagesCore();
6254
+ const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
6255
+ const words = knowledgeSearchWords(input.query);
6256
+ if (!words.length) return [];
6257
+ const phrase = input.query.trim().toLowerCase().replace(/\s+/g, " ");
6258
+ const ids = await this.#readable(input.read);
6259
+ const scopes = input.scopeIds?.length ? input.scopeIds : void 0;
6260
+ const inScope = (field) => {
6261
+ if (ids === null) return scopes ? { [field]: { $in: scopes } } : {};
6262
+ const allowed = scopes ? scopes.filter((id) => ids.includes(id)) : ids;
6263
+ return { [field]: { $in: allowed } };
6264
+ };
6265
+ const matcher = (word) => ({
6266
+ $regex: escapeRegex(word),
6267
+ $options: "i"
6268
+ });
6269
+ const pages = await this.#c("pages");
6270
+ const pageFilter = {
6271
+ ...live,
6272
+ ...notScope,
6273
+ ...inScope("scopeId"),
6274
+ $and: words.map((word) => ({ $or: [
6275
+ { name: matcher(word) },
6276
+ { description: matcher(word) },
6277
+ { body: matcher(word) }
6278
+ ] }))
6279
+ };
6280
+ const hits = (await pages.aggregate([
6281
+ { $match: pageFilter },
6282
+ { $addFields: { searchTier: { $cond: [
6283
+ { $regexMatch: {
6284
+ input: "$canonicalName",
6285
+ regex: escapeRegex(phrase)
6286
+ } },
6287
+ 0,
6288
+ { $cond: [
6289
+ { $and: words.map((word) => ({ $regexMatch: {
6290
+ input: "$name",
6291
+ regex: escapeRegex(word),
6292
+ options: "i"
6293
+ } })) },
6294
+ 1,
6295
+ 2
6296
+ ] }
6297
+ ] } } },
6298
+ { $sort: {
6299
+ searchTier: 1,
6300
+ updatedAt: -1,
6301
+ id: 1
6302
+ } },
6303
+ { $limit: limit },
6304
+ { $project: {
6305
+ searchTier: 0,
6306
+ _id: 0
6307
+ } }
6308
+ ]).toArray()).map((row) => ({
6309
+ type: "page",
6310
+ page: toPage(row)
5280
6311
  }));
5281
- if (results.length < limit) {
5282
- const records = await (await this.#knowledge()).find({
5283
- deletedAt: null,
5284
- scopeKey: { $in: visibleScopeKeys(scope) },
5285
- text: regex
5286
- }).sort({ id: -1 }).limit(limit - results.length).toArray();
5287
- for (const record of records) {
5288
- const parent = await this.#resolveTerminalNode(record.node);
5289
- const parentVisible = parent && isKnowledgeScopeVisible(parent.scope, scope);
5290
- results.push({
5291
- type: "record",
5292
- id: record.id,
5293
- recordId: parentVisible ? record.node : record.id,
5294
- name: parentVisible ? parent.name : "(private node)",
5295
- text: record.text,
5296
- scope: cloneScope(record.scope)
5297
- });
5298
- }
6312
+ if (hits.length >= limit) return hits;
6313
+ const noteRows = await (await this.#c("notes")).find({
6314
+ ...live,
6315
+ ...this.#visibleNotes(ids),
6316
+ ...scopes ? { pageScopeId: { $in: ids === null ? scopes : scopes.filter((id) => ids.includes(id)) } } : {},
6317
+ $and: words.map((word) => ({ text: matcher(word) }))
6318
+ }, noProjection).sort({
6319
+ updatedAt: -1,
6320
+ id: -1
6321
+ }).limit(limit - hits.length).toArray();
6322
+ const notePages = new Map((await pages.find({
6323
+ id: { $in: [...new Set(noteRows.map((row) => String(row.pageId)))] },
6324
+ ...live
6325
+ }, noProjection).toArray()).map((row) => [String(row.id), toPage(row)]));
6326
+ for (const row of noteRows) {
6327
+ const page = notePages.get(String(row.pageId));
6328
+ if (page) hits.push({
6329
+ type: "note",
6330
+ note: toNote(row),
6331
+ page
6332
+ });
5299
6333
  }
5300
- return results.slice(0, limit);
5301
- }
5302
- /**
5303
- * @deprecated Curation cursors were removed. Observation-time curate is the only Knowledge writer and needs no
5304
- * cursor. Always throws.
5305
- */
5306
- async getCurationCursor(_input) {
5307
- throw new Error(KNOWLEDGE_CURATION_CURSOR_REMOVED_MESSAGE);
6334
+ return hits;
5308
6335
  }
5309
- /**
5310
- * @deprecated Curation cursors were removed. Observation-time curate is the only Knowledge writer and needs no
5311
- * cursor. Always throws.
5312
- */
5313
- async advanceCurationCursor(_input) {
5314
- throw new Error(KNOWLEDGE_CURATION_CURSOR_REMOVED_MESSAGE);
5315
- }
5316
- async listActivity(input) {
5317
- const scope = canonicalizeKnowledgeScope(input.scope);
5318
- return (await (await this.#activityCollection()).find({
5319
- scopeKey: { $in: visibleScopeKeys(scope) },
5320
- ...input.after ? { id: { $lt: input.after } } : {}
5321
- }).sort({ id: -1 }).limit(input.limit ?? 100).toArray()).map((row) => ({
5322
- id: row.id,
5323
- action: row.action,
5324
- recordType: row.recordType,
5325
- recordId: row.recordId,
5326
- scope: cloneScope(row.scope),
5327
- sourceThreadId: row.sourceThreadId ?? void 0,
6336
+ async listPageActivity(input) {
6337
+ const core = await loadPagesCore();
6338
+ const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
6339
+ const ids = await this.#readable(input.read);
6340
+ const activity = await this.#c("activity");
6341
+ const notes = await this.#c("notes");
6342
+ const filter = {};
6343
+ if (ids !== null) filter.pageScopeId = { $in: ids };
6344
+ delete filter.scopeId;
6345
+ if (input.scopeId) {
6346
+ if (ids !== null && !ids.includes(input.scopeId)) filter.pageScopeId = { $in: [] };
6347
+ else filter.pageScopeId = input.scopeId;
6348
+ }
6349
+ if (input.pageId) filter.pageId = input.pageId;
6350
+ if (input.cursor) {
6351
+ const [createdAt, id] = decodeCursor(input.cursor);
6352
+ const at = new Date(createdAt);
6353
+ filter.$or = [{ createdAt: { $lt: at } }, {
6354
+ createdAt: at,
6355
+ id: { $lt: id }
6356
+ }];
6357
+ }
6358
+ const events = [];
6359
+ let cursorFilter = filter;
6360
+ for (;;) {
6361
+ const batch = await activity.find(cursorFilter, noProjection).sort({
6362
+ createdAt: -1,
6363
+ id: -1
6364
+ }).limit(limit * 4 + 1).toArray();
6365
+ const noteIds = [...new Set(batch.flatMap((row) => row.noteId ? [String(row.noteId)] : []))];
6366
+ const visible = new Set(noteIds.length ? (await notes.find({
6367
+ id: { $in: noteIds },
6368
+ ...this.#visibleNotes(ids)
6369
+ }, { projection: {
6370
+ _id: 0,
6371
+ id: 1
6372
+ } }).toArray()).map((row) => String(row.id)) : []);
6373
+ for (const row of batch) {
6374
+ if (row.noteId && !visible.has(String(row.noteId))) continue;
6375
+ events.push(row);
6376
+ if (events.length > limit) break;
6377
+ }
6378
+ if (events.length > limit || batch.length <= limit * 4) break;
6379
+ const last = batch[batch.length - 1];
6380
+ cursorFilter = {
6381
+ ...filter,
6382
+ $or: [{ createdAt: { $lt: last.createdAt } }, {
6383
+ createdAt: last.createdAt,
6384
+ id: { $lt: last.id }
6385
+ }]
6386
+ };
6387
+ }
6388
+ const page = events.slice(0, limit).map((row) => ({
6389
+ id: String(row.id),
6390
+ action: String(row.action),
6391
+ scopeId: String(row.scopeId),
6392
+ pageId: String(row.pageId),
6393
+ ...row.noteId == null ? {} : { noteId: String(row.noteId) },
6394
+ provenance: row.provenance,
5328
6395
  createdAt: new Date(row.createdAt)
5329
6396
  }));
5330
- }
5331
- async listSemanticOutbox(input = {}) {
5332
- const filter = {
5333
- ...input.status ? { status: input.status } : {},
5334
- ...input.scope ? { scopeKey: { $in: visibleScopeKeys(input.scope) } } : {}
6397
+ const last = page[page.length - 1];
6398
+ return {
6399
+ events: page,
6400
+ ...events.length > limit && last ? { nextCursor: encodeCursor([last.createdAt.toISOString(), last.id]) } : {}
5335
6401
  };
5336
- return (await (await this.#outboxCollection()).find(filter).sort({
5337
- createdAt: 1,
5338
- id: 1
5339
- }).limit(input.limit ?? 100).toArray()).map(outboxFromDocument);
5340
6402
  }
5341
- async claimSemanticOutbox(input) {
5342
- return this.#connector.withTransaction(async (session) => {
5343
- const collection = await this.#outboxCollection();
5344
- const now = input.now ?? /* @__PURE__ */ new Date();
5345
- const staleBefore = new Date(now.getTime() - (input.claimTimeoutMs ?? 6e4));
5346
- const filter = {
5347
- $or: [{
5348
- status: "pending",
5349
- availableAt: { $lte: now }
5350
- }, {
5351
- status: "processing",
5352
- claimedAt: { $lte: staleBefore }
5353
- }],
5354
- ...input.scope ? { scopeKey: { $in: visibleScopeKeys(input.scope) } } : {}
6403
+ /** Deletes every document but keeps the collections, indexes and schema-version marker. */
6404
+ async clearAll() {
6405
+ await this.#tx(async (session) => {
6406
+ for (const name of [
6407
+ "activity",
6408
+ "pins",
6409
+ "notes",
6410
+ "pages"
6411
+ ]) await (await this.#c(name)).deleteMany({}, { session });
6412
+ });
6413
+ }
6414
+ async addPinAudience(input) {
6415
+ return this.#tx(async (session) => {
6416
+ const page = await this.#requireLivePage(input.pageId, session);
6417
+ const now = /* @__PURE__ */ new Date();
6418
+ const { pin, changed } = await this.#upsertPinAudience(session, page.id, input.audience, input.createdBy, now);
6419
+ if (changed) await this.#log(session, {
6420
+ action: "pin-added",
6421
+ scopeId: page.scopeId,
6422
+ pageId: page.id,
6423
+ provenance: { author: input.createdBy },
6424
+ at: now
6425
+ });
6426
+ return pin;
6427
+ });
6428
+ }
6429
+ async #upsertPinAudience(session, pageId, audience, createdBy, now) {
6430
+ const pins = await this.#c("pins");
6431
+ const existing = await pins.findOne({ pageId }, {
6432
+ ...noProjection,
6433
+ session
6434
+ });
6435
+ if (!existing) {
6436
+ const doc = {
6437
+ id: createKnowledgeUlid(),
6438
+ pageId,
6439
+ audience: [...new Set(audience)],
6440
+ createdBy,
6441
+ createdAt: now,
6442
+ updatedAt: now
5355
6443
  };
5356
- const limit = input.limit ?? 100;
5357
- const candidates = await collection.find(filter, sessionOptions(session)).sort({
5358
- createdAt: 1,
6444
+ try {
6445
+ await pins.insertOne({ ...doc }, { session });
6446
+ } catch (error) {
6447
+ if (isDuplicateKey(error)) throw new KnowledgeConflictError(pageId);
6448
+ throw error;
6449
+ }
6450
+ return {
6451
+ pin: toPin(doc),
6452
+ changed: true
6453
+ };
6454
+ }
6455
+ const current = existing.audience ?? [];
6456
+ const merged = [.../* @__PURE__ */ new Set([...current, ...audience])];
6457
+ if (merged.length === current.length) return {
6458
+ pin: toPin(existing),
6459
+ changed: false
6460
+ };
6461
+ return {
6462
+ pin: toPin(await pins.findOneAndUpdate({ id: existing.id }, { $set: {
6463
+ audience: merged,
6464
+ updatedAt: now
6465
+ } }, {
6466
+ returnDocument: "after",
6467
+ projection: { _id: 0 },
6468
+ session
6469
+ })),
6470
+ changed: true
6471
+ };
6472
+ }
6473
+ async removePinAudience(input) {
6474
+ return this.#tx(async (session) => {
6475
+ const pins = await this.#c("pins");
6476
+ const existing = await pins.findOne({ pageId: input.pageId }, {
6477
+ ...noProjection,
6478
+ session
6479
+ });
6480
+ if (!existing) return null;
6481
+ const current = existing.audience ?? [];
6482
+ const remaining = current.filter((entry) => !input.audience.includes(entry));
6483
+ if (remaining.length === current.length) return toPin(existing);
6484
+ const now = /* @__PURE__ */ new Date();
6485
+ let pin = null;
6486
+ if (remaining.length === 0) await pins.deleteOne({ id: existing.id }, { session });
6487
+ else pin = toPin(await pins.findOneAndUpdate({ id: existing.id }, { $set: {
6488
+ audience: remaining,
6489
+ updatedAt: now
6490
+ } }, {
6491
+ returnDocument: "after",
6492
+ projection: { _id: 0 },
6493
+ session
6494
+ }));
6495
+ const page = await this.#page(input.pageId, session);
6496
+ await this.#log(session, {
6497
+ action: "pin-removed",
6498
+ scopeId: page?.scopeId ?? input.pageId,
6499
+ pageId: input.pageId,
6500
+ provenance: { author: input.removedBy },
6501
+ at: now
6502
+ });
6503
+ return pin;
6504
+ });
6505
+ }
6506
+ async getPages(input) {
6507
+ if (!input.ids.length) return [];
6508
+ const rows = await (await this.#c("pages")).find({
6509
+ id: { $in: [...new Set(input.ids)] },
6510
+ ...live
6511
+ }, noProjection).toArray();
6512
+ if (input.read === "all") return rows.map(toPage);
6513
+ const read = input.read;
6514
+ const scopeIds = [...new Set(rows.map((row) => String(row.scopeId)))];
6515
+ const readable = /* @__PURE__ */ new Set();
6516
+ const any = this.#readableClauses(read);
6517
+ if (scopeIds.length && any.length) {
6518
+ const scopes = await (await this.#c("pages")).find({
6519
+ id: { $in: scopeIds },
6520
+ isScope: true,
6521
+ $or: any
6522
+ }, { projection: {
6523
+ _id: 0,
5359
6524
  id: 1
5360
- }).limit(Math.max(limit * 10, 100)).toArray();
5361
- const claimed = [];
5362
- for (const candidate of candidates) {
5363
- if (claimed.length >= limit) break;
5364
- if (await collection.findOne({
5365
- documentId: candidate.documentId,
5366
- status: { $ne: "completed" },
5367
- $or: [{ createdAt: { $lt: candidate.createdAt } }, {
5368
- createdAt: candidate.createdAt,
5369
- id: { $lt: candidate.id }
5370
- }]
5371
- }, sessionOptions(session))) continue;
5372
- const result = await collection.findOneAndUpdate({
5373
- id: candidate.id,
5374
- $or: [{
5375
- status: "pending",
5376
- availableAt: { $lte: now }
5377
- }, {
5378
- status: "processing",
5379
- claimedAt: { $lte: staleBefore }
5380
- }]
5381
- }, {
5382
- $set: {
5383
- status: "processing",
5384
- claimedAt: now,
5385
- claimedBy: input.workerId
5386
- },
5387
- $inc: { attempts: 1 }
5388
- }, {
5389
- ...sessionOptions(session),
5390
- returnDocument: "after"
5391
- });
5392
- if (result) claimed.push(result);
6525
+ } }).toArray();
6526
+ for (const scope of scopes) readable.add(String(scope.id));
6527
+ }
6528
+ return rows.filter((row) => readable.has(String(row.scopeId))).map(toPage);
6529
+ }
6530
+ async listPins(input) {
6531
+ const core = await loadPagesCore();
6532
+ const limit = clampLimit(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
6533
+ const ids = await this.#readable(input.read);
6534
+ const match = {};
6535
+ if (input.audience !== "all") match.audience = { $in: input.audience };
6536
+ const pageId = {};
6537
+ if (input.pageIds) pageId.$in = input.pageIds;
6538
+ if (input.excludePageIds?.length) pageId.$nin = input.excludePageIds;
6539
+ if (Object.keys(pageId).length) match.pageId = pageId;
6540
+ const pins = await this.#c("pins");
6541
+ const base = [
6542
+ { $match: match },
6543
+ { $lookup: {
6544
+ from: MONGO_KNOWLEDGE_PAGES_COLLECTIONS.pages,
6545
+ localField: "pageId",
6546
+ foreignField: "id",
6547
+ as: "page"
6548
+ } },
6549
+ { $match: {
6550
+ "page.deletedAt": { $exists: false },
6551
+ ...ids === null ? {} : { "page.scopeId": { $in: ids } },
6552
+ ...input.withBody ? { "page.body": { $regex: "\\S" } } : {}
6553
+ } }
6554
+ ];
6555
+ const pageMatch = [];
6556
+ if (input.cursor) {
6557
+ const [createdAt, id] = decodeCursor(input.cursor);
6558
+ const at = new Date(createdAt);
6559
+ pageMatch.push({ $match: { $or: [{ createdAt: { $lt: at } }, {
6560
+ createdAt: at,
6561
+ id: { $lt: id }
6562
+ }] } });
6563
+ }
6564
+ const rows = await pins.aggregate([
6565
+ ...base,
6566
+ ...pageMatch,
6567
+ { $addFields: { pageName: { $arrayElemAt: ["$page.name", 0] } } },
6568
+ { $project: {
6569
+ _id: 0,
6570
+ page: 0
6571
+ } },
6572
+ { $sort: {
6573
+ createdAt: -1,
6574
+ id: -1
6575
+ } },
6576
+ { $limit: limit + 1 }
6577
+ ]).toArray();
6578
+ let total;
6579
+ if (input.count) {
6580
+ const counted = await pins.aggregate([...base, { $count: "total" }]).toArray();
6581
+ total = Number(counted[0]?.total ?? 0);
6582
+ }
6583
+ const listed = rows.slice(0, limit).map(toListedPin);
6584
+ const last = listed[listed.length - 1];
6585
+ return {
6586
+ pins: listed,
6587
+ ...rows.length > limit && last ? { nextCursor: encodeCursor([last.createdAt.toISOString(), last.id]) } : {},
6588
+ ...total === void 0 ? {} : { total }
6589
+ };
6590
+ }
6591
+ };
6592
+ // src/storage/domains/knowledge/index.ts
6593
+ const loadKnowledgePagesCore = createKnowledgePagesCoreLoader(coreFeatures, () => import("@mastra/core/storage"));
6594
+ /** @experimental Knowledge APIs are experimental and may change without notice. */
6595
+ var KnowledgeMongoDB = class extends KnowledgeStorage {
6596
+ static MANAGED_COLLECTIONS = Object.values(MONGO_KNOWLEDGE_PAGES_COLLECTIONS);
6597
+ #connector;
6598
+ #pages;
6599
+ constructor(config) {
6600
+ super();
6601
+ this.#connector = resolveMongoDBConfig(config);
6602
+ this.#pages = new MongoKnowledgePages({
6603
+ connector: this.#connector,
6604
+ bounds: async () => {
6605
+ const core = await loadKnowledgePagesCore();
6606
+ return {
6607
+ assertKnowledgePageDescriptionWithinBound: core.assertKnowledgePageDescriptionWithinBound,
6608
+ assertKnowledgeNoteTextWithinBound: core.assertKnowledgeNoteTextWithinBound
6609
+ };
5393
6610
  }
5394
- return claimed.map(outboxFromDocument);
5395
6611
  });
5396
6612
  }
5397
- async completeSemanticOutbox(input) {
5398
- if (!input.ids.length) return;
5399
- await (await this.#outboxCollection()).updateMany({
5400
- id: { $in: input.ids },
5401
- status: "processing",
5402
- claimedBy: input.workerId
5403
- }, { $set: {
5404
- status: "completed",
5405
- completedAt: /* @__PURE__ */ new Date(),
5406
- claimedAt: null,
5407
- claimedBy: null
5408
- } });
5409
- }
5410
- async releaseSemanticOutbox(input) {
5411
- if (!input.ids.length) return;
5412
- await (await this.#outboxCollection()).updateMany({
5413
- id: { $in: input.ids },
5414
- status: "processing",
5415
- claimedBy: input.workerId
5416
- }, { $set: {
5417
- status: "pending",
5418
- availableAt: input.retryAt ?? /* @__PURE__ */ new Date(),
5419
- claimedAt: null,
5420
- claimedBy: null
5421
- } });
6613
+ getCapabilities() {
6614
+ return {
6615
+ ...super.getCapabilities(),
6616
+ schemaVersion: 4,
6617
+ supportsSchemaInspection: true,
6618
+ supportsExplicitReset: true
6619
+ };
5422
6620
  }
5423
- async #collection(name) {
5424
- return this.#connector.getCollection(name);
6621
+ async init() {
6622
+ await this.#pages.init();
5425
6623
  }
5426
- #nodes() {
5427
- return this.#collection(TABLE_KNOWLEDGE_NODES);
6624
+ async inspectSchema() {
6625
+ return this.#pages.inspectSchema();
5428
6626
  }
5429
- #knowledge() {
5430
- return this.#collection(TABLE_KNOWLEDGE_RECORDS);
6627
+ async dangerouslyReset() {
6628
+ await this.#pages.reset();
5431
6629
  }
5432
- #mentions() {
5433
- return this.#collection(TABLE_KNOWLEDGE_MENTIONS);
6630
+ async dangerouslyClearAll() {
6631
+ await this.#pages.clearAll();
5434
6632
  }
5435
- #activityCollection() {
5436
- return this.#collection(TABLE_KNOWLEDGE_ACTIVITY);
6633
+ materializeScope(input) {
6634
+ return this.#pages.materializeScope(input);
5437
6635
  }
5438
- #outboxCollection() {
5439
- return this.#collection(TABLE_KNOWLEDGE_SEMANTIC_OUTBOX);
6636
+ getScopesByAddress(input) {
6637
+ return this.#pages.getScopesByAddress(input);
5440
6638
  }
5441
- async #getNode(id, session) {
5442
- const row = await (await this.#nodes()).findOne({
5443
- id,
5444
- type: "node"
5445
- }, sessionOptions(session));
5446
- return row ? nodeFromDocument(row) : null;
5447
- }
5448
- async #getNodeByName(name, scope, session) {
5449
- const row = await (await this.#nodes()).findOne({
5450
- type: "node",
5451
- scopeKey: knowledgeScopeKey(scope),
5452
- canonicalName: canonicalName(name)
5453
- }, sessionOptions(session));
5454
- return row ? nodeFromDocument(row) : null;
5455
- }
5456
- async #resolveNode(name, scope, session) {
5457
- const candidates = (await (await this.#nodes()).find({
5458
- type: "node",
5459
- canonicalName: canonicalName(name),
5460
- $or: [{ scopeKey: { $in: visibleScopeKeys(scope) } }, { mergedInto: { $ne: null } }]
5461
- }, sessionOptions(session)).toArray()).map(nodeFromDocument).sort((left, right) => right.scope.length - left.scope.length);
5462
- for (const candidate of candidates) {
5463
- const terminal = await this.#resolveTerminalNode(candidate.id, session);
5464
- if (terminal && isKnowledgeScopeVisible(terminal.scope, scope)) return terminal;
5465
- }
5466
- return null;
5467
- }
5468
- async #resolveTerminalNode(id, session) {
5469
- let node = await this.#getNode(id, session);
5470
- const seen = /* @__PURE__ */ new Set();
5471
- while (node?.mergedInto) {
5472
- if (seen.has(node.id)) throw new Error(`Knowledge merge cycle detected at ${node.id}`);
5473
- seen.add(node.id);
5474
- node = await this.#getNode(node.mergedInto, session);
5475
- }
5476
- return node;
6639
+ listScopeDeclarations() {
6640
+ return this.#pages.listScopeDeclarations();
5477
6641
  }
5478
- async #getPageByExactName(name, scope, session) {
5479
- const row = await (await this.#nodes()).findOne({
5480
- type: "page",
5481
- scopeKey: knowledgeScopeKey(scope),
5482
- canonicalName: canonicalName(name)
5483
- }, sessionOptions(session));
5484
- return row ? nodeFromDocument(row) : null;
6642
+ renameScopeDeclaration(input) {
6643
+ return this.#pages.renameScopeDeclaration(input);
5485
6644
  }
5486
- async #getKnowledge(id, includeDeleted, session) {
5487
- const row = await (await this.#knowledge()).findOne({
5488
- id,
5489
- ...includeDeleted ? {} : { deletedAt: null }
5490
- }, sessionOptions(session));
5491
- return row ? recordFromDocument(row) : null;
5492
- }
5493
- async #queryKnowledge(input, relationship) {
5494
- const scope = canonicalizeKnowledgeScope(input.scope);
5495
- const node = await this.#resolveTerminalNode(nodeReferenceId(input.node));
5496
- if (!node) return { records: [] };
5497
- const nodeIds = [node.id];
5498
- if (relationship !== "about") {
5499
- const mentions = await (await this.#mentions()).find({
5500
- recordId: node.id,
5501
- sourceType: "record"
5502
- }).toArray();
5503
- nodeIds.push(...mentions.map((row) => row.sourceId));
5504
- }
5505
- const limit = input.limit ?? 100;
5506
- const filter = relationship === "about" ? { node: node.id } : relationship === "mentioning" ? { id: { $in: nodeIds.slice(1) } } : { $or: [{ node: node.id }, { id: { $in: nodeIds.slice(1) } }] };
5507
- Object.assign(filter, {
5508
- scopeKey: { $in: visibleScopeKeys(scope) },
5509
- ...input.includeDeleted ? {} : { deletedAt: null },
5510
- ...input.after ? { id: { $lt: input.after } } : {}
5511
- });
5512
- const rows = await (await this.#knowledge()).find(filter).sort({ id: -1 }).limit(limit + 1).toArray();
5513
- return {
5514
- records: rows.slice(0, limit).map(recordFromDocument),
5515
- nextCursor: rows.length > limit ? rows[limit - 1]?.id : void 0
5516
- };
6645
+ listChildScopes(input) {
6646
+ return this.#pages.listChildScopes(input);
5517
6647
  }
5518
- async #replaceMentions(sourceType, sourceId, text, resolutionScope, defaultScope, session) {
5519
- const mentions = await this.#mentions();
5520
- await mentions.deleteMany({
5521
- sourceType,
5522
- sourceId
5523
- }, sessionOptions(session));
5524
- for (const name of parseKnowledgeWikilinks(text)) {
5525
- let node = await this.#resolveNode(name, resolutionScope, session);
5526
- if (!node) {
5527
- const existing = await this.#getNodeByName(name, defaultScope, session);
5528
- node = existing ? await this.#resolveTerminalNode(existing.id, session) : null;
5529
- }
5530
- if (!node) {
5531
- const now = /* @__PURE__ */ new Date();
5532
- node = {
5533
- id: crypto.randomUUID(),
5534
- type: "node",
5535
- name,
5536
- kind: "node",
5537
- scope: defaultScope,
5538
- version: 1,
5539
- createdAt: now,
5540
- updatedAt: now
5541
- };
5542
- try {
5543
- await (await this.#nodes()).insertOne({
5544
- ...node,
5545
- canonicalName: canonicalName(name),
5546
- scopeKey: knowledgeScopeKey(defaultScope),
5547
- mergedInto: null
5548
- }, sessionOptions(session));
5549
- await this.#activity("node-created", "node", node.id, defaultScope, void 0, session);
5550
- await this.#outbox("node", node.id, "upsert", 1, defaultScope, session);
5551
- } catch (error) {
5552
- if (error.code !== 11e3) throw error;
5553
- node = await this.#getNodeByName(name, defaultScope, session);
5554
- if (!node) throw error;
5555
- }
5556
- }
5557
- await mentions.updateOne({
5558
- sourceType,
5559
- sourceId,
5560
- recordId: node.id
5561
- }, { $setOnInsert: {
5562
- sourceType,
5563
- sourceId,
5564
- recordId: node.id
5565
- } }, {
5566
- ...sessionOptions(session),
5567
- upsert: true
5568
- });
5569
- }
6648
+ getPages(input) {
6649
+ return this.#pages.getPages(input);
5570
6650
  }
5571
- async #activity(action, recordType, recordId, scope, sourceThreadId, session) {
5572
- await (await this.#activityCollection()).insertOne({
5573
- id: createKnowledgeUlid(),
5574
- action,
5575
- recordType,
5576
- recordId,
5577
- scope,
5578
- scopeKey: knowledgeScopeKey(scope),
5579
- sourceThreadId: sourceThreadId ?? null,
5580
- createdAt: /* @__PURE__ */ new Date()
5581
- }, sessionOptions(session));
5582
- }
5583
- async #outbox(documentType, id, operation, version, scope, session) {
5584
- const documentId = knowledgeSemanticDocumentId(documentType, id);
5585
- const idempotencyKey = knowledgeSemanticIdempotencyKey(documentId, operation, version);
5586
- const now = /* @__PURE__ */ new Date();
5587
- try {
5588
- await (await this.#outboxCollection()).insertOne({
5589
- id: createKnowledgeUlid(),
5590
- idempotencyKey,
5591
- documentId,
5592
- documentType,
5593
- operation,
5594
- scope,
5595
- scopeKey: knowledgeScopeKey(scope),
5596
- status: "pending",
5597
- attempts: 0,
5598
- availableAt: now,
5599
- claimedAt: null,
5600
- claimedBy: null,
5601
- createdAt: now,
5602
- completedAt: null
5603
- }, sessionOptions(session));
5604
- } catch (error) {
5605
- if (error.code !== 11e3) throw error;
5606
- }
6651
+ getPage(input) {
6652
+ return this.#pages.getPage(input);
6653
+ }
6654
+ getPageByPath(input) {
6655
+ return this.#pages.getPageByPath(input);
6656
+ }
6657
+ findPagesByName(input) {
6658
+ return this.#pages.findPagesByName(input);
6659
+ }
6660
+ listChildPages(input) {
6661
+ return this.#pages.listChildPages(input);
6662
+ }
6663
+ listPages(input) {
6664
+ return this.#pages.listPages(input);
6665
+ }
6666
+ createPage(input) {
6667
+ return this.#pages.createPage(input);
6668
+ }
6669
+ updatePage(input) {
6670
+ return this.#pages.updatePage(input);
6671
+ }
6672
+ movePage(input) {
6673
+ return this.#pages.movePage(input);
6674
+ }
6675
+ mergePages(input) {
6676
+ return this.#pages.mergePages(input);
6677
+ }
6678
+ deletePage(input) {
6679
+ return this.#pages.deletePage(input);
6680
+ }
6681
+ getNote(input) {
6682
+ return this.#pages.getNote(input);
6683
+ }
6684
+ listNotes(input) {
6685
+ return this.#pages.listNotes(input);
6686
+ }
6687
+ countNotes(input) {
6688
+ return this.#pages.countNotes(input);
6689
+ }
6690
+ listNotesMentioning(input) {
6691
+ return this.#pages.listNotesMentioning(input);
6692
+ }
6693
+ addNote(input) {
6694
+ return this.#pages.addNote(input);
6695
+ }
6696
+ restampNote(input) {
6697
+ return this.#pages.restampNote(input);
6698
+ }
6699
+ removeNote(input) {
6700
+ return this.#pages.removeNote(input);
6701
+ }
6702
+ restoreNote(input) {
6703
+ return this.#pages.restoreNote(input);
6704
+ }
6705
+ searchPages(input) {
6706
+ return this.#pages.searchPages(input);
6707
+ }
6708
+ listPageActivity(input) {
6709
+ return this.#pages.listPageActivity(input);
6710
+ }
6711
+ addPinAudience(input) {
6712
+ return this.#pages.addPinAudience(input);
6713
+ }
6714
+ removePinAudience(input) {
6715
+ return this.#pages.removePinAudience(input);
5607
6716
  }
5608
- #escapeRegex(value) {
5609
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6717
+ listPins(input) {
6718
+ return this.#pages.listPins(input);
5610
6719
  }
5611
6720
  };
5612
6721
  // src/storage/domains/mcp-clients/index.ts