@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.
Files changed (60) hide show
  1. package/dist/biome/index.d.ts +1 -0
  2. package/dist/biome/index.d.ts.map +1 -1
  3. package/dist/biome/index.js +1 -0
  4. package/dist/biome/index.js.map +1 -1
  5. package/dist/biome/lib/biome-kind.d.ts +9 -0
  6. package/dist/biome/lib/biome-kind.d.ts.map +1 -0
  7. package/dist/biome/lib/biome-kind.js +13 -0
  8. package/dist/biome/lib/biome-kind.js.map +1 -0
  9. package/dist/biome/lib/biome-manifest.d.ts +9 -0
  10. package/dist/biome/lib/biome-manifest.d.ts.map +1 -1
  11. package/dist/biome/lib/biome-manifest.js +10 -0
  12. package/dist/biome/lib/biome-manifest.js.map +1 -1
  13. package/dist/connector/index.d.ts +1 -0
  14. package/dist/connector/index.d.ts.map +1 -1
  15. package/dist/connector/index.js +1 -0
  16. package/dist/connector/index.js.map +1 -1
  17. package/dist/connector/lib/connector-adapter-kind.d.ts +9 -0
  18. package/dist/connector/lib/connector-adapter-kind.d.ts.map +1 -0
  19. package/dist/connector/lib/connector-adapter-kind.js +13 -0
  20. package/dist/connector/lib/connector-adapter-kind.js.map +1 -0
  21. package/dist/connector/lib/connector-descriptor.d.ts +29 -1
  22. package/dist/connector/lib/connector-descriptor.d.ts.map +1 -1
  23. package/dist/connector/lib/connector-descriptor.js +10 -1
  24. package/dist/connector/lib/connector-descriptor.js.map +1 -1
  25. package/dist/connector/lib/integration-provider-contribution.d.ts +6 -0
  26. package/dist/connector/lib/integration-provider-contribution.d.ts.map +1 -1
  27. package/dist/object/lib/xema-object-ref.d.ts +5 -0
  28. package/dist/object/lib/xema-object-ref.d.ts.map +1 -1
  29. package/dist/object/lib/xema-object-ref.js +3 -2
  30. package/dist/object/lib/xema-object-ref.js.map +1 -1
  31. package/dist/search-source/index.d.ts +1 -0
  32. package/dist/search-source/index.d.ts.map +1 -1
  33. package/dist/search-source/index.js +1 -0
  34. package/dist/search-source/index.js.map +1 -1
  35. package/dist/search-source/lib/indexable-document.d.ts +28 -8
  36. package/dist/search-source/lib/indexable-document.d.ts.map +1 -1
  37. package/dist/search-source/lib/indexable-document.js +11 -4
  38. package/dist/search-source/lib/indexable-document.js.map +1 -1
  39. package/dist/search-source/lib/search-index-event.d.ts +30 -10
  40. package/dist/search-source/lib/search-index-event.d.ts.map +1 -1
  41. package/dist/search-source/lib/search-index-event.js +2 -3
  42. package/dist/search-source/lib/search-index-event.js.map +1 -1
  43. package/dist/search-source/lib/search-ref.d.ts +17 -0
  44. package/dist/search-source/lib/search-ref.d.ts.map +1 -0
  45. package/dist/search-source/lib/search-ref.js +72 -0
  46. package/dist/search-source/lib/search-ref.js.map +1 -0
  47. package/dist/search-source/lib/search-replay.d.ts +17 -4
  48. package/dist/search-source/lib/search-replay.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/biome/index.ts +1 -0
  51. package/src/biome/lib/biome-kind.ts +37 -0
  52. package/src/biome/lib/biome-manifest.ts +29 -0
  53. package/src/connector/index.ts +1 -0
  54. package/src/connector/lib/connector-adapter-kind.ts +44 -0
  55. package/src/connector/lib/connector-descriptor.ts +95 -1
  56. package/src/object/lib/xema-object-ref.ts +15 -3
  57. package/src/search-source/index.ts +1 -0
  58. package/src/search-source/lib/indexable-document.ts +72 -21
  59. package/src/search-source/lib/search-index-event.ts +10 -7
  60. 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. A searchable biome
5
- * projects its own domain entity into this shape (producer-side) so that
6
- * search-api can index it WITHOUT importing the biome's client or knowing its
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
- /** Registered source that produced this document (e.g. `knowledge-base`). */
18
- readonly sourceKey: string;
19
- /** Entity kind within the source (e.g. `kb_page`, `backlog_item`). Open taxonomy owned by search. */
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
- /** The source's stable primary key for this entity. */
22
- readonly sourceId: string;
23
- /** Tenant scope. Search re-derives/validates this from the event envelope the producer copy is advisory. */
24
- readonly orgId: string;
25
- /** Project scope, when applicable. */
26
- readonly projectId?: string;
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
- sourceKey: z.string().min(1),
113
+ ref: XemaObjectRefSchema,
66
114
  docType: z.string().min(1),
67
- sourceId: z.string().min(1),
68
- orgId: z.string().min(1),
69
- projectId: z.string().min(1).optional(),
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
- /** Delete: drop the identified document (or whole source scope) from the index. */
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 sourceKey: string;
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
- sourceKey: z.string().min(1),
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
+ }