@rizom/brain 0.2.0-alpha.694 → 0.2.0-alpha.695
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/brain.js +16 -16
- package/dist/chunks/{app-15nfqfss.js → app-vx1bjwc5.js} +2 -2
- package/dist/chunks/{index-xbh6e1rh.js → index-4jf2jzjs.js} +2 -2
- package/dist/chunks/{index-17rpw6kh.js → index-gd765s5q.js} +3 -3
- package/dist/chunks/{index-17rpw6kh.js.map → index-gd765s5q.js.map} +3 -3
- package/dist/chunks/{index-aadrhszz.js → index-gq9n2d3d.js} +2 -2
- package/dist/chunks/{index-2ksj6hdk.js → index-s5v5f614.js} +2 -2
- package/dist/chunks/{index-mp4q1mgj.js → index-xmxyszkz.js} +3 -3
- package/dist/chunks/{index-mp4q1mgj.js.map → index-xmxyszkz.js.map} +1 -1
- package/dist/chunks/{index-wzqxbtja.js → index-y81ng1rp.js} +2 -2
- package/dist/chunks/{linkedin-template-6kxt7ng4.js → linkedin-template-f7bt9fn1.js} +2 -2
- package/dist/chunks/{package-xpr15mjg.js → package-w0xfh9pe.js} +3 -3
- package/dist/chunks/{package-xpr15mjg.js.map → package-w0xfh9pe.js.map} +1 -1
- package/dist/entities.js +1 -1
- package/dist/index.js +1 -1
- package/dist/interfaces.js +1 -1
- package/dist/model.js +14 -14
- package/dist/model.js.map +1 -1
- package/dist/services.js +1 -1
- package/dist/testing.js +1 -1
- package/dist/ui/studio-chunks/main-x6wctnve.js.map +1 -1
- package/package.json +1 -1
- /package/dist/chunks/{app-15nfqfss.js.map → app-vx1bjwc5.js.map} +0 -0
- /package/dist/chunks/{index-xbh6e1rh.js.map → index-4jf2jzjs.js.map} +0 -0
- /package/dist/chunks/{index-aadrhszz.js.map → index-gq9n2d3d.js.map} +0 -0
- /package/dist/chunks/{index-2ksj6hdk.js.map → index-s5v5f614.js.map} +0 -0
- /package/dist/chunks/{index-wzqxbtja.js.map → index-y81ng1rp.js.map} +0 -0
- /package/dist/chunks/{linkedin-template-6kxt7ng4.js.map → linkedin-template-f7bt9fn1.js.map} +0 -0
package/dist/services.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import"./chunks/index-z0k97qgd.js";import{$g as a,Eh as k,Hi as l,Ii as n,ah as b,bh as c,oh as d,qh as g,th as h,uh as i,vh as j,xe as x}from"./chunks/index-
|
|
2
|
+
import"./chunks/index-z0k97qgd.js";import{$g as a,Eh as k,Hi as l,Ii as n,ah as b,bh as c,oh as d,qh as g,th as h,uh as i,vh as j,xe as x}from"./chunks/index-gd765s5q.js";import{Vl as f,Xl as m,Yl as p,km as t}from"./chunks/index-qnt46f4v.js";import"./chunks/index-yhsfzqsw.js";import"./chunks/index-g95msk2x.js";import{px as e}from"./chunks/index-q6a6w2ga.js";import"./chunks/index-x4qnbqvy.js";import"./chunks/index-edt6y87t.js";export{p as SdkError,x as contentGenerationResultSchema,d as defineAccountSettings,h as defineBusContract,t as defineChannel,a as defineDashboardWidget,c as defineEntityCatalog,i as defineJob,n as defineRoute,k as defineServicePlugin,b as defineStudioWorkspace,j as defineTool,g as defineWorkspaceAction,l as protocol,f as sdkErrorCodeSchema,m as sdkErrorSchema,e as z};
|
|
3
3
|
|
|
4
4
|
//# debugId=E81D80F380680E8964756E2164756E21
|
|
5
5
|
//# sourceMappingURL=services.js.map
|
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{O as k,P as V,Q as U}from"./chunks/index-
|
|
2
|
+
import{O as k,P as V,Q as U}from"./chunks/index-xmxyszkz.js";import{Ba as N,Ca as O,Da as z,Ea as M,Fa as q,Ga as J,Ha as L,Ja as _,Ka as j,R as E,U as I,ja as A,ka as x}from"./chunks/index-y81ng1rp.js";import{Ma as D}from"./chunks/index-s5v5f614.js";import{Ae as C,Kb as h,Lg as P,Mg as w,Qj as B}from"./chunks/index-gd765s5q.js";import{_k as p}from"./chunks/index-qnt46f4v.js";import{Xn as S,Yn as v}from"./chunks/index-yhsfzqsw.js";import"./chunks/index-g95msk2x.js";import"./chunks/index-q6a6w2ga.js";import"./chunks/index-x4qnbqvy.js";import"./chunks/index-edt6y87t.js";import{mkdtemp as K,rm as b}from"fs/promises";import{tmpdir as X}from"os";import{join as m}from"path";var y="none";function s(){return Promise.reject(Error("A test brain has no AI provider"))}function W(e){return{specificationVersion:"v3",provider:"test-brain",modelId:e,supportedUrls:{},doGenerate:s,doStream:s}}function T(e){let r={...e};return{generateText:s,generateObject:s,judge:s,generateImage:s,getModel:()=>W(r.model??"none"),canGenerateImages:()=>!1,getConfig:()=>({...r}),updateConfig:(o)=>{r={...r,...o}}}}function R(){return{identity:{model:"test:none",dimensions:1536,promptRevision:0},embedQuery:s,embedDocuments:s}}var Y="shell",ee=20;function te(e){if("needsConfirmation"in e)return{needsConfirmation:!0,summary:e.summary};if(!e.success)return{success:!1,error:e.error,...p({code:e.code})};return{success:!0,...p({data:e.data,message:e.message})}}function re(e,r){if(e.status==="completed"||e.status==="failed")return!0;return e.status==="pending"&&e.lastError!==null&&e.scheduledFor>r}function ne(e,r){return e.flatMap((o)=>{if(!D(o))throw Error("A test brain runs packages configured with use(), as defineBrain does");return P(o.definition,o.config,r)})}async function F(e,r){let o=k(r.directory);V(o,r.brainVersion);let f=ne(e.plugins,{name:o.name,version:o.version}),a=await K(m(r.temporaryRoot??X(),"rizom-test-brain-")),g={...E({dataDir:a,cacheDir:m(a,"cache"),distDir:m(a,"dist")}),embedding:{enabled:!1}},i;try{await new J(v.createFresh({level:S.WARN,context:"test-brain"}),{getStandardConfigWithDirectories:async()=>g,migrateEntities:O,migrateJobQueue:z,migrateConversations:M,migrateRuntimeState:q}).runAllMigrations();let t=_({name:o.name,version:o.version,logLevel:"warn",plugins:f,shellConfig:{...g,dataDir:m(a,"brain-data"),ai:{model:y}}}),n=I(j(t));i=N.createFresh(n,{aiService:T(A(n)),embeddingService:R()}),await i.initialize()}catch(t){throw await b(a,{recursive:!0,force:!0}),t}let Z=f.map((t)=>t.id),H,Q=async()=>{try{await i.shutdown()}finally{await b(a,{recursive:!0,force:!0})}};return Object.freeze({async tool(t,n){let l=i.getMCPService().listTools(),d=l.find(({tool:u})=>u.name===t);if(!d)throw Error(`No tool "${t}"; this brain has ${l.map(({tool:u})=>u.name).join(", ")}`);return te(await d.tool.handler(n,L()))},request:(t,n)=>h(i.getMessageBus(),t,n,Y),async fetch(t){let n=x(B(i),t.method.toUpperCase(),new URL(t.url).pathname);if(!n)return new Response("Not Found",{status:404});if(n.sharedHostAdmission==="deny")return new Response("Unauthorized",{status:401});return n.handler(t)},entities:C(i.getEntityService(),"restricted"),jobs:Object.freeze({async wait(t,n){let l=i.getJobQueueService(),d=async()=>{let c=await l.getStatus(n);if(!c||!Z.some((G)=>c.type===`${G}:${t.name}`))return null;if(re(c,Date.now()))return c;return await Bun.sleep(ee),d()},u=await d();return u?w(t,u):null}}),stop:()=>H??=Q()})}function Ee(e){return F(e,{brainVersion:U.version,directory:process.cwd()})}export{Ee as startTestBrain};
|
|
3
3
|
|
|
4
4
|
//# debugId=261DEEA8750E744864756E2164756E21
|
|
5
5
|
//# sourceMappingURL=testing.js.map
|
|
@@ -265,7 +265,7 @@
|
|
|
265
265
|
"import { sql, type SQL } from \"drizzle-orm\";\nimport { entities } from \"./schema/entities\";\n\nconst statusExpr = sql`json_extract(${entities.metadata}, '$.status')`;\n\n/**\n * The publish gate for one entity type. A type that declares its published\n * statuses gets exactly those — no status is not published. Any other type\n * keeps the default lifecycle: published, active, or no status at all.\n */\nexport function publishedStatusCondition(publishedStatuses?: string[]): SQL {\n if (publishedStatuses && publishedStatuses.length > 0) {\n return sql`${statusExpr} IN (${sql.join(\n publishedStatuses.map((status) => sql`${status}`),\n sql`, `,\n )})`;\n }\n return sql`(${statusExpr} = 'published' OR ${statusExpr} = 'active' OR ${statusExpr} IS NULL)`;\n}\n\n/**\n * The publish gate across entity types, for queries such as search that span\n * them: each declaring type (`gates`, statuses by type) by its own statuses,\n * every other type by the default lifecycle.\n */\nexport function publishedAcrossTypesCondition(\n gates: Record<string, string[]>,\n): SQL {\n const declared = Object.entries(gates).filter(\n ([, statuses]) => statuses.length > 0,\n );\n if (declared.length === 0) return publishedStatusCondition();\n const byType = declared.map(\n ([entityType, statuses]) =>\n sql`(${entities.entityType} = ${entityType} AND ${publishedStatusCondition(statuses)})`,\n );\n const undeclared = sql`(${entities.entityType} NOT IN (${sql.join(\n declared.map(([entityType]) => sql`${entityType}`),\n sql`, `,\n )}) AND ${publishedStatusCondition()})`;\n return sql`(${sql.join([...byType, undeclared], sql` OR `)})`;\n}\n",
|
|
266
266
|
"import type { EntitySearchDB } from \"./db\";\nimport {\n getVisibleContentVisibilities,\n type BaseEntity,\n type ContentVisibility,\n type SearchResult,\n type SearchOptions,\n type NearestEntity,\n type NearestToEntityRequest,\n type ProjectSemanticSpaceRequest,\n type SemanticSpaceProjection,\n} from \"./types\";\nimport type { IEmbeddingService } from \"./embedding-types\";\nimport { markdownBody } from \"./frontmatter\";\nimport type { EntitySerializer } from \"./entity-serializer\";\nimport { type Logger } from \"@brains/utils/logger\";\nimport { z } from \"@brains/utils/zod\";\nimport { sql, and, asc, desc, inArray, type SQL } from \"drizzle-orm\";\nimport { entities } from \"./schema/entities\";\nimport { SemanticIndexingDisabledError } from \"./errors\";\nimport { publishedAcrossTypesCondition } from \"./published-condition\";\nimport {\n buildSemanticSpaceProjection,\n type SemanticEmbedding,\n} from \"./semantic-space\";\n\n/**\n * The one embedding call search makes: it embeds the prepared query.\n *\n * IEmbeddingService also embeds documents and reports its identity;\n * asking for all of it meant a test could not supply one function without\n * asserting it was the whole service.\n */\nexport type QueryEmbedder = Pick<IEmbeddingService, \"embedQuery\">;\n\nexport const MAX_SEARCH_QUERY_CHARS = 12_000;\nconst MAX_VECTOR_DISTANCE = 0.82;\n\n/**\n * Results that rank equal list by id, then type, so an offset page never\n * follows the order rows were written. Ranking itself is untouched.\n */\nconst EQUAL_RANK_ORDER: SQL[] = [asc(entities.id), asc(entities.entityType)];\n\nfunction prepareSearchQuery(\n query: string,\n logger?: Logger,\n maxChars: number = MAX_SEARCH_QUERY_CHARS,\n): string {\n const normalizedQuery = query.trim().replace(/\\s+/g, \" \");\n\n if (normalizedQuery.length <= maxChars) {\n return normalizedQuery;\n }\n\n logger?.warn(\"Truncating search query that exceeds max length\", {\n originalLength: normalizedQuery.length,\n truncatedLength: maxChars,\n });\n\n return normalizedQuery.slice(0, maxChars);\n}\n\n/**\n * Schema for search options (excluding tags)\n */\nconst searchOptionsSchema = z.object({\n limit: z.number().int().positive().optional().default(20),\n offset: z.number().int().min(0).optional().default(0),\n types: z.array(z.string()).optional().default([]),\n excludeTypes: z.array(z.string()).optional().default([]),\n weight: z.record(z.string(), z.number()).optional(),\n visibilityScope: z.enum([\"public\", \"shared\", \"restricted\"]).optional(),\n includeUngenerated: z.boolean().optional().default(false),\n publishedOnly: z.boolean().optional().default(false),\n minScore: z.number().min(0).optional(),\n signal: z.instanceof(AbortSignal).optional(),\n});\n\nconst entityMetadataSchema = z.preprocess(\n (value) => {\n if (typeof value !== \"string\") return value;\n return JSON.parse(value);\n },\n z.record(z.string(), z.unknown()),\n);\n\n/**\n * EntitySearch handles all search operations for entities\n * Extracted from EntityService for single responsibility\n */\nexport class EntitySearch {\n private db: EntitySearchDB;\n private embeddingService: QueryEmbedder;\n private serializer: EntitySerializer;\n private logger: Logger;\n private readonly embeddingsEnabled: boolean;\n /** Published statuses by entity type, for the types that declare them. */\n private readonly publishGates: () => Record<string, string[]>;\n /**\n * The types this brain loads. Rows of any other type, left by a plugin the\n * brain no longer runs, cannot be read back, so search never returns them.\n */\n private readonly registeredTypes: (() => readonly string[]) | undefined;\n\n constructor(\n db: EntitySearchDB,\n embeddingService: QueryEmbedder,\n serializer: EntitySerializer,\n logger: Logger,\n embeddingsEnabled = true,\n publishGates: () => Record<string, string[]> = () => ({}),\n registeredTypes?: () => readonly string[],\n ) {\n this.db = db;\n this.embeddingService = embeddingService;\n this.serializer = serializer;\n this.logger = logger.child(\"EntitySearch\");\n this.embeddingsEnabled = embeddingsEnabled;\n this.publishGates = publishGates;\n this.registeredTypes = registeredTypes;\n }\n\n /** The requested types, or every type, narrowed to those the brain loads. */\n private searchableTypes(requested: string[]): string[] {\n const registered = this.registeredTypes?.();\n if (!registered) return requested;\n const candidates = requested.length > 0 ? requested : registered;\n return candidates.filter((type) => registered.includes(type));\n }\n\n /**\n * Search entities by query using vector similarity\n */\n public async search(\n query: string,\n options?: SearchOptions,\n ): Promise<SearchResult<BaseEntity>[]> {\n const validatedOptions = searchOptionsSchema.parse(options ?? {});\n const {\n offset,\n types: requestedTypes,\n excludeTypes,\n weight,\n visibilityScope,\n includeUngenerated,\n publishedOnly,\n minScore,\n signal,\n limit,\n } = validatedOptions;\n signal?.throwIfAborted();\n const types = this.searchableTypes(requestedTypes);\n if (this.registeredTypes && types.length === 0) return [];\n\n // Check if we have weights to apply\n const hasWeights = weight && Object.keys(weight).length > 0;\n const preparedQuery = prepareSearchQuery(query, this.logger);\n\n this.logger.debug(\n `Searching entities with query (${preparedQuery.length} chars)`,\n );\n\n if (!this.embeddingsEnabled) {\n return this.searchLexically(preparedQuery, {\n limit,\n offset,\n types,\n excludeTypes,\n weight,\n visibilityScope,\n includeUngenerated,\n publishedOnly,\n minScore,\n signal,\n });\n }\n\n // Generate embedding for the query\n const vector = (\n await (signal\n ? this.embeddingService.embedQuery(preparedQuery, signal)\n : this.embeddingService.embedQuery(preparedQuery))\n ).embedding;\n signal?.throwIfAborted();\n\n // Convert Float32Array to JSON array for SQL\n const embeddingArray = JSON.stringify(Array.from(vector));\n\n const weightMultiplier = this.buildWeightMultiplier(\n hasWeights ? weight : undefined,\n );\n\n // Build type filter conditions for drizzle\n const typeConditions: SQL[] = [];\n if (types.length > 0) {\n typeConditions.push(\n sql`${entities.entityType} IN (${sql.join(\n types.map((t) => sql`${t}`),\n sql`, `,\n )})`,\n );\n }\n if (excludeTypes.length > 0) {\n typeConditions.push(\n sql`${entities.entityType} NOT IN (${sql.join(\n excludeTypes.map((t) => sql`${t}`),\n sql`, `,\n )})`,\n );\n }\n\n return this.searchWithAttachedDb(\n embeddingArray,\n weightMultiplier,\n [\n ...typeConditions,\n ...this.buildVisibilityConditions(visibilityScope),\n ...this.buildGenerationStatusConditions(includeUngenerated),\n ...this.buildPublishedConditions(publishedOnly),\n ],\n limit,\n offset,\n preparedQuery,\n minScore,\n signal,\n );\n }\n\n private async searchLexically(\n query: string,\n options: {\n readonly limit: number;\n readonly offset: number;\n readonly types: string[];\n readonly excludeTypes: string[];\n readonly weight: Record<string, number> | undefined;\n readonly visibilityScope: ContentVisibility | undefined;\n readonly includeUngenerated: boolean;\n readonly publishedOnly: boolean;\n readonly minScore: number | undefined;\n readonly signal: AbortSignal | undefined;\n },\n ): Promise<SearchResult<BaseEntity>[]> {\n if (!query) return [];\n const ftsQuery = '\"' + query.replace(/\"/g, '\"\"') + '\"';\n const conditions: SQL[] = [sql`entity_fts MATCH ${ftsQuery}`];\n if (options.types.length > 0) {\n conditions.push(inArray(entities.entityType, options.types));\n }\n if (options.excludeTypes.length > 0) {\n conditions.push(\n sql`${entities.entityType} NOT IN (${sql.join(\n options.excludeTypes.map((type) => sql`${type}`),\n sql`, `,\n )})`,\n );\n }\n conditions.push(\n ...this.buildVisibilityConditions(options.visibilityScope),\n ...this.buildGenerationStatusConditions(options.includeUngenerated),\n ...this.buildPublishedConditions(options.publishedOnly),\n );\n\n // bm25() returns lower-is-better negative relevance. Map it to (0.5, 1)\n // so the system search default threshold still admits lexical matches,\n // while stronger matches remain ordered above weaker ones.\n const multiplier = this.buildWeightMultiplier(options.weight);\n const weightedScore = sql<number>`(0.5 + 0.5 * ((-bm25(entity_fts)) / (1.0 - bm25(entity_fts)))) * (${multiplier})`;\n if (options.minScore !== undefined) {\n conditions.push(sql`${weightedScore} >= ${options.minScore}`);\n }\n const results = await this.db\n .select({\n id: entities.id,\n entityType: entities.entityType,\n content: entities.content,\n contentHash: entities.contentHash,\n visibility: entities.visibility,\n created: entities.created,\n updated: entities.updated,\n metadata: entities.metadata,\n weighted_score: weightedScore.as(\"weighted_score\"),\n })\n .from(entities)\n .innerJoin(\n sql`entity_fts`,\n sql`entity_fts.entity_id = ${entities.id} AND entity_fts.entity_type = ${entities.entityType}`,\n )\n .where(and(...conditions))\n .orderBy(sql`weighted_score DESC`, ...EQUAL_RANK_ORDER)\n .limit(options.limit)\n .offset(options.offset);\n options.signal?.throwIfAborted();\n return this.mapSearchResults(results, query);\n }\n\n private buildVisibilityConditions(\n visibilityScope?: ContentVisibility,\n ): SQL[] {\n // Fail closed: undefined scope filters to public-only.\n const scope: ContentVisibility = visibilityScope ?? \"public\";\n if (scope === \"restricted\") {\n return [];\n }\n return [inArray(entities.visibility, getVisibleContentVisibilities(scope))];\n }\n\n private buildGenerationStatusConditions(includeUngenerated: boolean): SQL[] {\n if (includeUngenerated) return [];\n return [\n sql`(json_extract(${entities.metadata}, '$.status') IS NULL OR json_extract(${entities.metadata}, '$.status') NOT IN ('generating', 'failed'))`,\n ];\n }\n\n private buildPublishedConditions(publishedOnly: boolean): SQL[] {\n return publishedOnly\n ? [publishedAcrossTypesCondition(this.publishGates())]\n : [];\n }\n\n /**\n * FTS5 boost weight. When a keyword match is found, this fraction of the\n * final score comes from FTS5 rank, the rest from vector similarity.\n * 0.3 = 30% keyword, 70% semantic.\n */\n private static readonly FTS_ALPHA = 0.3;\n\n /**\n * Build a parameterized CASE expression for entity-type score multipliers.\n * Weight keys may be caller-provided, so avoid raw SQL string interpolation.\n */\n private buildWeightMultiplier(weight?: Record<string, number>): SQL<number> {\n const entries = Object.entries(weight ?? {}).filter(([, multiplier]) =>\n Number.isFinite(multiplier),\n );\n\n if (entries.length === 0) {\n return sql`1.0`;\n }\n\n const cases = entries.map(\n ([entityType, multiplier]) =>\n sql`WHEN ${entities.entityType} = ${entityType} THEN ${multiplier}`,\n );\n\n return sql`CASE ${sql.join(cases, sql` `)} ELSE 1.0 END`;\n }\n\n /**\n * Execute search against an attached embedding database (aliased as \"emb\").\n */\n private async searchWithAttachedDb(\n embeddingArray: string,\n weightMultiplier: SQL,\n typeConditions: SQL[],\n limit: number,\n offset: number,\n query: string,\n minScore: number | undefined,\n signal?: AbortSignal,\n ): Promise<SearchResult<BaseEntity>[]> {\n const alpha = EntitySearch.FTS_ALPHA;\n\n // Vector similarity score (0..1, higher is better)\n const vectorScore = sql<number>`(1.0 - vector_distance_cos(emb_e.embedding, vector32(${embeddingArray})) / 2.0) * (${weightMultiplier})`;\n const distanceExpr = sql<number>`vector_distance_cos(emb_e.embedding, vector32(${embeddingArray}))`;\n\n // FTS5 keyword boost: 1.0 when matched, 0.0 when not. The matches are a\n // list SQLite builds once per statement; a subquery correlated with the\n // candidate reruns the match for every entity, seconds on a book corpus.\n // Wrap in double quotes for phrase matching — prevents special characters\n // (?, *, OR, AND, etc.) from being parsed as FTS5 operators.\n const ftsQuery = '\"' + query.replace(/\"/g, '\"\"') + '\"';\n const ftsBoost = sql<number>`CASE WHEN (${entities.entityType}, ${entities.id}) IN (\n SELECT entity_type, entity_id FROM entity_fts WHERE entity_fts MATCH ${ftsQuery}\n ) THEN 1.0 ELSE 0.0 END`;\n\n // Combined score: (1-α)*vector + α*keyword_match\n const combinedScore = sql<number>`(${1 - alpha} * ${vectorScore}) + (${alpha} * ${ftsBoost})`;\n\n const results = await this.db\n .select({\n id: entities.id,\n entityType: entities.entityType,\n content: entities.content,\n contentHash: entities.contentHash,\n visibility: entities.visibility,\n created: entities.created,\n updated: entities.updated,\n metadata: entities.metadata,\n distance: distanceExpr,\n weighted_score: combinedScore,\n })\n .from(entities)\n .innerJoin(\n sql`emb.embeddings AS emb_e`,\n sql`${entities.id} = emb_e.entity_id AND ${entities.entityType} = emb_e.entity_type`,\n )\n .where(\n and(\n sql`${distanceExpr} < ${MAX_VECTOR_DISTANCE}`,\n ...(minScore !== undefined\n ? [sql`${combinedScore} >= ${minScore}`]\n : []),\n ...typeConditions,\n ),\n )\n .orderBy(desc(combinedScore), ...EQUAL_RANK_ORDER)\n .limit(limit)\n .offset(offset);\n signal?.throwIfAborted();\n return this.mapSearchResults(results, query);\n }\n\n /**\n * Search entities by type and query\n */\n public async searchEntities(\n entityType: string,\n query: string,\n options?: { limit?: number },\n ): Promise<SearchResult[]> {\n // Build search options with the entity type filter\n const searchOptions: SearchOptions = {\n types: [entityType],\n limit: options?.limit ?? 20,\n offset: 0,\n sortBy: \"relevance\",\n sortDirection: \"desc\",\n };\n\n return this.search(query, searchOptions);\n }\n\n /**\n * Project visible entity embeddings into a provider-independent semantic\n * space. Raw vectors never cross the entity-service boundary.\n */\n public async projectSemanticSpace(\n request: ProjectSemanticSpaceRequest,\n ): Promise<SemanticSpaceProjection> {\n if (!this.embeddingsEnabled) {\n throw new SemanticIndexingDisabledError();\n }\n const listed = request.entities\n ? new Set(request.entities.map(referenceKey))\n : undefined;\n const pointTypes = request.entities\n ? new Set(request.entities.map(({ entityType }) => entityType))\n : request.types && request.types.length > 0\n ? new Set(request.types)\n : undefined;\n\n const embeddings = (\n await this.readEmbeddings(\n pointTypes ? Array.from(pointTypes) : undefined,\n request.visibilityScope,\n request.entities?.map(({ entityId }) => entityId),\n )\n ).filter((embedding) => !listed || listed.has(referenceKey(embedding)));\n const originReference = request.origin;\n // An origin of a type outside the points is read alone: reading its whole\n // type to find one vector makes a page per entry quadratic.\n const origin = originReference\n ? (embeddings.find(\n (embedding) =>\n embedding.entityId === originReference.entityId &&\n embedding.entityType === originReference.entityType,\n ) ??\n (pointTypes?.has(originReference.entityType) === false\n ? (\n await this.readEmbeddings(\n [originReference.entityType],\n request.visibilityScope,\n [originReference.entityId],\n )\n )[0]\n : undefined))\n : undefined;\n const points = embeddings.filter((embedding) => {\n const matchesPointType = pointTypes?.has(embedding.entityType) ?? true;\n const isOrigin =\n embedding.entityId === originReference?.entityId &&\n embedding.entityType === originReference.entityType;\n return matchesPointType && !isOrigin;\n });\n\n return buildSemanticSpaceProjection(points, {\n ...(origin && { origin }),\n ...(request.maxNeighborDistance !== undefined && {\n maxNeighborDistance: request.maxNeighborDistance,\n }),\n });\n }\n\n /** Read and decode vectors from the attached embedding database. */\n private async readEmbeddings(\n types?: string[],\n visibilityScope?: ContentVisibility,\n entityIds?: readonly string[],\n ): Promise<SemanticEmbedding[]> {\n const conditions = this.buildVisibilityConditions(visibilityScope);\n if (types && types.length > 0) {\n conditions.push(inArray(entities.entityType, types));\n }\n if (entityIds !== undefined) {\n conditions.push(inArray(entities.id, [...entityIds]));\n }\n\n const embeddingExpr = sql<Float32Array>`emb_e.embedding`.mapWith({\n mapFromDriverValue(value: unknown): Float32Array {\n if (!ArrayBuffer.isView(value)) {\n throw new TypeError(\n \"Expected embedding blob to be an ArrayBuffer view\",\n );\n }\n\n const source = new Uint8Array(\n value.buffer,\n value.byteOffset,\n value.byteLength,\n );\n if (source.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0) {\n throw new RangeError(\n \"Embedding blob byte length must be divisible by 4\",\n );\n }\n\n const copy = new Uint8Array(source.byteLength);\n copy.set(source);\n return new Float32Array(copy.buffer);\n },\n });\n\n return this.db\n .select({\n entityId: entities.id,\n entityType: entities.entityType,\n embedding: embeddingExpr,\n })\n .from(entities)\n .innerJoin(\n sql`emb.embeddings AS emb_e`,\n sql`${entities.id} = emb_e.entity_id AND ${entities.entityType} = emb_e.entity_type`,\n )\n .where(conditions.length > 0 ? and(...conditions) : undefined)\n .orderBy(asc(entities.entityType), asc(entities.id));\n }\n\n /**\n * Visible entities of the given types nearest an entity's stored embedding,\n * closest first. The origin is read alone and within the same scope; an\n * origin out of scope or without an embedding has no neighbours.\n */\n public async nearestToEntity(\n request: NearestToEntityRequest,\n ): Promise<NearestEntity[]> {\n if (!this.embeddingsEnabled) {\n throw new SemanticIndexingDisabledError();\n }\n const [origin] = await this.readEmbeddings(\n [request.origin.entityType],\n request.visibilityScope,\n [request.origin.entityId],\n );\n if (!origin || request.types.length === 0) return [];\n\n const originVector = JSON.stringify(Array.from(origin.embedding));\n const distanceExpr = sql<number>`vector_distance_cos(emb_e.embedding, vector32(${originVector}))`;\n const query = this.db\n .select({\n entityId: entities.id,\n entityType: entities.entityType,\n distance: distanceExpr,\n metadata: entities.metadata,\n })\n .from(entities)\n .innerJoin(\n sql`emb.embeddings AS emb_e`,\n sql`${entities.id} = emb_e.entity_id AND ${entities.entityType} = emb_e.entity_type`,\n )\n .where(\n and(\n ...this.buildVisibilityConditions(request.visibilityScope),\n inArray(entities.entityType, request.types),\n request.publishedOnly\n ? publishedAcrossTypesCondition(this.publishGates())\n : undefined,\n sql`NOT (${entities.id} = ${request.origin.entityId} AND ${entities.entityType} = ${request.origin.entityType})`,\n request.maxDistance !== undefined\n ? sql`${distanceExpr} <= ${request.maxDistance}`\n : undefined,\n ),\n )\n .orderBy(sql`${distanceExpr} ASC`, ...EQUAL_RANK_ORDER);\n const rows =\n request.limit !== undefined\n ? await query.limit(request.limit)\n : await query;\n return rows.map(({ metadata, ...nearest }) =>\n request.includeMetadata ? { ...nearest, metadata } : nearest,\n );\n }\n\n /**\n * Return embedded entities with their raw cosine distance to the query,\n * sorted closest first. Without filters every embedded entity is returned,\n * for diagnostics and threshold tuning; `types` and `maxDistance` narrow it\n * in the query for callers that look for one close match.\n */\n public async searchWithDistances(\n query: string,\n filters: {\n readonly types?: string[] | undefined;\n readonly maxDistance?: number | undefined;\n } = {},\n ): Promise<\n Array<{ entityId: string; entityType: string; distance: number }>\n > {\n if (!this.embeddingsEnabled) {\n throw new SemanticIndexingDisabledError();\n }\n const preparedQuery = prepareSearchQuery(query, this.logger);\n const { embedding: queryEmbedding } =\n await this.embeddingService.embedQuery(preparedQuery);\n const embeddingArray = JSON.stringify(Array.from(queryEmbedding));\n\n const distanceExpr = sql<number>`vector_distance_cos(emb_e.embedding, vector32(${embeddingArray}))`;\n\n const results = await this.db\n .select({\n entityId: entities.id,\n entityType: entities.entityType,\n distance: distanceExpr,\n })\n .from(entities)\n .innerJoin(\n sql`emb.embeddings AS emb_e`,\n sql`${entities.id} = emb_e.entity_id AND ${entities.entityType} = emb_e.entity_type`,\n )\n .where(\n and(\n filters.types && filters.types.length > 0\n ? inArray(entities.entityType, filters.types)\n : undefined,\n filters.maxDistance !== undefined\n ? sql`${distanceExpr} <= ${filters.maxDistance}`\n : undefined,\n ),\n )\n .orderBy(sql`${distanceExpr} ASC`, ...EQUAL_RANK_ORDER);\n\n return results;\n }\n\n /**\n * Transform raw query rows into SearchResult objects\n */\n private mapSearchResults(\n results: Array<{\n id: string;\n entityType: string;\n content: string;\n contentHash: string;\n visibility: ContentVisibility;\n created: number;\n updated: number;\n metadata: unknown;\n weighted_score: number;\n }>,\n query: string,\n ): SearchResult<BaseEntity>[] {\n const searchResults: SearchResult<BaseEntity>[] = [];\n\n for (const row of results) {\n try {\n const metadata = entityMetadataSchema.parse(row.metadata);\n\n const entity = this.serializer.reconstructEntity({\n id: row.id,\n entityType: row.entityType,\n content: row.content,\n contentHash: row.contentHash,\n visibility: row.visibility,\n created: row.created,\n updated: row.updated,\n metadata,\n });\n\n searchResults.push({\n entity,\n score: row.weighted_score,\n excerpt: this.createExcerpt(row.content, query),\n });\n } catch (error) {\n this.logger.error(`Failed to parse entity during search: ${error}`);\n }\n }\n\n const queryPreview =\n query.length > 50 ? query.substring(0, 50) + \"...\" : query;\n this.logger.debug(\n `Found ${searchResults.length} results for query \"${queryPreview}\"`,\n );\n\n return searchResults;\n }\n\n /**\n * Create an excerpt from content based on query\n */\n private createExcerpt(markdown: string, query: string): string {\n // A reader of the result sees its text, not its frontmatter.\n const content = markdownBody(markdown);\n const maxLength = 200;\n const queryLower = query.toLowerCase();\n const contentLower = content.toLowerCase();\n\n // Find the position of the query in the content\n const position = contentLower.indexOf(queryLower);\n\n if (position !== -1) {\n // Extract text around the query\n const start = Math.max(0, position - 50);\n const end = Math.min(content.length, position + queryLower.length + 50);\n let excerpt = content.slice(start, end);\n\n // Add ellipsis if needed\n if (start > 0) excerpt = \"...\" + excerpt;\n if (end < content.length) excerpt = excerpt + \"...\";\n\n return excerpt;\n }\n\n // If query not found, return beginning of content\n return (\n content.slice(0, maxLength) + (content.length > maxLength ? \"...\" : \"\")\n );\n }\n}\n\n/** One key for an entity reference: type and id. */\nfunction referenceKey(reference: {\n entityType: string;\n entityId: string;\n}): string {\n return `${reference.entityType}:${reference.entityId}`;\n}\n",
|
|
267
267
|
"import {\n generateMarkdown,\n readMarkdownFrontmatter,\n} from \"@brains/utils/markdown-frontmatter\";\nimport { isRecord } from \"@brains/utils/is-record\";\nimport { z } from \"@brains/utils/zod\";\nimport { ENTITY_SYSTEM_FIELDS } from \"./frontmatter\";\nimport type { FrontmatterSchema } from \"./types\";\n\nconst recordSchema = z.record(z.string(), z.unknown());\nconst policyKeys: ReadonlySet<string> = new Set(ENTITY_SYSTEM_FIELDS);\n\n/** How a registered field whose stored value fails its own schema is treated. */\nexport type InvalidFieldPolicy = \"reject\" | \"omit\";\n\n/** Projection is derived from source, never from a caller's cached metadata. */\nexport function projectFrontmatterExtensions(\n source: Record<string, unknown>,\n metadata: Record<string, unknown>,\n extensions: readonly FrontmatterSchema[],\n invalid: InvalidFieldPolicy = \"reject\",\n): Record<string, unknown> {\n if (extensions.length === 0) return metadata;\n const projected = { ...metadata };\n for (const extension of extensions) {\n const keys = Object.keys(extension.shape);\n const input = Object.fromEntries(\n keys\n .filter((key) => Object.hasOwn(source, key))\n .map((key) => [key, source[key]]),\n );\n // Parse the whole extension, including cross-field refinements. Only this\n // extension's own keys are read, so another field's error never lands here.\n const result = extension.safeParse(input);\n if (!result.success && invalid === \"reject\") throw result.error;\n for (const key of keys) {\n delete projected[key];\n if (\n result.success &&\n Object.hasOwn(source, key) &&\n result.data[key] !== undefined &&\n !policyKeys.has(key)\n ) {\n projected[key] = result.data[key];\n }\n }\n }\n return projected;\n}\n\n/** Preserve unclaimed source fields even after their extension is disabled.\n * Export deliberately does not validate/repair malformed persisted values.\n */\nexport function preserveSourceFrontmatter(\n sourceContent: string,\n serializedContent: string,\n ownerSchema: FrontmatterSchema | undefined,\n extensions: readonly FrontmatterSchema[],\n groupingFields: readonly string[] = [],\n retiredFields: readonly string[] = [],\n): string {\n if (!ownerSchema) return serializedContent;\n // A nested path retires a key inside its object, never the object itself.\n const retired = new Set(retiredFields.filter((path) => !path.includes(\".\")));\n const source = readMarkdownFrontmatter(sourceContent);\n const serialized = readMarkdownFrontmatter(serializedContent);\n const sourceFields = recordSchema.parse(source.data);\n const serializedFields = recordSchema.parse(serialized.data);\n const extensionKeys = new Set([\n ...extensions.flatMap((schema) => Object.keys(schema.shape)),\n ...groupingFields,\n ]);\n const additions = Object.entries(sourceFields).filter(\n ([key, value]) =>\n !policyKeys.has(key) &&\n !retired.has(key) &&\n (!Object.hasOwn(ownerSchema.shape, key) || extensionKeys.has(key)) &&\n (!Object.hasOwn(serializedFields, key) ||\n JSON.stringify(serializedFields[key]) !== JSON.stringify(value)),\n );\n const removals = [\n ...[...extensionKeys].filter(\n (key) =>\n !Object.hasOwn(sourceFields, key) &&\n Object.hasOwn(serializedFields, key),\n ),\n ...[...retired].filter((key) => Object.hasOwn(serializedFields, key)),\n ];\n const merged = { ...serializedFields };\n for (const key of removals) delete merged[key];\n for (const [key, value] of additions) merged[key] = value;\n const result = dropFields(merged, retiredFields) ?? merged;\n if (additions.length === 0 && removals.length === 0 && result === merged)\n return serializedContent;\n // Unlike the domain formatter, preserve explicit null in unclaimed fields.\n return Object.keys(result).length > 0\n ? generateMarkdown(result, serialized.content)\n : serialized.content;\n}\n\n/** Keys a type never writes: its retired and its translated fields. */\nexport function unwrittenFields(config: {\n retiredFields?: readonly string[] | undefined;\n translatedFields?: readonly string[] | undefined;\n}): readonly string[] {\n return [...(config.retiredFields ?? []), ...(config.translatedFields ?? [])];\n}\n\n/**\n * The record without the given fields: a top-level key, or a key one level\n * down as `parent.key`. Undefined when the record holds none of them.\n */\nexport function dropFields(\n record: Record<string, unknown>,\n paths: readonly string[],\n): Record<string, unknown> | undefined {\n const result = paths.reduce<Record<string, unknown>>((current, path) => {\n const [parent, key] = path.split(\".\", 2);\n if (parent === undefined || !Object.hasOwn(current, parent)) return current;\n if (key === undefined) {\n const { [parent]: _dropped, ...rest } = current;\n return rest;\n }\n const nested = current[parent];\n if (!isRecord(nested) || !Object.hasOwn(nested, key)) return current;\n const { [key]: _dropped, ...kept } = nested;\n return { ...current, [parent]: kept };\n }, record);\n return result === record ? undefined : result;\n}\n\n/** Markdown with the given frontmatter keys removed; unchanged when it has none of them. */\nexport function withoutFrontmatterFields(\n content: string,\n fields: readonly string[],\n): string {\n if (fields.length === 0) return content;\n const parsed = readMarkdownFrontmatter(content);\n const data = recordSchema.safeParse(parsed.data);\n if (!data.success) return content;\n const kept = dropFields(data.data, fields);\n if (!kept) return content;\n return Object.keys(kept).length > 0\n ? generateMarkdown(kept, parsed.content)\n : parsed.content;\n}\n",
|
|
268
|
-
"import { getErrorMessage } from \"@brains/utils/error\";\nimport {\n generateMarkdown,\n readMarkdownFrontmatter,\n} from \"@brains/utils/markdown-frontmatter\";\nimport { isPlainRecord } from \"@brains/utils/predicates\";\nimport type { BaseEntity, EntityRegistry, SourceReadOption } from \"./types\";\nimport {\n ENTITY_SYSTEM_FIELDS,\n applyVisibilityToMarkdown,\n extractVisibilityFromMarkdown,\n} from \"./frontmatter\";\nimport type { Logger } from \"@brains/utils/logger\";\nimport type { EntityData } from \"./entity-data\";\nimport { toEntityValidationError } from \"./errors\";\nimport {\n dropFields,\n preserveSourceFrontmatter,\n unwrittenFields,\n withoutFrontmatterFields,\n} from \"./frontmatter-extensions\";\n\n/**\n * Whether two frontmatter values are the same, whatever their keys' order:\n * parsed YAML, so plain objects, arrays and scalars. The serializer runs in\n * browser bundles too, so it cannot reach node:util.\n */\nfunction sameValue(left: unknown, right: unknown): boolean {\n if (Array.isArray(left) || Array.isArray(right)) {\n return (\n Array.isArray(left) &&\n Array.isArray(right) &&\n left.length === right.length &&\n left.every((item, index) => sameValue(item, right[index]))\n );\n }\n if (isPlainRecord(left) && isPlainRecord(right)) {\n const keys = Object.keys(left);\n return (\n keys.length === Object.keys(right).length &&\n keys.every(\n (key) => Object.hasOwn(right, key) && sameValue(left[key], right[key]),\n )\n );\n }\n return Object.is(left, right);\n}\n\n/**\n * The written document's frontmatter in the order of the document it\n * replaces: each written key where that document has it, the document's\n * own value when it equals the written one (so a nested value keeps its\n * key order), and the keys the document lacks after the rest.\n */\nconst KEPT_RECORD_FIELDS: ReadonlySet<string> = new Set(\n ENTITY_SYSTEM_FIELDS.filter((field) => field !== \"visibility\"),\n);\n\nfunction inDocumentOrder(written: string, replaced: string): string {\n const source = readMarkdownFrontmatter(replaced).data;\n const target = readMarkdownFrontmatter(written);\n if (!isPlainRecord(source) || !isPlainRecord(target.data)) return written;\n // The record fields a file states stay where it states them, as an\n // entity plugin class's adapter wrote over existing frontmatter; the\n // serializer leaves them out of what it writes. Visibility is written\n // from the record itself. A date the file leaves unquoted is written\n // back as the quoted timestamp the class wrote.\n const fields = {\n ...Object.fromEntries(\n Object.entries(source)\n .filter(([key]) => KEPT_RECORD_FIELDS.has(key))\n .map(([key, value]) => [\n key,\n value instanceof Date ? value.toISOString() : value,\n ]),\n ),\n ...target.data,\n };\n const ordered = [\n ...Object.keys(source).filter((key) => Object.hasOwn(fields, key)),\n ...Object.keys(fields).filter((key) => !Object.hasOwn(source, key)),\n ];\n const result = Object.fromEntries(\n ordered.map((key) => [\n key,\n sameValue(source[key], fields[key]) ? source[key] : fields[key],\n ]),\n );\n return generateMarkdown(result, target.content);\n}\n\n/**\n * EntitySerializer handles conversion between entities and markdown\n * Extracted from EntityService for single responsibility\n */\nexport class EntitySerializer {\n private entityRegistry: EntityRegistry;\n private logger: Logger;\n\n constructor(entityRegistry: EntityRegistry, logger: Logger) {\n this.entityRegistry = entityRegistry;\n this.logger = logger.child(\"EntitySerializer\");\n }\n\n /**\n * Serialize an entity to markdown format. `stored` is the markdown the\n * entity replaces, given when the update carries a declared type's body\n * alone: the authored frontmatter the type does not write is kept from it.\n */\n public serializeEntity(entity: BaseEntity, stored?: string): string {\n const adapter = this.entityRegistry.getAdapter(entity.entityType);\n const serialized = applyVisibilityToMarkdown(\n adapter.toMarkdown(entity),\n entity.visibility,\n );\n const written = preserveSourceFrontmatter(\n stored === undefined\n ? entity.content\n : unwrittenSource(stored, serialized),\n serialized,\n this.entityRegistry.getEntityTypeConfig(entity.entityType)\n .binaryStorage === \"asset\"\n ? undefined\n : (adapter.writtenFrontmatterSchema ?? adapter.frontmatterSchema),\n this.entityRegistry.getFrontmatterExtensions(entity.entityType),\n this.entityRegistry\n .getGroupings()\n .filter((grouping) => grouping.types.includes(entity.entityType))\n .map((grouping) => grouping.field),\n unwrittenFields(\n this.entityRegistry.getEntityTypeConfig(entity.entityType),\n ),\n );\n return adapter.writesInDocumentOrder === true\n ? inDocumentOrder(written, stored ?? entity.content)\n : written;\n }\n\n /**\n * Deserialize markdown content to an entity (partial)\n * Returns parsed fields from markdown - caller should merge with metadata\n */\n public deserializeEntity(\n markdown: string,\n entityType: string,\n ): Partial<BaseEntity> {\n const adapter = this.entityRegistry.getAdapter(entityType);\n const current = withoutFrontmatterFields(\n markdown,\n this.retiredFields(entityType),\n );\n let parsedFromMarkdown: Partial<BaseEntity>;\n try {\n parsedFromMarkdown = adapter.fromMarkdown(current);\n } catch (error) {\n throw toEntityValidationError(entityType, error) ?? error;\n }\n const {\n metadata: parsedMetadata,\n visibility: _parsedVisibility,\n ...parsed\n } = parsedFromMarkdown;\n // Left unset when the file declares no visibility, so callers can tell\n // \"the file said nothing\" from \"the file said public\".\n const visibility = extractVisibilityFromMarkdown(markdown);\n const metadata = this.stripPolicyMetadata(\n this.entityRegistry.projectMetadata(\n entityType,\n current,\n parsedMetadata ?? {},\n ),\n entityType,\n );\n return {\n ...parsed,\n ...(visibility && { visibility }),\n metadata,\n };\n }\n\n /**\n * Convert a database row to a validated entity, or null if parsing/validation fails\n */\n public async convertToEntity(\n entityData: EntityData,\n source?: SourceReadOption,\n ): Promise<BaseEntity | null> {\n try {\n return this.reconstructEntity(entityData, source);\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n this.logger.error(\n `Failed to parse entity of type ${entityData.entityType} with ID ${entityData.id}: ${errorMessage}`,\n );\n return null;\n }\n }\n\n /**\n * Convert multiple database rows to entities, skipping any that fail validation\n */\n public async convertToEntities(\n rows: EntityData[],\n entityType: string,\n source?: SourceReadOption,\n ): Promise<BaseEntity[]> {\n const entityList: BaseEntity[] = [];\n\n for (const entityData of rows) {\n try {\n entityList.push(this.reconstructEntity(entityData, source));\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n this.logger.error(\n `Failed to parse entity of type ${entityType} with ID ${entityData.id}: ${errorMessage}`,\n );\n }\n }\n\n return entityList;\n }\n\n /**\n * Reconstruct a typed entity from a database row by merging DB fields,\n * metadata, and adapter-parsed content, then validating against the schema\n */\n public reconstructEntity(\n entityData: EntityData,\n source?: SourceReadOption,\n ): BaseEntity {\n const adapter = this.entityRegistry.getAdapter(entityData.entityType);\n // Stored content keeps retired keys until rewritten; parse past them.\n const parsedContent = adapter.fromMarkdown(\n withoutFrontmatterFields(\n entityData.content,\n this.retiredFields(entityData.entityType),\n ),\n );\n // Strip parsed metadata — DB metadata is the source of truth.\n // `system_update({ operation: { fields } })` mutates DB metadata without touching\n // markdown, so parsed frontmatter can be stale. Body-parsed top-level\n // fields (e.g. `about`, `skills`) still land via parsedRest.\n const {\n metadata: _parsedMeta,\n visibility: _parsedVisibility,\n ...parsedRest\n } = parsedContent;\n\n const entity: unknown = {\n id: entityData.id,\n entityType: entityData.entityType,\n content: entityData.content,\n contentHash: entityData.contentHash,\n created: new Date(entityData.created).toISOString(),\n updated: new Date(entityData.updated).toISOString(),\n visibility: entityData.visibility,\n ...parsedRest,\n ...this.stripPolicyMetadata(entityData.metadata, entityData.entityType),\n metadata: this.stripPolicyMetadata(\n entityData.metadata,\n entityData.entityType,\n ),\n };\n\n const parsed = adapter.schema.parse(entity);\n // A declared type reads back its decoded body; its source is the stored\n // markdown. Every other type's content is the stored markdown already.\n return source === true && adapter.metadataFromFrontmatter === true\n ? { ...parsed, content: entityData.content }\n : parsed;\n }\n\n /**\n * Prepare entity for database storage\n */\n public prepareEntityForStorage(\n entity: BaseEntity,\n entityType: string,\n stored?: string,\n ): {\n markdown: string;\n metadata: Record<string, unknown>;\n } {\n // Get adapter for the entity type\n const adapter = this.entityRegistry.getAdapter(entityType);\n\n // Convert to markdown using adapter\n const markdown = this.serializeEntity(entity, stored);\n\n // Extract metadata using adapter, keeping visibility as a top-level field.\n const metadata = this.stripPolicyMetadata(\n this.entityRegistry.projectMetadata(\n entityType,\n markdown,\n adapter.extractMetadata(entity),\n ),\n entityType,\n );\n\n return { markdown, metadata };\n }\n\n /**\n * Whether an update has to keep authored frontmatter from the stored\n * record: a declared type's reads hand back its body alone, so a writer\n * that sends the body back has not seen that frontmatter, and leaving it\n * out removes nothing. A writer that sends frontmatter sends the whole\n * document, as a file import or an editor does.\n */\n public keepsStoredFrontmatter(entity: BaseEntity): boolean {\n if (\n this.entityRegistry.getAdapter(entity.entityType)\n .metadataFromFrontmatter !== true\n ) {\n return false;\n }\n const written = readMarkdownFrontmatter(entity.content);\n return (\n !isPlainRecord(written.data) || Object.keys(written.data).length === 0\n );\n }\n\n /**\n * An update to a type whose metadata is its frontmatter, with the\n * frontmatter it writes carried into its metadata. Reads hand such a type\n * back its body alone, so frontmatter on an update is one the caller\n * wrote: it is laid over the metadata the caller read and decoded through\n * the type's adapter, so the stored metadata matches the stored\n * frontmatter at once rather than at the next import.\n */\n public withFrontmatterWrites<T extends BaseEntity>(entity: T): T {\n const adapter = this.entityRegistry.getAdapter(entity.entityType);\n if (adapter.metadataFromFrontmatter !== true) return entity;\n const written = readMarkdownFrontmatter(entity.content);\n if (!isPlainRecord(written.data)) return entity;\n if (Object.keys(written.data).length === 0) return entity;\n const encoded = readMarkdownFrontmatter(\n adapter.toMarkdown({ ...entity, content: written.content }),\n );\n let decoded: Partial<BaseEntity>;\n try {\n decoded = adapter.fromMarkdown(\n generateMarkdown(\n {\n ...(isPlainRecord(encoded.data) ? encoded.data : {}),\n ...written.data,\n },\n written.content,\n ),\n );\n } catch (error) {\n throw toEntityValidationError(entity.entityType, error) ?? error;\n }\n return {\n ...entity,\n metadata: this.stripPolicyMetadata(\n decoded.metadata ?? entity.metadata,\n entity.entityType,\n ),\n };\n }\n\n /**\n * The entity with its type's retired fields left out of its content, so\n * validators and storage see only the fields the type still owns.\n */\n public withoutRetiredFields<\n T extends { entityType: string; content: string },\n >(entity: T): T {\n const content = withoutFrontmatterFields(\n entity.content,\n this.retiredFields(entity.entityType),\n );\n return content === entity.content ? entity : { ...entity, content };\n }\n\n /** Metadata without policy fields or the type's retired fields. */\n private stripPolicyMetadata(\n metadata: Record<string, unknown>,\n entityType: string,\n ): Record<string, unknown> {\n const { visibility: _visibility, ...rest } = metadata;\n const unwritten = unwrittenFields(\n this.entityRegistry.getEntityTypeConfig(entityType),\n );\n return dropFields(rest, unwritten) ?? rest;\n }\n\n private retiredFields(entityType: string): readonly string[] {\n return (\n this.entityRegistry.getEntityTypeConfig(entityType).retiredFields ?? []\n );\n }\n}\n\n/**\n * The stored frontmatter a serialized document does not write, as a source\n * for preserving it: what the type writes comes from the entity, never the\n * stored copy.\n */\nfunction unwrittenSource(stored: string, serialized: string): string {\n const storedFields = readMarkdownFrontmatter(stored).data;\n const written = readMarkdownFrontmatter(serialized).data;\n if (!isPlainRecord(storedFields)) return \"\";\n const kept = Object.fromEntries(\n Object.entries(storedFields).filter(\n ([key]) => !isPlainRecord(written) || !Object.hasOwn(written, key),\n ),\n );\n return Object.keys(kept).length > 0 ? generateMarkdown(kept, \"\") : \"\";\n}\n",
|
|
268
|
+
"import { getErrorMessage } from \"@brains/utils/error\";\nimport {\n generateMarkdown,\n readMarkdownFrontmatter,\n} from \"@brains/utils/markdown-frontmatter\";\nimport { isPlainRecord } from \"@brains/utils/predicates\";\nimport type { BaseEntity, EntityRegistry, SourceReadOption } from \"./types\";\nimport {\n ENTITY_SYSTEM_FIELDS,\n applyVisibilityToMarkdown,\n extractVisibilityFromMarkdown,\n} from \"./frontmatter\";\nimport type { Logger } from \"@brains/utils/logger\";\nimport type { EntityData } from \"./entity-data\";\nimport { toEntityValidationError } from \"./errors\";\nimport {\n dropFields,\n preserveSourceFrontmatter,\n unwrittenFields,\n withoutFrontmatterFields,\n} from \"./frontmatter-extensions\";\n\n/**\n * Whether two frontmatter values are the same, whatever their keys' order:\n * parsed YAML, so plain objects, arrays and scalars. The serializer runs in\n * browser bundles too, so it cannot reach node:util.\n */\nfunction sameValue(left: unknown, right: unknown): boolean {\n if (Array.isArray(left) || Array.isArray(right)) {\n return (\n Array.isArray(left) &&\n Array.isArray(right) &&\n left.length === right.length &&\n left.every((item, index) => sameValue(item, right[index]))\n );\n }\n if (isPlainRecord(left) && isPlainRecord(right)) {\n const keys = Object.keys(left);\n return (\n keys.length === Object.keys(right).length &&\n keys.every(\n (key) => Object.hasOwn(right, key) && sameValue(left[key], right[key]),\n )\n );\n }\n return Object.is(left, right);\n}\n\n/**\n * The written document's frontmatter in the order of the document it\n * replaces: each written key where that document has it, the document's\n * own value when it equals the written one (so a nested value keeps its\n * key order), and the keys the document lacks after the rest.\n */\nconst KEPT_RECORD_FIELDS: ReadonlySet<string> = new Set(\n ENTITY_SYSTEM_FIELDS.filter((field) => field !== \"visibility\"),\n);\n\nfunction inDocumentOrder(written: string, replaced: string): string {\n const source = readMarkdownFrontmatter(replaced).data;\n const target = readMarkdownFrontmatter(written);\n if (!isPlainRecord(source) || !isPlainRecord(target.data)) return written;\n // The record fields a file states stay where it states them, as an\n // entity plugin class's adapter wrote over existing frontmatter; the\n // serializer leaves them out of what it writes. Visibility is written\n // from the record itself. A date the file leaves unquoted is written\n // back as the quoted timestamp the class wrote.\n const fields = {\n ...Object.fromEntries(\n Object.entries(source)\n .filter(([key]) => KEPT_RECORD_FIELDS.has(key))\n .map(([key, value]) => [\n key,\n value instanceof Date ? value.toISOString() : value,\n ]),\n ),\n ...target.data,\n };\n const ordered = [\n ...Object.keys(source).filter((key) => Object.hasOwn(fields, key)),\n ...Object.keys(fields).filter((key) => !Object.hasOwn(source, key)),\n ];\n const result = Object.fromEntries(\n ordered.map((key) => [\n key,\n sameValue(source[key], fields[key]) ? source[key] : fields[key],\n ]),\n );\n return generateMarkdown(result, target.content);\n}\n\n/**\n * EntitySerializer handles conversion between entities and markdown\n * Extracted from EntityService for single responsibility\n */\nexport class EntitySerializer {\n private entityRegistry: EntityRegistry;\n private logger: Logger;\n\n constructor(entityRegistry: EntityRegistry, logger: Logger) {\n this.entityRegistry = entityRegistry;\n this.logger = logger.child(\"EntitySerializer\");\n }\n\n /**\n * Serialize an entity to markdown format. `stored` is the markdown the\n * entity replaces, given when the update carries a declared type's body\n * alone: the authored frontmatter the type does not write is kept from it.\n */\n public serializeEntity(entity: BaseEntity, stored?: string): string {\n const adapter = this.entityRegistry.getAdapter(entity.entityType);\n const serialized = applyVisibilityToMarkdown(\n adapter.toMarkdown(entity),\n entity.visibility,\n );\n const written = preserveSourceFrontmatter(\n stored === undefined\n ? entity.content\n : unwrittenSource(stored, serialized),\n serialized,\n this.entityRegistry.getEntityTypeConfig(entity.entityType)\n .binaryStorage === \"asset\"\n ? undefined\n : (adapter.writtenFrontmatterSchema ?? adapter.frontmatterSchema),\n this.entityRegistry.getFrontmatterExtensions(entity.entityType),\n this.entityRegistry\n .getGroupings()\n .filter((grouping) => grouping.types.includes(entity.entityType))\n .map((grouping) => grouping.field),\n unwrittenFields(\n this.entityRegistry.getEntityTypeConfig(entity.entityType),\n ),\n );\n return adapter.writesInDocumentOrder === true\n ? inDocumentOrder(written, stored ?? entity.content)\n : written;\n }\n\n /**\n * Deserialize markdown content to an entity (partial)\n * Returns parsed fields from markdown - caller should merge with metadata\n */\n public deserializeEntity(\n markdown: string,\n entityType: string,\n ): Partial<BaseEntity> {\n const adapter = this.entityRegistry.getAdapter(entityType);\n const current = withoutFrontmatterFields(\n markdown,\n this.retiredFields(entityType),\n );\n let parsedFromMarkdown: Partial<BaseEntity>;\n try {\n parsedFromMarkdown = adapter.fromMarkdown(current);\n } catch (error) {\n throw toEntityValidationError(entityType, error) ?? error;\n }\n const {\n metadata: parsedMetadata,\n visibility: _parsedVisibility,\n ...parsed\n } = parsedFromMarkdown;\n // Left unset when the file declares no visibility, so callers can tell\n // \"the file said nothing\" from \"the file said public\".\n const visibility = extractVisibilityFromMarkdown(markdown);\n const metadata = this.stripPolicyMetadata(\n this.entityRegistry.projectMetadata(\n entityType,\n current,\n parsedMetadata ?? {},\n ),\n entityType,\n );\n return {\n ...parsed,\n ...(visibility && { visibility }),\n metadata,\n };\n }\n\n /**\n * Convert a database row to a validated entity, or null if parsing/validation fails\n */\n public async convertToEntity(\n entityData: EntityData,\n source?: SourceReadOption,\n ): Promise<BaseEntity | null> {\n try {\n return this.reconstructEntity(entityData, source);\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n this.logger.error(\n `Failed to parse entity of type ${entityData.entityType} with ID ${entityData.id}: ${errorMessage}`,\n );\n return null;\n }\n }\n\n /**\n * Convert multiple database rows to entities, skipping any that fail validation\n */\n public async convertToEntities(\n rows: EntityData[],\n entityType: string,\n source?: SourceReadOption,\n ): Promise<BaseEntity[]> {\n const entityList: BaseEntity[] = [];\n\n for (const entityData of rows) {\n try {\n entityList.push(this.reconstructEntity(entityData, source));\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n this.logger.error(\n `Failed to parse entity of type ${entityType} with ID ${entityData.id}: ${errorMessage}`,\n );\n }\n }\n\n return entityList;\n }\n\n /**\n * Reconstruct a typed entity from a database row by merging DB fields,\n * metadata, and adapter-parsed content, then validating against the schema.\n *\n * A type whose metadata is its frontmatter may hold rows an entity plugin\n * class stored, whose metadata the declaration no longer accepts until the\n * boot pass re-derives it. Such a row reads with the metadata re-derived\n * from its stored markdown, as the pass derives it; nothing is written. A\n * row whose markdown does not decode either fails as it would have.\n */\n public reconstructEntity(\n entityData: EntityData,\n source?: SourceReadOption,\n ): BaseEntity {\n try {\n return this.reconstructFromRow(entityData, source);\n } catch (error) {\n const derived = this.declaredMetadataFromMarkdown(entityData);\n if (!derived) throw error;\n return this.reconstructFromRow(\n { ...entityData, metadata: derived },\n source,\n );\n }\n }\n\n /**\n * A declared type's metadata as its stored markdown declares it, or\n * undefined for another type or markdown that does not decode.\n */\n private declaredMetadataFromMarkdown(\n entityData: EntityData,\n ): Record<string, unknown> | undefined {\n const { entityType, content } = entityData;\n if (\n !this.entityRegistry.hasEntityType(entityType) ||\n this.entityRegistry.getAdapter(entityType).metadataFromFrontmatter !==\n true\n ) {\n return undefined;\n }\n try {\n return this.deserializeEntity(content, entityType).metadata ?? {};\n } catch {\n return undefined;\n }\n }\n\n private reconstructFromRow(\n entityData: EntityData,\n source?: SourceReadOption,\n ): BaseEntity {\n const adapter = this.entityRegistry.getAdapter(entityData.entityType);\n // Stored content keeps retired keys until rewritten; parse past them.\n const parsedContent = adapter.fromMarkdown(\n withoutFrontmatterFields(\n entityData.content,\n this.retiredFields(entityData.entityType),\n ),\n );\n // Strip parsed metadata — DB metadata is the source of truth.\n // `system_update({ operation: { fields } })` mutates DB metadata without touching\n // markdown, so parsed frontmatter can be stale. Body-parsed top-level\n // fields (e.g. `about`, `skills`) still land via parsedRest.\n const {\n metadata: _parsedMeta,\n visibility: _parsedVisibility,\n ...parsedRest\n } = parsedContent;\n\n const entity: unknown = {\n id: entityData.id,\n entityType: entityData.entityType,\n content: entityData.content,\n contentHash: entityData.contentHash,\n created: new Date(entityData.created).toISOString(),\n updated: new Date(entityData.updated).toISOString(),\n visibility: entityData.visibility,\n ...parsedRest,\n ...this.stripPolicyMetadata(entityData.metadata, entityData.entityType),\n metadata: this.stripPolicyMetadata(\n entityData.metadata,\n entityData.entityType,\n ),\n };\n\n const parsed = adapter.schema.parse(entity);\n // A declared type reads back its decoded body; its source is the stored\n // markdown. Every other type's content is the stored markdown already.\n return source === true && adapter.metadataFromFrontmatter === true\n ? { ...parsed, content: entityData.content }\n : parsed;\n }\n\n /**\n * Prepare entity for database storage\n */\n public prepareEntityForStorage(\n entity: BaseEntity,\n entityType: string,\n stored?: string,\n ): {\n markdown: string;\n metadata: Record<string, unknown>;\n } {\n // Get adapter for the entity type\n const adapter = this.entityRegistry.getAdapter(entityType);\n\n // Convert to markdown using adapter\n const markdown = this.serializeEntity(entity, stored);\n\n // Extract metadata using adapter, keeping visibility as a top-level field.\n const metadata = this.stripPolicyMetadata(\n this.entityRegistry.projectMetadata(\n entityType,\n markdown,\n adapter.extractMetadata(entity),\n ),\n entityType,\n );\n\n return { markdown, metadata };\n }\n\n /**\n * Whether an update has to keep authored frontmatter from the stored\n * record: a declared type's reads hand back its body alone, so a writer\n * that sends the body back has not seen that frontmatter, and leaving it\n * out removes nothing. A writer that sends frontmatter sends the whole\n * document, as a file import or an editor does.\n */\n public keepsStoredFrontmatter(entity: BaseEntity): boolean {\n if (\n this.entityRegistry.getAdapter(entity.entityType)\n .metadataFromFrontmatter !== true\n ) {\n return false;\n }\n const written = readMarkdownFrontmatter(entity.content);\n return (\n !isPlainRecord(written.data) || Object.keys(written.data).length === 0\n );\n }\n\n /**\n * An update to a type whose metadata is its frontmatter, with the\n * frontmatter it writes carried into its metadata. Reads hand such a type\n * back its body alone, so frontmatter on an update is one the caller\n * wrote: it is laid over the metadata the caller read and decoded through\n * the type's adapter, so the stored metadata matches the stored\n * frontmatter at once rather than at the next import.\n */\n public withFrontmatterWrites<T extends BaseEntity>(entity: T): T {\n const adapter = this.entityRegistry.getAdapter(entity.entityType);\n if (adapter.metadataFromFrontmatter !== true) return entity;\n const written = readMarkdownFrontmatter(entity.content);\n if (!isPlainRecord(written.data)) return entity;\n if (Object.keys(written.data).length === 0) return entity;\n const encoded = readMarkdownFrontmatter(\n adapter.toMarkdown({ ...entity, content: written.content }),\n );\n let decoded: Partial<BaseEntity>;\n try {\n decoded = adapter.fromMarkdown(\n generateMarkdown(\n {\n ...(isPlainRecord(encoded.data) ? encoded.data : {}),\n ...written.data,\n },\n written.content,\n ),\n );\n } catch (error) {\n throw toEntityValidationError(entity.entityType, error) ?? error;\n }\n return {\n ...entity,\n metadata: this.stripPolicyMetadata(\n decoded.metadata ?? entity.metadata,\n entity.entityType,\n ),\n };\n }\n\n /**\n * The entity with its type's retired fields left out of its content, so\n * validators and storage see only the fields the type still owns.\n */\n public withoutRetiredFields<\n T extends { entityType: string; content: string },\n >(entity: T): T {\n const content = withoutFrontmatterFields(\n entity.content,\n this.retiredFields(entity.entityType),\n );\n return content === entity.content ? entity : { ...entity, content };\n }\n\n /** Metadata without policy fields or the type's retired fields. */\n private stripPolicyMetadata(\n metadata: Record<string, unknown>,\n entityType: string,\n ): Record<string, unknown> {\n const { visibility: _visibility, ...rest } = metadata;\n const unwritten = unwrittenFields(\n this.entityRegistry.getEntityTypeConfig(entityType),\n );\n return dropFields(rest, unwritten) ?? rest;\n }\n\n private retiredFields(entityType: string): readonly string[] {\n return (\n this.entityRegistry.getEntityTypeConfig(entityType).retiredFields ?? []\n );\n }\n}\n\n/**\n * The stored frontmatter a serialized document does not write, as a source\n * for preserving it: what the type writes comes from the entity, never the\n * stored copy.\n */\nfunction unwrittenSource(stored: string, serialized: string): string {\n const storedFields = readMarkdownFrontmatter(stored).data;\n const written = readMarkdownFrontmatter(serialized).data;\n if (!isPlainRecord(storedFields)) return \"\";\n const kept = Object.fromEntries(\n Object.entries(storedFields).filter(\n ([key]) => !isPlainRecord(written) || !Object.hasOwn(written, key),\n ),\n );\n return Object.keys(kept).length > 0 ? generateMarkdown(kept, \"\") : \"\";\n}\n",
|
|
269
269
|
"import { z } from \"@brains/utils/zod\";\nimport { canonicalContentVisibilitySchema } from \"./visibility\";\n\nexport const entityGroupingSchema: z.ZodObject<{\n key: z.ZodString;\n label: z.ZodString;\n field: z.ZodString;\n types: z.ZodArray<z.ZodString>;\n}> = z.strictObject({\n key: z\n .string()\n .min(1)\n .max(80)\n .regex(/^[a-z][a-z0-9-]*$/),\n label: z.string().trim().min(1).max(100),\n field: z\n .string()\n .min(1)\n .max(80)\n .regex(/^[a-z][a-z0-9-]*$/),\n types: z\n .array(z.string().min(1).max(100))\n .min(1)\n .max(100)\n .refine(\n (types) => new Set(types).size === types.length,\n \"Grouping types must be unique\",\n ),\n});\nexport type EntityGrouping = z.infer<typeof entityGroupingSchema>;\n\n/** One ordering vocabulary, shared by the queries, the routes and the UI. */\nexport const groupingSortSchema: z.ZodEnum<{\n \"updated-desc\": \"updated-desc\";\n \"updated-asc\": \"updated-asc\";\n \"created-desc\": \"created-desc\";\n \"created-asc\": \"created-asc\";\n}> = z.enum([\"updated-desc\", \"updated-asc\", \"created-desc\", \"created-asc\"]);\nexport type GroupingSort = z.infer<typeof groupingSortSchema>;\n\nexport const GROUPING_PAGE_LIMIT = 50;\nexport const GROUPING_MAX_PAGE_LIMIT = 100;\n/** Bounds every grouping read, whoever builds the request. */\nexport const groupingKeySchema: z.ZodString = z.string().min(1).max(80);\nexport const groupingValueSchema: z.ZodString = z.string().max(10000);\nexport const groupingSearchSchema: z.ZodString = z.string().max(200);\n\nexport const queryGroupingCatalogSchema: z.ZodObject<{\n grouping: z.ZodString;\n entityTypes: z.ZodArray<z.ZodString>;\n visibilityScope: z.ZodOptional<typeof canonicalContentVisibilitySchema>;\n limit: z.ZodDefault<z.ZodNumber>;\n offset: z.ZodDefault<z.ZodNumber>;\n signal: z.ZodOptional<z.ZodCustom<AbortSignal, AbortSignal>>;\n}> = z.object({\n grouping: groupingKeySchema,\n /** Required caller-admitted set; it can only narrow the declared types. */\n entityTypes: z.array(z.string().min(1)).max(100),\n visibilityScope: canonicalContentVisibilitySchema.optional(),\n limit: z\n .number()\n .int()\n .min(1)\n .max(GROUPING_MAX_PAGE_LIMIT)\n .default(GROUPING_PAGE_LIMIT),\n offset: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),\n signal: z.instanceof(AbortSignal).optional(),\n});\n\nexport const queryGroupingMembersSchema: z.ZodObject<{\n grouping: z.ZodString;\n entityTypes: z.ZodArray<z.ZodString>;\n visibilityScope: z.ZodOptional<typeof canonicalContentVisibilitySchema>;\n limit: z.ZodDefault<z.ZodNumber>;\n offset: z.ZodDefault<z.ZodNumber>;\n signal: z.ZodOptional<z.ZodCustom<AbortSignal, AbortSignal>>;\n value: z.ZodString;\n q: z.ZodOptional<z.ZodString>;\n sort: z.ZodDefault<typeof groupingSortSchema>;\n source: z.ZodOptional<z.ZodBoolean>;\n}> = queryGroupingCatalogSchema.extend({\n value: groupingValueSchema,\n q: groupingSearchSchema.optional(),\n sort: groupingSortSchema.default(\"updated-desc\"),\n /** Members read as stored, as `source` reads elsewhere. */\n source: z.boolean().optional(),\n});\n\n/** Usage is a distinct entry total plus counts for a bounded set of exact values. */\nexport const queryGroupingUsageSchema: z.ZodObject<{\n grouping: z.ZodString;\n entityTypes: z.ZodArray<z.ZodString>;\n visibilityScope: z.ZodOptional<typeof canonicalContentVisibilitySchema>;\n signal: z.ZodOptional<z.ZodCustom<AbortSignal, AbortSignal>>;\n values: z.ZodDefault<z.ZodArray<z.ZodString>>;\n}> = queryGroupingCatalogSchema.omit({ limit: true, offset: true }).extend({\n values: z.array(groupingValueSchema).max(GROUPING_MAX_PAGE_LIMIT).default([]),\n});\nexport type QueryGroupingUsageRequest = z.input<\n typeof queryGroupingUsageSchema\n>;\nexport interface EntityGroupingUsage {\n /** Entries with at least one text membership, including values outside a list. */\n entries: number;\n /** Request order and literals preserved; unused values have a zero count. */\n values: Array<{ value: string; count: number }>;\n}\n\nexport type QueryGroupingCatalogRequest = z.input<\n typeof queryGroupingCatalogSchema\n>;\nexport type QueryGroupingMembersRequest = z.input<\n typeof queryGroupingMembersSchema\n>;\n\nexport interface EntityGroupingCatalog {\n values: Array<{ value: string; count: number }>;\n total: number;\n}\nexport const GROUPING_RESERVED_FIELDS: ReadonlySet<string> = new Set([\n \"id\",\n \"entityType\",\n \"entity-type\",\n \"content\",\n \"contentHash\",\n \"content-hash\",\n \"created\",\n \"updated\",\n \"visibility\",\n \"metadata\",\n \"__proto__\",\n \"constructor\",\n \"prototype\",\n]);\n",
|
|
270
270
|
"import { z } from \"@brains/utils/zod\";\nimport { sql, type SQL, type SQLWrapper } from \"drizzle-orm\";\n\nexport type EntityIdPath = [string, ...string[]];\nexport type EntityIdPathInput = readonly [string, ...string[]];\n\nconst SEPARATOR = \":\";\n\nconst storedSegmentSchema = z\n .string()\n .refine((segment) => !segment.includes(SEPARATOR), {\n message: `Entity ID path segments cannot contain '${SEPARATOR}'`,\n });\n\n/** Navigation of existing identity is not validation of a new filesystem-safe path. */\nexport const storedEntityIdPathSchema: z.ZodType<EntityIdPath, unknown> =\n z.tuple([storedSegmentSchema], storedSegmentSchema);\n\nconst entityIdPathSegmentSchema = storedSegmentSchema\n .min(1)\n .refine((segment) => segment !== \".\" && segment !== \"..\", {\n message: \"Entity ID path segments cannot be dot segments\",\n })\n .refine((segment) => !segment.includes(\"/\") && !segment.includes(\"\\\\\"), {\n message: \"Entity ID path segments cannot contain path separators\",\n })\n .refine((segment) => !segment.includes(\"\\0\"), {\n message: \"Entity ID path segments cannot contain null bytes\",\n });\n\n/** Validates author input once; typed paths past this boundary are trusted. */\nexport const entityIdPathSchema: z.ZodType<EntityIdPath, unknown> = z.tuple(\n [entityIdPathSegmentSchema],\n entityIdPathSegmentSchema,\n);\n\n/** Serialize segments at a storage boundary; validate new author input separately. */\nexport function encodeEntityIdPath(path: EntityIdPathInput): string {\n return path.join(SEPARATOR);\n}\n\n/**\n * Decode stored identity losslessly, including values predating authoring validation.\n * Do not filter empty segments or normalize path-like values here: re-encoding must\n * reproduce the original ID. Filesystem placement belongs to directory-sync.\n */\nexport function decodeEntityIdPath(id: string): EntityIdPath {\n const [first = \"\", ...rest] = id.split(SEPARATOR);\n return [first, ...rest];\n}\n\n/** Database projection uses the same separator as encoding, never a second parser. */\nexport function entityIdHierarchyExpressions(\n id: SQLWrapper,\n prefix: EntityIdPathInput | null,\n): { withinPrefix: SQL; directChild: SQL; childName: SQL<ArrayBuffer> } {\n const prefixId =\n prefix === null ? \"\" : encodeEntityIdPath(prefix) + SEPARATOR;\n // Byte comparisons are case-sensitive, treat LIKE metacharacters literally,\n // and preserve Unicode and embedded NULs (SQLite text length stops at NUL).\n const bytes = sql`CAST(${id} AS BLOB)`;\n const prefixBytes = sql`CAST(${prefixId} AS BLOB)`;\n const remainder = sql`substr(${bytes}, length(${prefixBytes}) + 1)`;\n const separatorAt = sql`instr(${remainder}, CAST(${SEPARATOR} AS BLOB))`;\n return {\n // A half-open range preserves literal, binary prefix matching and lets\n // SQLite seek its existing ID index. The terminal separator supplies the\n // upper bound; callers never construct it or know its representation.\n withinPrefix:\n prefix === null\n ? sql`1 = 1`\n : sql`${id} >= ${prefixId} AND ${id} < ${encodeEntityIdPath(prefix) + String.fromCharCode(SEPARATOR.charCodeAt(0) + 1)}`,\n directChild: sql`${separatorAt} = 0`,\n childName: sql<ArrayBuffer>`substr(${remainder}, 1, ${separatorAt} - 1)`,\n };\n}\n",
|
|
271
271
|
"import type { Entity } from \"./schema/entities\";\nimport { z } from \"@brains/utils/zod\";\nimport { normalizeContentVisibility, type ContentVisibility } from \"./types\";\n\nconst metadataSchema = z.record(z.string(), z.unknown());\n\n/**\n * Normalized entity row shape used by query and serialization layers.\n * Database timestamps are stored as Unix milliseconds; serializers expose ISO strings.\n */\nexport interface EntityData {\n id: string;\n entityType: string;\n content: string;\n contentHash: string;\n visibility: ContentVisibility;\n created: number;\n updated: number;\n metadata: Record<string, unknown>;\n}\n\n/**\n * Convert a Drizzle entity row into the normalized shape consumed by serializers.\n */\nexport function normalizeEntityRow(row: Entity): EntityData {\n return {\n id: row.id,\n entityType: row.entityType,\n content: row.content,\n contentHash: row.contentHash,\n visibility: normalizeContentVisibility(row.visibility),\n created: row.created,\n updated: row.updated,\n metadata: metadataSchema.parse(row.metadata),\n };\n}\n",
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/dist/chunks/{linkedin-template-6kxt7ng4.js.map → linkedin-template-f7bt9fn1.js.map}
RENAMED
|
File without changes
|