@xemahq/kernel-contracts 0.25.0 → 0.27.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/biome/index.d.ts +1 -0
- package/dist/biome/index.d.ts.map +1 -1
- package/dist/biome/index.js +1 -0
- package/dist/biome/index.js.map +1 -1
- package/dist/biome/lib/biome-kind.d.ts +9 -0
- package/dist/biome/lib/biome-kind.d.ts.map +1 -0
- package/dist/biome/lib/biome-kind.js +13 -0
- package/dist/biome/lib/biome-kind.js.map +1 -0
- package/dist/biome/lib/biome-manifest.d.ts +9 -0
- package/dist/biome/lib/biome-manifest.d.ts.map +1 -1
- package/dist/biome/lib/biome-manifest.js +10 -0
- package/dist/biome/lib/biome-manifest.js.map +1 -1
- package/dist/connector/index.d.ts +1 -0
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -0
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/lib/connector-adapter-kind.d.ts +9 -0
- package/dist/connector/lib/connector-adapter-kind.d.ts.map +1 -0
- package/dist/connector/lib/connector-adapter-kind.js +13 -0
- package/dist/connector/lib/connector-adapter-kind.js.map +1 -0
- package/dist/connector/lib/connector-descriptor.d.ts +29 -1
- package/dist/connector/lib/connector-descriptor.d.ts.map +1 -1
- package/dist/connector/lib/connector-descriptor.js +10 -1
- package/dist/connector/lib/connector-descriptor.js.map +1 -1
- package/dist/connector/lib/integration-provider-contribution.d.ts +6 -0
- package/dist/connector/lib/integration-provider-contribution.d.ts.map +1 -1
- package/dist/object/lib/xema-object-ref.d.ts +5 -0
- package/dist/object/lib/xema-object-ref.d.ts.map +1 -1
- package/dist/object/lib/xema-object-ref.js +3 -2
- package/dist/object/lib/xema-object-ref.js.map +1 -1
- package/dist/search-source/index.d.ts +1 -0
- package/dist/search-source/index.d.ts.map +1 -1
- package/dist/search-source/index.js +1 -0
- package/dist/search-source/index.js.map +1 -1
- package/dist/search-source/lib/indexable-document.d.ts +28 -8
- package/dist/search-source/lib/indexable-document.d.ts.map +1 -1
- package/dist/search-source/lib/indexable-document.js +11 -4
- package/dist/search-source/lib/indexable-document.js.map +1 -1
- package/dist/search-source/lib/search-index-event.d.ts +30 -10
- package/dist/search-source/lib/search-index-event.d.ts.map +1 -1
- package/dist/search-source/lib/search-index-event.js +2 -3
- package/dist/search-source/lib/search-index-event.js.map +1 -1
- package/dist/search-source/lib/search-ref.d.ts +17 -0
- package/dist/search-source/lib/search-ref.d.ts.map +1 -0
- package/dist/search-source/lib/search-ref.js +72 -0
- package/dist/search-source/lib/search-ref.js.map +1 -0
- package/dist/search-source/lib/search-replay.d.ts +17 -4
- package/dist/search-source/lib/search-replay.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/biome/index.ts +1 -0
- package/src/biome/lib/biome-kind.ts +37 -0
- package/src/biome/lib/biome-manifest.ts +29 -0
- package/src/connector/index.ts +1 -0
- package/src/connector/lib/connector-adapter-kind.ts +44 -0
- package/src/connector/lib/connector-descriptor.ts +95 -1
- package/src/object/lib/xema-object-ref.ts +15 -3
- package/src/search-source/index.ts +1 -0
- package/src/search-source/lib/indexable-document.ts +72 -21
- package/src/search-source/lib/search-index-event.ts +10 -7
- package/src/search-source/lib/search-ref.ts +153 -0
|
@@ -1,29 +1,77 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
XemaObjectKindSchema,
|
|
5
|
+
XemaObjectRefSchema,
|
|
6
|
+
type XemaObjectKind,
|
|
7
|
+
type XemaObjectRef,
|
|
8
|
+
} from '../../object';
|
|
9
|
+
import {
|
|
10
|
+
ResourceVisibilityPatternSchema,
|
|
11
|
+
type ResourceVisibilityPattern,
|
|
12
|
+
} from '../../resource';
|
|
13
|
+
import {
|
|
14
|
+
DataClassificationSchema,
|
|
15
|
+
SpaceRefSchema,
|
|
16
|
+
type DataClassification,
|
|
17
|
+
type SpaceRef,
|
|
18
|
+
} from '../../space';
|
|
19
|
+
import { SubjectRefSchema, type SubjectRef } from '../../subject';
|
|
20
|
+
|
|
3
21
|
/**
|
|
4
|
-
* The canonical, source-AGNOSTIC indexable document
|
|
5
|
-
* projects its own domain entity into this shape
|
|
6
|
-
* search-api can index it WITHOUT importing the biome's
|
|
7
|
-
* entity schema.
|
|
22
|
+
* The canonical, source-AGNOSTIC indexable document (v2, object-model-aligned).
|
|
23
|
+
* A searchable biome projects its own domain entity into this shape
|
|
24
|
+
* (producer-side) so that search-api can index it WITHOUT importing the biome's
|
|
25
|
+
* client or knowing its entity schema.
|
|
26
|
+
*
|
|
27
|
+
* v2 realigns identity + scope onto the XSI Object Model:
|
|
28
|
+
*
|
|
29
|
+
* - `ref` is the natural key — `xema://<scope-path>/<docType>/<sourceId>`
|
|
30
|
+
* (see `formatSearchRef`). It replaces the v1 `sourceKey`+`docType`+`sourceId`
|
|
31
|
+
* triple as the addressable, scope-correct, authz-mappable identity.
|
|
32
|
+
* - `docType` is the REQUIRED type discriminator: biome-owned, open, and
|
|
33
|
+
* exactly what a `ContributionKind.SearchType` declares. A document is a
|
|
34
|
+
* `kb_page`, an `email`, a `backlog_item`, … by its `docType`.
|
|
35
|
+
* - `kind` is OPTIONAL enrichment: an object-model-native producer
|
|
36
|
+
* (knowledge-base → `KnowledgePage`, artifact-store → `Artifact`) also stamps
|
|
37
|
+
* the kernel `XemaObjectKind`; Layer-7 biome concepts (email, design-idea)
|
|
38
|
+
* omit it and rely on `docType` — that is why `kind` is NOT required (it would
|
|
39
|
+
* otherwise force domain concepts into the kernel's zero-domain enum).
|
|
40
|
+
* - `scope` is the {@link SpaceRef} that replaces the flat v1 `orgId`/`projectId`
|
|
41
|
+
* pair. search-api re-derives/validates it from the trusted event envelope —
|
|
42
|
+
* the producer copy is advisory.
|
|
43
|
+
* - `owner`, `resourceType`, `visibility` (and optional `classification`) are
|
|
44
|
+
* the authz descriptor: they map an instance onto the result-level permission
|
|
45
|
+
* model (§4). `resourceType`+`visibility` drive the tier-gate + FGA push-down;
|
|
46
|
+
* `owner` identifies the owning subject for `OwnerOnly`/`ExplicitShare`.
|
|
8
47
|
*
|
|
9
48
|
* Server-owned fields (vector embedding, chunk index, parent-doc linkage) are
|
|
10
49
|
* NOT part of this contract — search computes them from `content`. The producer
|
|
11
|
-
* supplies identity, scope, and the human/searchable text only.
|
|
12
|
-
*
|
|
13
|
-
* `sourceKey` + `docType` + `sourceId` is the stable natural key: re-emitting
|
|
14
|
-
* the same triple is an idempotent upsert.
|
|
50
|
+
* supplies identity, scope, ownership, and the human/searchable text only.
|
|
15
51
|
*/
|
|
16
52
|
export interface IndexableDocument {
|
|
17
|
-
/**
|
|
18
|
-
readonly
|
|
19
|
-
/**
|
|
53
|
+
/** Natural key: `xema://<scope-path>/<docType>/<sourceId>`. */
|
|
54
|
+
readonly ref: XemaObjectRef;
|
|
55
|
+
/**
|
|
56
|
+
* REQUIRED type discriminator — the biome-owned, open facet (e.g. `kb_page`,
|
|
57
|
+
* `email`, `backlog_item`). The `SearchType` contribution declares it.
|
|
58
|
+
*/
|
|
20
59
|
readonly docType: string;
|
|
21
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
60
|
+
/**
|
|
61
|
+
* OPTIONAL kernel object kind — populated only by object-model-native
|
|
62
|
+
* producers. Absent for Layer-7 biome content that has no kernel kind.
|
|
63
|
+
*/
|
|
64
|
+
readonly kind?: XemaObjectKind;
|
|
65
|
+
/** Tenant/ownership scope. Search re-derives/validates this from the event envelope. */
|
|
66
|
+
readonly scope: SpaceRef;
|
|
67
|
+
/** The subject that owns this document (drives owner-scoped result authz). */
|
|
68
|
+
readonly owner: SubjectRef;
|
|
69
|
+
/** Optional data-classification for policy obligations / redaction. */
|
|
70
|
+
readonly classification?: DataClassification;
|
|
71
|
+
/** FGA/authorization resource-type slug for the result-level ListObjects push-down. */
|
|
72
|
+
readonly resourceType: string;
|
|
73
|
+
/** How this instance is shared — the result-level visibility pattern. */
|
|
74
|
+
readonly visibility: ResourceVisibilityPattern;
|
|
27
75
|
readonly title?: string;
|
|
28
76
|
readonly summary?: string;
|
|
29
77
|
/** Full searchable text. */
|
|
@@ -62,11 +110,14 @@ export interface IndexableDocument {
|
|
|
62
110
|
*/
|
|
63
111
|
export const IndexableDocumentSchema = z
|
|
64
112
|
.object({
|
|
65
|
-
|
|
113
|
+
ref: XemaObjectRefSchema,
|
|
66
114
|
docType: z.string().min(1),
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
115
|
+
kind: XemaObjectKindSchema.optional(),
|
|
116
|
+
scope: SpaceRefSchema,
|
|
117
|
+
owner: SubjectRefSchema,
|
|
118
|
+
classification: DataClassificationSchema.optional(),
|
|
119
|
+
resourceType: z.string().min(1),
|
|
120
|
+
visibility: ResourceVisibilityPatternSchema,
|
|
70
121
|
title: z.string().optional(),
|
|
71
122
|
summary: z.string().optional(),
|
|
72
123
|
content: z.string().optional(),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
import { SpaceRefSchema, type SpaceRef } from '../../space';
|
|
3
4
|
import { IndexableDocumentSchema, type IndexableDocument } from './indexable-document';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -25,14 +26,18 @@ export interface SearchIndexUpsert {
|
|
|
25
26
|
readonly document: IndexableDocument;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Delete: drop the identified document from the index. v2-aligned with
|
|
31
|
+
* {@link IndexableDocument} — the flat `orgId`/`projectId` pair is replaced by
|
|
32
|
+
* the {@link SpaceRef} `scope` (search derives the per-org index from it). The
|
|
33
|
+
* document is identified by `docType` + `sourceId` (the same `sourceId` that is
|
|
34
|
+
* the trailing segment of the document's `ref`).
|
|
35
|
+
*/
|
|
29
36
|
export interface SearchIndexDelete {
|
|
30
37
|
readonly op: SearchIndexOp.Delete;
|
|
31
|
-
readonly
|
|
38
|
+
readonly scope: SpaceRef;
|
|
32
39
|
readonly docType: string;
|
|
33
40
|
readonly sourceId: string;
|
|
34
|
-
readonly orgId: string;
|
|
35
|
-
readonly projectId?: string;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
export type SearchIndexChange = SearchIndexUpsert | SearchIndexDelete;
|
|
@@ -47,10 +52,8 @@ export const SearchIndexUpsertSchema = z
|
|
|
47
52
|
export const SearchIndexDeleteSchema = z
|
|
48
53
|
.object({
|
|
49
54
|
op: z.literal(SearchIndexOp.Delete),
|
|
50
|
-
|
|
55
|
+
scope: SpaceRefSchema,
|
|
51
56
|
docType: z.string().min(1),
|
|
52
57
|
sourceId: z.string().min(1),
|
|
53
|
-
orgId: z.string().min(1),
|
|
54
|
-
projectId: z.string().min(1).optional(),
|
|
55
58
|
})
|
|
56
59
|
.strict();
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseObjectRefScopeSegments,
|
|
3
|
+
type XemaObjectRef,
|
|
4
|
+
} from '../../object';
|
|
5
|
+
import { SpaceKind, spaceRefToPath, type SpaceRef } from '../../space';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The natural-key / address grammar for a searchable document:
|
|
9
|
+
*
|
|
10
|
+
* `xema://<scope-path>/<docType>/<sourceId>`
|
|
11
|
+
*
|
|
12
|
+
* This is a SEARCH-SOURCE-LOCAL ref helper. It deliberately does NOT go through
|
|
13
|
+
* `parseXemaObjectRef`, because that parser requires a kernel `XemaObjectKind`
|
|
14
|
+
* in the type position — and the whole point of `IndexableDocument` v2 is that
|
|
15
|
+
* the type discriminator is `docType` (biome-owned, open), so a searchable
|
|
16
|
+
* document (an email, a design-idea, a backlog item) can be addressed WITHOUT a
|
|
17
|
+
* kernel object kind. The scope grammar itself is shared: this helper reuses
|
|
18
|
+
* {@link parseObjectRefScopeSegments} + {@link spaceRefToPath} so the
|
|
19
|
+
* `<scope-path>` portion stays value-identical to `XemaObjectRef`, and object-
|
|
20
|
+
* model ref validation is untouched for object-model callers.
|
|
21
|
+
*
|
|
22
|
+
* `docType` occupies the segment where an object ref carries `<kind>`; it is a
|
|
23
|
+
* lowercase, slash-free facet slug (e.g. `kb_page`, `email_thread`). `sourceId`
|
|
24
|
+
* is the source's stable per-entity id (what v1 carried as `sourceId`); it is
|
|
25
|
+
* opaque and slash-free but MAY contain other characters real ids use (`@`,
|
|
26
|
+
* `.`, `:`) — search refs are not versioned, so no `@version` suffix is parsed.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const SEARCH_REF_PREFIX = 'xema://';
|
|
30
|
+
|
|
31
|
+
/** A docType facet segment: lowercase, starts alnum, no slash/whitespace. */
|
|
32
|
+
const DOC_TYPE_PATTERN = /^[a-z0-9][a-z0-9_.-]*$/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Structured error raised by {@link parseSearchRef} / {@link formatSearchRef}.
|
|
36
|
+
* Carries a stable code so callers fail-fast with a typed error (no silent
|
|
37
|
+
* fallbacks).
|
|
38
|
+
*/
|
|
39
|
+
export class SearchRefParseError extends Error {
|
|
40
|
+
public readonly code = 'SEARCH_REF_INVALID';
|
|
41
|
+
public readonly ref: string;
|
|
42
|
+
public readonly reason: string;
|
|
43
|
+
|
|
44
|
+
public constructor(ref: string, reason: string) {
|
|
45
|
+
super(`Invalid search ref "${ref}": ${reason}`);
|
|
46
|
+
this.name = 'SearchRefParseError';
|
|
47
|
+
this.ref = ref;
|
|
48
|
+
this.reason = reason;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Parsed parts of a search ref. */
|
|
53
|
+
export interface SearchRefParts {
|
|
54
|
+
readonly scope: SpaceRef;
|
|
55
|
+
readonly docType: string;
|
|
56
|
+
readonly sourceId: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assertValidDocType(docType: string, refForError: string): void {
|
|
60
|
+
if (!DOC_TYPE_PATTERN.test(docType)) {
|
|
61
|
+
throw new SearchRefParseError(
|
|
62
|
+
refForError,
|
|
63
|
+
`docType "${docType}" must match ${DOC_TYPE_PATTERN}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function assertValidSourceId(sourceId: string, refForError: string): void {
|
|
69
|
+
if (sourceId.length === 0) {
|
|
70
|
+
throw new SearchRefParseError(refForError, 'sourceId must be non-empty');
|
|
71
|
+
}
|
|
72
|
+
if (/[/\s]/.test(sourceId)) {
|
|
73
|
+
throw new SearchRefParseError(
|
|
74
|
+
refForError,
|
|
75
|
+
`sourceId "${sourceId}" must not contain "/" or whitespace`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Compose a search ref `xema://<scope-path>/<docType>/<sourceId>`. Fail-fast on
|
|
82
|
+
* a malformed `docType`/`sourceId` or a scope missing a required segment
|
|
83
|
+
* ({@link spaceRefToPath} throws). Never silently coerces.
|
|
84
|
+
*/
|
|
85
|
+
export function formatSearchRef(
|
|
86
|
+
scope: SpaceRef,
|
|
87
|
+
docType: string,
|
|
88
|
+
sourceId: string,
|
|
89
|
+
): XemaObjectRef {
|
|
90
|
+
assertValidDocType(docType, '<unformatted>');
|
|
91
|
+
assertValidSourceId(sourceId, '<unformatted>');
|
|
92
|
+
const scopePath = spaceRefToPath(scope).replace(/^\//, '');
|
|
93
|
+
return `${SEARCH_REF_PREFIX}${scopePath}/${docType}/${sourceId}` as XemaObjectRef;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse a search ref back into `{ scope, docType, sourceId }`. Fail-fast on
|
|
98
|
+
* every malformed input. The scope grammar is validated by the shared
|
|
99
|
+
* {@link parseObjectRefScopeSegments}; the trailing two segments are the
|
|
100
|
+
* `docType` and the (slash-free) `sourceId`.
|
|
101
|
+
*/
|
|
102
|
+
export function parseSearchRef(ref: string): SearchRefParts {
|
|
103
|
+
if (!ref.startsWith(SEARCH_REF_PREFIX)) {
|
|
104
|
+
throw new SearchRefParseError(ref, `must start with "${SEARCH_REF_PREFIX}"`);
|
|
105
|
+
}
|
|
106
|
+
const body = ref.slice(SEARCH_REF_PREFIX.length);
|
|
107
|
+
if (body.length === 0) {
|
|
108
|
+
throw new SearchRefParseError(ref, 'empty body after scheme');
|
|
109
|
+
}
|
|
110
|
+
const segments = body.split('/').filter((segment) => segment.length > 0);
|
|
111
|
+
if (segments.length < 3) {
|
|
112
|
+
throw new SearchRefParseError(
|
|
113
|
+
ref,
|
|
114
|
+
'must contain at least <scope>/<docType>/<sourceId>',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const { scope, consumed } = parseObjectRefScopeSegments(segments, ref);
|
|
119
|
+
const remaining = segments.slice(consumed);
|
|
120
|
+
if (remaining.length !== 2) {
|
|
121
|
+
throw new SearchRefParseError(
|
|
122
|
+
ref,
|
|
123
|
+
`expected exactly <docType>/<sourceId> after the scope segments, got ${remaining.length}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const [docType, sourceId] = remaining as [string, string];
|
|
128
|
+
assertValidDocType(docType, ref);
|
|
129
|
+
assertValidSourceId(sourceId, ref);
|
|
130
|
+
return { scope, docType, sourceId };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Build the {@link SpaceRef} for a search document from the flat tenant scope
|
|
135
|
+
* every producer already holds: a `Project` space when `projectId` is present,
|
|
136
|
+
* otherwise an `Org` space. Centralizes the org/project → SpaceRef mapping so
|
|
137
|
+
* all five producers construct v2 scope identically. Fail-fast on a missing
|
|
138
|
+
* `orgId` (an org-less content document is unindexable — the per-org index has
|
|
139
|
+
* nowhere to place it).
|
|
140
|
+
*/
|
|
141
|
+
export function searchDocumentScope(
|
|
142
|
+
orgId: string,
|
|
143
|
+
projectId?: string,
|
|
144
|
+
): SpaceRef {
|
|
145
|
+
if (!orgId) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
'searchDocumentScope: orgId is required to scope a searchable document',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return projectId
|
|
151
|
+
? { tier: SpaceKind.Project, orgId, projectId }
|
|
152
|
+
: { tier: SpaceKind.Org, orgId };
|
|
153
|
+
}
|