@xemahq/kernel-contracts 0.50.0 → 0.57.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/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 +2 -0
- package/dist/biome/lib/biome-manifest.js.map +1 -1
- package/dist/capability/index.d.ts +3 -1
- package/dist/capability/index.d.ts.map +1 -1
- package/dist/capability/index.js +3 -1
- package/dist/capability/index.js.map +1 -1
- package/dist/capability/lib/authority-effect.d.ts +8 -0
- package/dist/capability/lib/authority-effect.d.ts.map +1 -0
- package/dist/capability/lib/authority-effect.js +17 -0
- package/dist/capability/lib/authority-effect.js.map +1 -0
- package/dist/capability/lib/capability-contribution.d.ts +4 -0
- package/dist/capability/lib/capability-contribution.d.ts.map +1 -1
- package/dist/capability/lib/capability-contribution.js +4 -0
- package/dist/capability/lib/capability-contribution.js.map +1 -1
- package/dist/capability/lib/capability-domain.d.ts +8 -0
- package/dist/capability/lib/capability-domain.d.ts.map +1 -0
- package/dist/capability/lib/capability-domain.js +24 -0
- package/dist/capability/lib/capability-domain.js.map +1 -0
- package/dist/capability/lib/capability-reach.d.ts +13 -0
- package/dist/capability/lib/capability-reach.d.ts.map +1 -0
- package/dist/capability/lib/capability-reach.js +34 -0
- package/dist/capability/lib/capability-reach.js.map +1 -0
- package/dist/capability/lib/meta-tool.d.ts +51 -14
- package/dist/capability/lib/meta-tool.d.ts.map +1 -1
- package/dist/capability/lib/meta-tool.js +66 -12
- package/dist/capability/lib/meta-tool.js.map +1 -1
- package/dist/capability-projection/index.d.ts +4 -0
- package/dist/capability-projection/index.d.ts.map +1 -0
- package/dist/capability-projection/index.js +20 -0
- package/dist/capability-projection/index.js.map +1 -0
- package/dist/capability-projection/lib/capability-invocation-binding.d.ts +72 -0
- package/dist/capability-projection/lib/capability-invocation-binding.d.ts.map +1 -0
- package/dist/capability-projection/lib/capability-invocation-binding.js +101 -0
- package/dist/capability-projection/lib/capability-invocation-binding.js.map +1 -0
- package/dist/capability-projection/lib/capability-sync-manifest.d.ts +8 -0
- package/dist/capability-projection/lib/capability-sync-manifest.d.ts.map +1 -0
- package/dist/capability-projection/lib/capability-sync-manifest.js +8 -0
- package/dist/capability-projection/lib/capability-sync-manifest.js.map +1 -0
- package/dist/capability-projection/lib/projected-capability.d.ts +15 -0
- package/dist/capability-projection/lib/projected-capability.d.ts.map +1 -0
- package/dist/capability-projection/lib/projected-capability.js +17 -0
- package/dist/capability-projection/lib/projected-capability.js.map +1 -0
- package/dist/connector/lib/connector-adapter-contribution.d.ts +2 -0
- package/dist/connector/lib/connector-adapter-contribution.d.ts.map +1 -1
- package/dist/connector/lib/onboarding-manifest.d.ts +2 -0
- package/dist/connector/lib/onboarding-manifest.d.ts.map +1 -1
- package/dist/connector/lib/onboarding-manifest.js +1 -0
- package/dist/connector/lib/onboarding-manifest.js.map +1 -1
- package/dist/connector/lib/provider-descriptor.d.ts +2 -0
- package/dist/connector/lib/provider-descriptor.d.ts.map +1 -1
- package/dist/contribution/lib/contribution-kind.d.ts +1 -6
- package/dist/contribution/lib/contribution-kind.d.ts.map +1 -1
- package/dist/contribution/lib/contribution-kind.js +1 -6
- package/dist/contribution/lib/contribution-kind.js.map +1 -1
- package/dist/distribution/index.d.ts +1 -0
- package/dist/distribution/index.d.ts.map +1 -1
- package/dist/distribution/index.js +1 -0
- package/dist/distribution/index.js.map +1 -1
- package/dist/distribution/lib/distribution-lock.d.ts +6 -0
- package/dist/distribution/lib/distribution-lock.d.ts.map +1 -1
- package/dist/distribution/lib/distribution-lock.js +9 -1
- package/dist/distribution/lib/distribution-lock.js.map +1 -1
- package/dist/distribution/lib/first-party-capability-domains.d.ts +15 -0
- package/dist/distribution/lib/first-party-capability-domains.d.ts.map +1 -0
- package/dist/distribution/lib/first-party-capability-domains.js +52 -0
- package/dist/distribution/lib/first-party-capability-domains.js.map +1 -0
- package/dist/object/lib/xema-object-ref.d.ts +0 -5
- package/dist/object/lib/xema-object-ref.d.ts.map +1 -1
- package/dist/object/lib/xema-object-ref.js +12 -62
- package/dist/object/lib/xema-object-ref.js.map +1 -1
- package/dist/output-surface-inspect/index.d.ts +1 -0
- package/dist/output-surface-inspect/index.d.ts.map +1 -1
- package/dist/output-surface-inspect/index.js +1 -0
- package/dist/output-surface-inspect/index.js.map +1 -1
- package/dist/output-surface-inspect/lib/buffers.d.ts +52 -0
- package/dist/output-surface-inspect/lib/buffers.d.ts.map +1 -0
- package/dist/output-surface-inspect/lib/buffers.js +132 -0
- package/dist/output-surface-inspect/lib/buffers.js.map +1 -0
- package/dist/output-surface-inspect/lib/output-surface-inspect.d.ts +6 -1
- package/dist/output-surface-inspect/lib/output-surface-inspect.d.ts.map +1 -1
- package/dist/output-surface-inspect/lib/output-surface-inspect.js +12 -1
- package/dist/output-surface-inspect/lib/output-surface-inspect.js.map +1 -1
- package/dist/output-surface-inspect/lib/step-schema.d.ts +2 -1
- package/dist/output-surface-inspect/lib/step-schema.d.ts.map +1 -1
- package/dist/output-surface-inspect/lib/step-schema.js +9 -2
- package/dist/output-surface-inspect/lib/step-schema.js.map +1 -1
- package/dist/output-surface-inspect/runtime/index.d.ts +1 -0
- package/dist/output-surface-inspect/runtime/index.d.ts.map +1 -1
- package/dist/output-surface-inspect/runtime/index.js +1 -0
- package/dist/output-surface-inspect/runtime/index.js.map +1 -1
- package/dist/search/index.d.ts +1 -0
- package/dist/search/index.d.ts.map +1 -1
- package/dist/search/index.js +1 -0
- package/dist/search/index.js.map +1 -1
- package/dist/search/lib/route-template.d.ts +31 -0
- package/dist/search/lib/route-template.d.ts.map +1 -0
- package/dist/search/lib/route-template.js +142 -0
- package/dist/search/lib/route-template.js.map +1 -0
- package/dist/search/lib/search-contribution.d.ts +4 -2
- package/dist/search/lib/search-contribution.d.ts.map +1 -1
- package/dist/search/lib/search-contribution.js +46 -4
- package/dist/search/lib/search-contribution.js.map +1 -1
- package/dist/search-source/lib/indexable-document.d.ts +22 -0
- package/dist/search-source/lib/indexable-document.d.ts.map +1 -1
- package/dist/search-source/lib/indexable-document.js +10 -1
- package/dist/search-source/lib/indexable-document.js.map +1 -1
- package/dist/search-source/lib/search-index-event.d.ts +10 -0
- package/dist/search-source/lib/search-index-event.d.ts.map +1 -1
- package/dist/search-source/lib/search-ref.d.ts.map +1 -1
- package/dist/search-source/lib/search-ref.js +11 -2
- package/dist/search-source/lib/search-ref.js.map +1 -1
- package/dist/search-source/lib/search-replay.d.ts +10 -0
- package/dist/search-source/lib/search-replay.d.ts.map +1 -1
- package/dist/service-registry/lib/service-descriptor.d.ts +1 -0
- package/dist/service-registry/lib/service-descriptor.d.ts.map +1 -1
- package/dist/space/lib/space-ref-parser.d.ts +6 -4
- package/dist/space/lib/space-ref-parser.d.ts.map +1 -1
- package/dist/space/lib/space-ref-parser.js +90 -126
- package/dist/space/lib/space-ref-parser.js.map +1 -1
- package/dist/space/lib/space-traversal.d.ts.map +1 -1
- package/dist/space/lib/space-traversal.js +9 -13
- package/dist/space/lib/space-traversal.js.map +1 -1
- package/dist/space/lib/space.d.ts +7 -0
- package/dist/space/lib/space.d.ts.map +1 -1
- package/dist/space/lib/space.js +48 -29
- package/dist/space/lib/space.js.map +1 -1
- package/dist/subject/lib/subject.d.ts +1 -0
- package/dist/subject/lib/subject.d.ts.map +1 -1
- package/dist/subject/lib/subject.js +1 -0
- package/dist/subject/lib/subject.js.map +1 -1
- package/dist/surface/index.d.ts +4 -0
- package/dist/surface/index.d.ts.map +1 -0
- package/dist/surface/index.js +20 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/surface/lib/surface-contribution.d.ts +24 -0
- package/dist/surface/lib/surface-contribution.d.ts.map +1 -0
- package/dist/surface/lib/surface-contribution.js +23 -0
- package/dist/surface/lib/surface-contribution.js.map +1 -0
- package/dist/surface/lib/surface-kind.d.ts +23 -0
- package/dist/surface/lib/surface-kind.d.ts.map +1 -0
- package/dist/surface/lib/surface-kind.js +27 -0
- package/dist/surface/lib/surface-kind.js.map +1 -0
- package/dist/surface/lib/vista-surface.d.ts +11 -0
- package/dist/surface/lib/vista-surface.d.ts.map +1 -0
- package/dist/surface/lib/vista-surface.js +13 -0
- package/dist/surface/lib/vista-surface.js.map +1 -0
- package/dist/widget/lib/chat-widget-envelope.d.ts.map +1 -1
- package/dist/widget/lib/chat-widget-envelope.js.map +1 -1
- package/dist/workflow/lib/enums.d.ts.map +1 -1
- package/dist/workflow/lib/enums.js.map +1 -1
- package/package.json +1 -1
- package/src/biome/lib/biome-manifest.ts +41 -1
- package/src/capability/index.ts +3 -1
- package/src/capability/lib/authority-effect.ts +88 -0
- package/src/capability/lib/capability-contribution.ts +44 -0
- package/src/capability/lib/capability-domain.ts +51 -0
- package/src/capability/lib/capability-grant.ts +1 -1
- package/src/capability/lib/capability-reach.ts +119 -0
- package/src/capability/lib/meta-tool.ts +172 -39
- package/src/capability-projection/index.ts +3 -0
- package/src/capability-projection/lib/capability-invocation-binding.ts +223 -0
- package/src/capability-projection/lib/capability-sync-manifest.ts +52 -0
- package/src/capability-projection/lib/projected-capability.ts +63 -0
- package/src/connector/lib/onboarding-manifest.ts +16 -0
- package/src/contribution/lib/contribution-kind.ts +165 -61
- package/src/distribution/index.ts +1 -0
- package/src/distribution/lib/distribution-lock.ts +36 -0
- package/src/distribution/lib/first-party-capability-domains.ts +166 -0
- package/src/object/lib/xema-object-ref.ts +27 -82
- package/src/output-surface-inspect/index.ts +2 -0
- package/src/output-surface-inspect/lib/buffers.ts +252 -0
- package/src/output-surface-inspect/lib/output-surface-inspect.ts +65 -6
- package/src/output-surface-inspect/lib/step-schema.ts +36 -3
- package/src/output-surface-inspect/runtime/index.ts +1 -0
- package/src/search/index.ts +1 -0
- package/src/search/lib/route-template.ts +381 -0
- package/src/search/lib/search-contribution.ts +138 -31
- package/src/search-source/lib/indexable-document.ts +44 -0
- package/src/search-source/lib/search-ref.ts +22 -9
- package/src/service-registry/lib/service-descriptor.ts +16 -0
- package/src/space/lib/skill-mirror-path.ts +1 -1
- package/src/space/lib/space-ref-parser.ts +190 -165
- package/src/space/lib/space-traversal.ts +34 -17
- package/src/space/lib/space.ts +110 -35
- package/src/subject/lib/subject.ts +16 -0
- package/src/surface/index.ts +3 -0
- package/src/surface/lib/surface-contribution.ts +101 -0
- package/src/surface/lib/surface-kind.ts +65 -0
- package/src/surface/lib/vista-surface.ts +44 -0
- package/src/widget/lib/chat-widget-envelope.ts +6 -2
- package/src/workflow/lib/enums.ts +6 -7
- package/src/capability/lib/first-party-capability-domains.ts +0 -77
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `routeTemplate` — the deep-link grammar a search RESULT-TYPE contributes so a
|
|
3
|
+
* hit can be turned into a concrete frontend URL WITHOUT any consumer
|
|
4
|
+
* hardcoding a per-kind switch.
|
|
5
|
+
*
|
|
6
|
+
* A template is a path pattern such as
|
|
7
|
+
* `/spaces/projects/{projectId}/design-system-builder/{sourceId}`. The
|
|
8
|
+
* placeholder vocabulary is a CLOSED set ({@link SearchRouteTemplateVariable})
|
|
9
|
+
* — exactly the fields a caller is guaranteed to hold for a search hit or an
|
|
10
|
+
* object ref. Anything outside that set is a contribution bug, rejected at
|
|
11
|
+
* parse time rather than discovered as a broken link in production.
|
|
12
|
+
*
|
|
13
|
+
* Everything here is fail-fast: an unknown placeholder, a malformed template,
|
|
14
|
+
* or a placeholder with no value at expansion time throws
|
|
15
|
+
* {@link SearchRouteTemplateError}. Expansion NEVER leaves a `{x}` in the
|
|
16
|
+
* output and NEVER silently drops a segment — a half-substituted URL is worse
|
|
17
|
+
* than no URL, because it looks clickable.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The closed set of placeholders a `routeTemplate` may reference.
|
|
22
|
+
*
|
|
23
|
+
* Every member has exactly ONE documented supply route — see
|
|
24
|
+
* {@link SEARCH_ROUTE_TEMPLATE_VARIABLE_SOURCE}:
|
|
25
|
+
*
|
|
26
|
+
* | Variable | Source | Search hit | Object ref |
|
|
27
|
+
* | --------------- | ------------ | --------------------------------------- | ------------------------------ |
|
|
28
|
+
* | `orgId` | `scope` | the hit's tenant org | the ref's scope org |
|
|
29
|
+
* | `projectId` | `scope` | the hit's project (org-wide ⇒ n/a) | the ref's scope project |
|
|
30
|
+
* | `docType` | `identity` | the hit's open, biome-owned facet | n/a (object refs carry a kind) |
|
|
31
|
+
* | `objectKind` | `identity` | the registered result-type's kind | the ref's `XemaObjectKind` |
|
|
32
|
+
* | `sourceId` | `identity` | the source's stable entity id | n/a |
|
|
33
|
+
* | `title` | `identity` | the hit's title | n/a |
|
|
34
|
+
* | `slug` | `projected` | `IndexableDocument.routeParams.slug` | the ref's slug segment |
|
|
35
|
+
* | `containerSlug` | `projected` | `IndexableDocument.routeParams.containerSlug` | n/a |
|
|
36
|
+
*
|
|
37
|
+
* `scope`/`identity` variables are derived by the PLATFORM from the hit or ref
|
|
38
|
+
* itself — always available, never declared. `projected` variables can only
|
|
39
|
+
* come from the owning source, so a result-type that references one MUST
|
|
40
|
+
* declare it in `renderHints.routeParams`; a template referencing an
|
|
41
|
+
* undeclared projected variable is rejected at contribution-parse time
|
|
42
|
+
* (see `assertSearchRouteTemplateSatisfiable`). That is what keeps a
|
|
43
|
+
* slug-based route from silently becoming a dead link.
|
|
44
|
+
*
|
|
45
|
+
* A template referencing a variable the caller cannot supply at EXPANSION time
|
|
46
|
+
* still simply does not expand for that caller (fail-fast, no URL) — a
|
|
47
|
+
* result-type may legitimately declare a route only the object-ref caller can
|
|
48
|
+
* resolve.
|
|
49
|
+
*
|
|
50
|
+
* Adding a member is a deliberate kernel change: it must be classified in
|
|
51
|
+
* {@link SEARCH_ROUTE_TEMPLATE_VARIABLE_SOURCE}, and every consumer that
|
|
52
|
+
* expands a template must be able to supply the new value.
|
|
53
|
+
*/
|
|
54
|
+
export enum SearchRouteTemplateVariable {
|
|
55
|
+
/** Owning organization id. */
|
|
56
|
+
OrgId = 'orgId',
|
|
57
|
+
/** Owning project id. Absent for org-wide documents. */
|
|
58
|
+
ProjectId = 'projectId',
|
|
59
|
+
/** The open, biome-contributed doc-type facet. */
|
|
60
|
+
DocType = 'docType',
|
|
61
|
+
/** The `XemaObjectKind` the result-type covers. */
|
|
62
|
+
ObjectKind = 'objectKind',
|
|
63
|
+
/** The source's stable per-entity id (what a search ref addresses). */
|
|
64
|
+
SourceId = 'sourceId',
|
|
65
|
+
/**
|
|
66
|
+
* The instance's own URL slug — the human-readable, route-bearing key a
|
|
67
|
+
* slug-addressed frontend uses instead of an opaque id. Projected by the
|
|
68
|
+
* producer onto `IndexableDocument.routeParams.slug`; an object ref supplies
|
|
69
|
+
* it intrinsically from its slug segment.
|
|
70
|
+
*/
|
|
71
|
+
Slug = 'slug',
|
|
72
|
+
/** The instance's human title. */
|
|
73
|
+
Title = 'title',
|
|
74
|
+
/**
|
|
75
|
+
* The slug of the instance's IMMEDIATE container — the collection a
|
|
76
|
+
* slug-addressed route nests the instance under (a knowledge-base space, a
|
|
77
|
+
* folder, a collection). Kernel-neutral on purpose: it is NOT the Xema
|
|
78
|
+
* `SpaceRef` tier (that is `orgId`/`projectId`), it is the domain container
|
|
79
|
+
* whose slug occupies a route segment. Projected by the producer onto
|
|
80
|
+
* `IndexableDocument.routeParams.containerSlug`; an object ref cannot supply
|
|
81
|
+
* it, so a container-nested template resolves for hits only.
|
|
82
|
+
*/
|
|
83
|
+
ContainerSlug = 'containerSlug',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Where the value of a {@link SearchRouteTemplateVariable} comes from. Closed
|
|
88
|
+
* set — the classification is what makes "can this contribution actually supply
|
|
89
|
+
* this placeholder?" a decidable, parse-time question.
|
|
90
|
+
*/
|
|
91
|
+
export enum SearchRouteVariableSource {
|
|
92
|
+
/** Derived by the platform from the result's `SpaceRef` scope. */
|
|
93
|
+
Scope = 'scope',
|
|
94
|
+
/** Carried by the result's own identity (hit fields / ref segments). */
|
|
95
|
+
Identity = 'identity',
|
|
96
|
+
/**
|
|
97
|
+
* Projected by the OWNING SOURCE — the producer stamps it on
|
|
98
|
+
* `IndexableDocument.routeParams` (and/or an object ref carries it). The
|
|
99
|
+
* platform cannot derive it, so a result-type must DECLARE it in
|
|
100
|
+
* `renderHints.routeParams` before a template may reference it.
|
|
101
|
+
*/
|
|
102
|
+
Projected = 'projected',
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The single source-of-truth classification of every placeholder. Frozen: a
|
|
107
|
+
* consumer must not be able to reclassify a variable at runtime.
|
|
108
|
+
*/
|
|
109
|
+
export const SEARCH_ROUTE_TEMPLATE_VARIABLE_SOURCE: Readonly<
|
|
110
|
+
Record<SearchRouteTemplateVariable, SearchRouteVariableSource>
|
|
111
|
+
> = Object.freeze({
|
|
112
|
+
[SearchRouteTemplateVariable.OrgId]: SearchRouteVariableSource.Scope,
|
|
113
|
+
[SearchRouteTemplateVariable.ProjectId]: SearchRouteVariableSource.Scope,
|
|
114
|
+
[SearchRouteTemplateVariable.DocType]: SearchRouteVariableSource.Identity,
|
|
115
|
+
[SearchRouteTemplateVariable.ObjectKind]: SearchRouteVariableSource.Identity,
|
|
116
|
+
[SearchRouteTemplateVariable.SourceId]: SearchRouteVariableSource.Identity,
|
|
117
|
+
[SearchRouteTemplateVariable.Title]: SearchRouteVariableSource.Identity,
|
|
118
|
+
[SearchRouteTemplateVariable.Slug]: SearchRouteVariableSource.Projected,
|
|
119
|
+
[SearchRouteTemplateVariable.ContainerSlug]:
|
|
120
|
+
SearchRouteVariableSource.Projected,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The placeholders a result-type must DECLARE (`renderHints.routeParams`) and a
|
|
125
|
+
* producer must STAMP (`IndexableDocument.routeParams`) before a template may
|
|
126
|
+
* reference them. Derived from {@link SEARCH_ROUTE_TEMPLATE_VARIABLE_SOURCE} so
|
|
127
|
+
* the two can never drift.
|
|
128
|
+
*/
|
|
129
|
+
export const SEARCH_ROUTE_PROJECTED_VARIABLES: ReadonlySet<SearchRouteTemplateVariable> =
|
|
130
|
+
new Set(
|
|
131
|
+
(
|
|
132
|
+
Object.values(SearchRouteTemplateVariable) as SearchRouteTemplateVariable[]
|
|
133
|
+
).filter(
|
|
134
|
+
(variable) =>
|
|
135
|
+
SEARCH_ROUTE_TEMPLATE_VARIABLE_SOURCE[variable] ===
|
|
136
|
+
SearchRouteVariableSource.Projected,
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
/** Whether a placeholder must be projected by the owning source. */
|
|
141
|
+
export function isProjectedSearchRouteVariable(
|
|
142
|
+
variable: SearchRouteTemplateVariable,
|
|
143
|
+
): boolean {
|
|
144
|
+
return SEARCH_ROUTE_PROJECTED_VARIABLES.has(variable);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Every legal placeholder name, as a lookup set. Frozen so a consumer cannot
|
|
149
|
+
* widen the vocabulary at runtime.
|
|
150
|
+
*/
|
|
151
|
+
export const SEARCH_ROUTE_TEMPLATE_VARIABLES: ReadonlySet<string> = new Set(
|
|
152
|
+
Object.values(SearchRouteTemplateVariable),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Matches one `{placeholder}` occurrence. Deliberately permissive about the
|
|
157
|
+
* NAME (`[A-Za-z0-9_]+`) so an unknown-but-well-formed placeholder is reported
|
|
158
|
+
* as "unknown variable" rather than "malformed template" — the actionable
|
|
159
|
+
* message is the whole point.
|
|
160
|
+
*
|
|
161
|
+
* Global flag: callers must not rely on `lastIndex` (use `matchAll`, or reset).
|
|
162
|
+
*/
|
|
163
|
+
export const SEARCH_ROUTE_TEMPLATE_PLACEHOLDER_PATTERN = /\{([A-Za-z0-9_]+)\}/g;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Structured error raised by every function in this module. Carries a stable
|
|
167
|
+
* `code` so HTTP edges can map it to a typed response instead of a 500.
|
|
168
|
+
*/
|
|
169
|
+
export class SearchRouteTemplateError extends Error {
|
|
170
|
+
public readonly code = 'SEARCH_ROUTE_TEMPLATE_INVALID';
|
|
171
|
+
public readonly template: string;
|
|
172
|
+
public readonly reason: string;
|
|
173
|
+
|
|
174
|
+
public constructor(template: string, reason: string) {
|
|
175
|
+
super(`Invalid search route template "${template}": ${reason}`);
|
|
176
|
+
this.name = 'SearchRouteTemplateError';
|
|
177
|
+
this.template = template;
|
|
178
|
+
this.reason = reason;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function allowedVariableList(): string {
|
|
183
|
+
return [...SEARCH_ROUTE_TEMPLATE_VARIABLES].sort().join(', ');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Assert a `routeTemplate` is well-formed. A template MUST:
|
|
188
|
+
*
|
|
189
|
+
* - be non-empty and start with `/` (it is a site-relative path, never an
|
|
190
|
+
* absolute URL and never a relative fragment);
|
|
191
|
+
* - contain no whitespace;
|
|
192
|
+
* - contain no empty path segment (`//`);
|
|
193
|
+
* - use only well-formed `{name}` placeholders whose `name` is a
|
|
194
|
+
* {@link SearchRouteTemplateVariable}.
|
|
195
|
+
*
|
|
196
|
+
* Throws {@link SearchRouteTemplateError} on the first violation. Never
|
|
197
|
+
* "repairs" a template.
|
|
198
|
+
*/
|
|
199
|
+
export function assertValidSearchRouteTemplate(template: string): void {
|
|
200
|
+
if (template.length === 0) {
|
|
201
|
+
throw new SearchRouteTemplateError(template, 'must be non-empty');
|
|
202
|
+
}
|
|
203
|
+
if (!template.startsWith('/')) {
|
|
204
|
+
throw new SearchRouteTemplateError(
|
|
205
|
+
template,
|
|
206
|
+
'must be a site-relative path starting with "/"',
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (/\s/.test(template)) {
|
|
210
|
+
throw new SearchRouteTemplateError(template, 'must not contain whitespace');
|
|
211
|
+
}
|
|
212
|
+
if (template.includes('//')) {
|
|
213
|
+
throw new SearchRouteTemplateError(
|
|
214
|
+
template,
|
|
215
|
+
'must not contain an empty path segment ("//")',
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Strip well-formed placeholders; any residual brace is a malformed one.
|
|
220
|
+
const residue = template.replace(
|
|
221
|
+
SEARCH_ROUTE_TEMPLATE_PLACEHOLDER_PATTERN,
|
|
222
|
+
'',
|
|
223
|
+
);
|
|
224
|
+
if (residue.includes('{') || residue.includes('}')) {
|
|
225
|
+
throw new SearchRouteTemplateError(
|
|
226
|
+
template,
|
|
227
|
+
'contains a malformed placeholder — every placeholder must be "{name}" with name matching [A-Za-z0-9_]+',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const name of readTemplateVariableNames(template)) {
|
|
232
|
+
if (!SEARCH_ROUTE_TEMPLATE_VARIABLES.has(name)) {
|
|
233
|
+
throw new SearchRouteTemplateError(
|
|
234
|
+
template,
|
|
235
|
+
`unknown placeholder "{${name}}" — allowed placeholders are: ${allowedVariableList()}`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Assert a `routeTemplate` can ACTUALLY be expanded by the result-type that
|
|
243
|
+
* declares it — the second half of contribution-parse-time validation.
|
|
244
|
+
*
|
|
245
|
+
* {@link assertValidSearchRouteTemplate} answers "is this placeholder in the
|
|
246
|
+
* closed vocabulary?"; this answers "can this contribution supply it?". Every
|
|
247
|
+
* {@link SearchRouteVariableSource.Projected} placeholder a template references
|
|
248
|
+
* MUST appear in `provided` (the contribution's `renderHints.routeParams`,
|
|
249
|
+
* i.e. what the producer stamps on `IndexableDocument.routeParams`).
|
|
250
|
+
* `scope`/`identity` placeholders are derived by the platform and are never
|
|
251
|
+
* declared.
|
|
252
|
+
*
|
|
253
|
+
* Also rejects a DEAD declaration — a `provided` entry the template never
|
|
254
|
+
* references — because an unused supply promise is a contribution bug that
|
|
255
|
+
* silently misleads the next reader.
|
|
256
|
+
*
|
|
257
|
+
* Throws {@link SearchRouteTemplateError} on the first violation.
|
|
258
|
+
*/
|
|
259
|
+
export function assertSearchRouteTemplateSatisfiable(
|
|
260
|
+
template: string,
|
|
261
|
+
provided: readonly SearchRouteTemplateVariable[],
|
|
262
|
+
): void {
|
|
263
|
+
assertValidSearchRouteTemplate(template);
|
|
264
|
+
|
|
265
|
+
const declared = new Set<SearchRouteTemplateVariable>();
|
|
266
|
+
for (const variable of provided) {
|
|
267
|
+
if (!SEARCH_ROUTE_TEMPLATE_VARIABLES.has(variable)) {
|
|
268
|
+
throw new SearchRouteTemplateError(
|
|
269
|
+
template,
|
|
270
|
+
`declared route param "${String(variable)}" is not a search route variable — ` +
|
|
271
|
+
`allowed variables are: ${allowedVariableList()}`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (!isProjectedSearchRouteVariable(variable)) {
|
|
275
|
+
throw new SearchRouteTemplateError(
|
|
276
|
+
template,
|
|
277
|
+
`route param "${variable}" must not be declared — it is supplied by the platform ` +
|
|
278
|
+
`(source "${SEARCH_ROUTE_TEMPLATE_VARIABLE_SOURCE[variable]}"), not by the producer. ` +
|
|
279
|
+
`Only ${projectedVariableList()} may be declared.`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
if (declared.has(variable)) {
|
|
283
|
+
throw new SearchRouteTemplateError(
|
|
284
|
+
template,
|
|
285
|
+
`route param "${variable}" is declared more than once`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
declared.add(variable);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const referenced = new Set(
|
|
292
|
+
readTemplateVariableNames(template) as SearchRouteTemplateVariable[],
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
for (const variable of referenced) {
|
|
296
|
+
if (isProjectedSearchRouteVariable(variable) && !declared.has(variable)) {
|
|
297
|
+
throw new SearchRouteTemplateError(
|
|
298
|
+
template,
|
|
299
|
+
`placeholder "{${variable}}" is projected by the owning source, but this result-type ` +
|
|
300
|
+
`does not declare it in renderHints.routeParams — the template could never expand. ` +
|
|
301
|
+
`Either declare it (and stamp it on IndexableDocument.routeParams) or drop it from the template.`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const variable of declared) {
|
|
307
|
+
if (!referenced.has(variable)) {
|
|
308
|
+
throw new SearchRouteTemplateError(
|
|
309
|
+
template,
|
|
310
|
+
`route param "${variable}" is declared but never referenced by the template — ` +
|
|
311
|
+
'remove the dead declaration',
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function projectedVariableList(): string {
|
|
318
|
+
return [...SEARCH_ROUTE_PROJECTED_VARIABLES].sort().join(', ');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Raw placeholder names, in order of first appearance, duplicates removed. */
|
|
322
|
+
function readTemplateVariableNames(template: string): string[] {
|
|
323
|
+
const seen = new Set<string>();
|
|
324
|
+
for (const match of template.matchAll(
|
|
325
|
+
SEARCH_ROUTE_TEMPLATE_PLACEHOLDER_PATTERN,
|
|
326
|
+
)) {
|
|
327
|
+
const name = match[1];
|
|
328
|
+
if (name !== undefined) {
|
|
329
|
+
seen.add(name);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return [...seen];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The distinct {@link SearchRouteTemplateVariable}s a template references, in
|
|
337
|
+
* order of first appearance. Validates first, so the result is always a
|
|
338
|
+
* well-typed subset of the closed vocabulary — consumers use this to decide
|
|
339
|
+
* whether they can supply everything a template needs BEFORE attempting an
|
|
340
|
+
* expansion.
|
|
341
|
+
*/
|
|
342
|
+
export function extractSearchRouteTemplateVariables(
|
|
343
|
+
template: string,
|
|
344
|
+
): SearchRouteTemplateVariable[] {
|
|
345
|
+
assertValidSearchRouteTemplate(template);
|
|
346
|
+
return readTemplateVariableNames(template) as SearchRouteTemplateVariable[];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Expand a `routeTemplate` into a concrete site-relative URL.
|
|
351
|
+
*
|
|
352
|
+
* Validates the template first, then substitutes every placeholder. Each
|
|
353
|
+
* substituted value is `encodeURIComponent`-escaped because it occupies a
|
|
354
|
+
* single path segment — a value containing `/`, `?`, `#` or a space must not
|
|
355
|
+
* be able to change the shape of the resulting path.
|
|
356
|
+
*
|
|
357
|
+
* Fail-fast: a placeholder whose value is missing, `undefined`, or the empty
|
|
358
|
+
* string throws {@link SearchRouteTemplateError}. There is no partial
|
|
359
|
+
* expansion and no silent drop.
|
|
360
|
+
*/
|
|
361
|
+
export function expandSearchRouteTemplate(
|
|
362
|
+
template: string,
|
|
363
|
+
values: Readonly<Partial<Record<SearchRouteTemplateVariable, string>>>,
|
|
364
|
+
): string {
|
|
365
|
+
assertValidSearchRouteTemplate(template);
|
|
366
|
+
|
|
367
|
+
return template.replace(
|
|
368
|
+
SEARCH_ROUTE_TEMPLATE_PLACEHOLDER_PATTERN,
|
|
369
|
+
(_match, rawName: string) => {
|
|
370
|
+
const name = rawName as SearchRouteTemplateVariable;
|
|
371
|
+
const value = values[name];
|
|
372
|
+
if (value === undefined || value.length === 0) {
|
|
373
|
+
throw new SearchRouteTemplateError(
|
|
374
|
+
template,
|
|
375
|
+
`no value supplied for placeholder "{${name}}" — the template cannot be expanded in this context`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return encodeURIComponent(value);
|
|
379
|
+
},
|
|
380
|
+
);
|
|
381
|
+
}
|
|
@@ -6,6 +6,12 @@ import {
|
|
|
6
6
|
ResourceVisibilityPattern,
|
|
7
7
|
ResourceVisibilityPatternSchema,
|
|
8
8
|
} from '../../resource';
|
|
9
|
+
import {
|
|
10
|
+
SEARCH_ROUTE_PROJECTED_VARIABLES,
|
|
11
|
+
SearchRouteTemplateError,
|
|
12
|
+
SearchRouteTemplateVariable,
|
|
13
|
+
assertSearchRouteTemplateSatisfiable,
|
|
14
|
+
} from './route-template';
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* Maximum length of the short identifier / label strings on a search-type
|
|
@@ -41,22 +47,89 @@ export interface SearchTypeRenderHints {
|
|
|
41
47
|
readonly icon?: string;
|
|
42
48
|
/**
|
|
43
49
|
* Optional deep-link route template with `{placeholder}` segments the
|
|
44
|
-
*
|
|
45
|
-
* ≤ 500
|
|
50
|
+
* consumer fills from a result's fields (e.g.
|
|
51
|
+
* `/spaces/projects/{projectId}/documents/{containerSlug}/{slug}`). ≤ 500
|
|
52
|
+
* chars. Absent ⇒ the result is not directly deep-linkable.
|
|
53
|
+
*
|
|
54
|
+
* The placeholder vocabulary is the CLOSED
|
|
55
|
+
* {@link SearchRouteTemplateVariable} set and the syntax + SATISFIABILITY are
|
|
56
|
+
* validated by {@link assertSearchRouteTemplateSatisfiable} — a malformed
|
|
57
|
+
* template, an unknown placeholder, or a projected placeholder this
|
|
58
|
+
* result-type does not declare in {@link routeParams} is rejected at
|
|
59
|
+
* contribution-parse time AND at the owning service's wire, never discovered
|
|
60
|
+
* as a broken link. Expand with `expandSearchRouteTemplate`.
|
|
46
61
|
*/
|
|
47
62
|
readonly routeTemplate?: string;
|
|
63
|
+
/**
|
|
64
|
+
* The PROJECTED route variables this result-type supplies — the ones the
|
|
65
|
+
* platform cannot derive from a hit's scope/identity, so the producer must
|
|
66
|
+
* stamp them on `IndexableDocument.routeParams` (an object ref additionally
|
|
67
|
+
* carries `slug` intrinsically). Currently
|
|
68
|
+
* {@link SEARCH_ROUTE_PROJECTED_VARIABLES}: `slug`, `containerSlug`.
|
|
69
|
+
*
|
|
70
|
+
* This is the contribution's SUPPLY CONTRACT and it is enforced both ways:
|
|
71
|
+
* a `routeTemplate` referencing a projected variable that is not listed here
|
|
72
|
+
* is rejected, and a variable listed here that the template never references
|
|
73
|
+
* is rejected as a dead declaration. Absent ⇒ the template may only use
|
|
74
|
+
* platform-derived (`scope`/`identity`) variables.
|
|
75
|
+
*
|
|
76
|
+
* Only meaningful together with a `routeTemplate`; declaring it alone is a
|
|
77
|
+
* contribution bug and is rejected.
|
|
78
|
+
*/
|
|
79
|
+
readonly routeParams?: readonly SearchRouteTemplateVariable[];
|
|
48
80
|
}
|
|
49
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Hard cap on declared route params: the projected vocabulary itself is the
|
|
84
|
+
* ceiling — a contribution can never legitimately declare more entries than
|
|
85
|
+
* there are projected variables.
|
|
86
|
+
*/
|
|
87
|
+
const SEARCH_TYPE_ROUTE_PARAMS_MAX = SEARCH_ROUTE_PROJECTED_VARIABLES.size;
|
|
88
|
+
|
|
50
89
|
export const SearchTypeRenderHintsSchema: z.ZodType<SearchTypeRenderHints> =
|
|
51
|
-
z
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
90
|
+
z
|
|
91
|
+
.object({
|
|
92
|
+
label: z.string().min(1).max(SEARCH_TYPE_STRING_MAX),
|
|
93
|
+
icon: z.string().min(1).max(SEARCH_TYPE_STRING_MAX).optional(),
|
|
94
|
+
routeTemplate: z
|
|
95
|
+
.string()
|
|
96
|
+
.min(1)
|
|
97
|
+
.max(SEARCH_TYPE_ROUTE_TEMPLATE_MAX)
|
|
98
|
+
.optional(),
|
|
99
|
+
routeParams: z
|
|
100
|
+
.array(z.nativeEnum(SearchRouteTemplateVariable))
|
|
101
|
+
.max(SEARCH_TYPE_ROUTE_PARAMS_MAX)
|
|
102
|
+
.optional(),
|
|
103
|
+
})
|
|
104
|
+
.superRefine((value, ctx) => {
|
|
105
|
+
if (value.routeTemplate === undefined) {
|
|
106
|
+
if (value.routeParams !== undefined) {
|
|
107
|
+
ctx.addIssue({
|
|
108
|
+
code: z.ZodIssueCode.custom,
|
|
109
|
+
path: ['routeParams'],
|
|
110
|
+
message:
|
|
111
|
+
'routeParams declares route variables but no routeTemplate uses them — ' +
|
|
112
|
+
'a supply declaration without a template is dead weight.',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
assertSearchRouteTemplateSatisfiable(
|
|
119
|
+
value.routeTemplate,
|
|
120
|
+
value.routeParams ?? [],
|
|
121
|
+
);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (!(error instanceof SearchRouteTemplateError)) {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
ctx.addIssue({
|
|
127
|
+
code: z.ZodIssueCode.custom,
|
|
128
|
+
path: ['routeTemplate'],
|
|
129
|
+
message: error.reason,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}) as z.ZodType<SearchTypeRenderHints>;
|
|
60
133
|
|
|
61
134
|
/**
|
|
62
135
|
* `SearchTypeAuthzMapping` — how instances of this result-type map onto the
|
|
@@ -97,14 +170,26 @@ export const SearchTypeAuthzMappingSchema: z.ZodType<SearchTypeAuthzMapping> =
|
|
|
97
170
|
*/
|
|
98
171
|
export interface SearchTypeContributionManifest {
|
|
99
172
|
/**
|
|
100
|
-
* The `XemaObjectKind` this result-type covers
|
|
101
|
-
*
|
|
173
|
+
* The `XemaObjectKind` this result-type covers, when one genuinely applies.
|
|
174
|
+
*
|
|
175
|
+
* OPTIONAL BY DESIGN. `XemaObjectKind` is a CLOSED kernel enum; a biome
|
|
176
|
+
* concept that has no kernel kind (an email, a backlog item, a design idea)
|
|
177
|
+
* must not be forced to mislabel itself as the nearest neighbour. Four of the
|
|
178
|
+
* five first-party contributions did exactly that — all declaring `artifact`
|
|
179
|
+
* as a closest fit — which is worse than declaring nothing: it makes the
|
|
180
|
+
* object registry claim kinds it cannot enumerate, and it reads as a real
|
|
181
|
+
* classification to anything that consumes it.
|
|
182
|
+
*
|
|
183
|
+
* Absent ⇒ the result-type is discriminated by the OPEN `docType` alone,
|
|
184
|
+
* which is what `IndexableDocument.kind` being optional already anticipates.
|
|
185
|
+
* The natural key remains `(objectKind, docType)`; a contribution must carry
|
|
186
|
+
* at least one of the two, enforced by the schema below.
|
|
102
187
|
*/
|
|
103
|
-
readonly objectKind
|
|
188
|
+
readonly objectKind?: XemaObjectKind;
|
|
104
189
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* The natural key of a result-type is `(objectKind, docType)`.
|
|
190
|
+
* Biome sub-facet under `objectKind` (e.g. `kb_page` under `knowledge-page`),
|
|
191
|
+
* or — when no kernel kind applies — the sole discriminator for this
|
|
192
|
+
* result-type. The natural key of a result-type is `(objectKind, docType)`.
|
|
108
193
|
*/
|
|
109
194
|
readonly docType?: string;
|
|
110
195
|
/** Generic render/route hints so the frontend presents hits without a per-kind switch. */
|
|
@@ -139,27 +224,49 @@ export interface SearchTypeContributionManifest {
|
|
|
139
224
|
* `CapabilityContributionManifestSchema`).
|
|
140
225
|
*/
|
|
141
226
|
export const SearchTypeContributionManifestSchema: z.ZodType<SearchTypeContributionManifest> =
|
|
142
|
-
z
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
227
|
+
z
|
|
228
|
+
.object({
|
|
229
|
+
objectKind: XemaObjectKindSchema.optional(),
|
|
230
|
+
docType: z.string().min(1).max(SEARCH_TYPE_STRING_MAX).optional(),
|
|
231
|
+
renderHints: SearchTypeRenderHintsSchema,
|
|
232
|
+
searchableFields: z
|
|
233
|
+
.array(z.string().min(1).max(SEARCH_TYPE_STRING_MAX))
|
|
234
|
+
.min(1)
|
|
235
|
+
.max(SEARCH_TYPE_FIELDS_MAX),
|
|
236
|
+
embeddingEligibleDefault: z.boolean(),
|
|
237
|
+
authz: SearchTypeAuthzMappingSchema,
|
|
238
|
+
searchReplayCapabilityRef: CapabilityRefSchema.optional(),
|
|
239
|
+
})
|
|
240
|
+
// Both may not be absent: the natural key is `(objectKind, docType)`, so a
|
|
241
|
+
// contribution declaring neither has no identity at all. Making `objectKind`
|
|
242
|
+
// optional must not open a hole where a result-type is unaddressable —
|
|
243
|
+
// fail fast at the boundary, exactly as before.
|
|
244
|
+
.refine(
|
|
245
|
+
(m) => m.objectKind !== undefined || m.docType !== undefined,
|
|
246
|
+
'a search-type contribution must declare objectKind, docType, or both — ' +
|
|
247
|
+
'the natural key is (objectKind, docType) and cannot be empty',
|
|
248
|
+
) as unknown as z.ZodType<SearchTypeContributionManifest>;
|
|
154
249
|
|
|
155
250
|
/**
|
|
156
|
-
* The stable natural key of a result-type: `objectKind` when no `docType`,
|
|
251
|
+
* The stable natural key of a result-type: `objectKind` when no `docType`,
|
|
252
|
+
* `docType` when no `objectKind` (a biome concept with no kernel kind), else
|
|
157
253
|
* `objectKind/docType`. Centralized so the parser slug, the DB unique key, and
|
|
158
254
|
* the reconciler's `seenKeyOf` all agree.
|
|
255
|
+
*
|
|
256
|
+
* The schema guarantees at least one is present; this throws rather than
|
|
257
|
+
* returning `"undefined"` if that guarantee is ever bypassed, because a
|
|
258
|
+
* silently-stringified key would collide every unkeyed result-type into one row.
|
|
159
259
|
*/
|
|
160
260
|
export function searchTypeNaturalKey(
|
|
161
|
-
objectKind
|
|
261
|
+
objectKind?: XemaObjectKind,
|
|
162
262
|
docType?: string,
|
|
163
263
|
): string {
|
|
264
|
+
if (objectKind === undefined && docType === undefined) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
'searchTypeNaturalKey: neither objectKind nor docType was supplied — ' +
|
|
267
|
+
'a result-type must be addressable by at least one.',
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (objectKind === undefined) return String(docType);
|
|
164
271
|
return docType ? `${objectKind}/${docType}` : String(objectKind);
|
|
165
272
|
}
|
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
ResourceVisibilityPatternSchema,
|
|
11
11
|
type ResourceVisibilityPattern,
|
|
12
12
|
} from '../../resource';
|
|
13
|
+
import {
|
|
14
|
+
SEARCH_ROUTE_PROJECTED_VARIABLES,
|
|
15
|
+
SearchRouteTemplateVariable,
|
|
16
|
+
} from '../../search';
|
|
13
17
|
import {
|
|
14
18
|
DataClassificationSchema,
|
|
15
19
|
SpaceRefSchema,
|
|
@@ -89,6 +93,24 @@ export interface IndexableDocument {
|
|
|
89
93
|
readonly updatedAt?: string;
|
|
90
94
|
/** Free-form per-source metadata carried verbatim onto the index document. */
|
|
91
95
|
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
96
|
+
/**
|
|
97
|
+
* Deep-link route parameters this source PROJECTS onto the document so a
|
|
98
|
+
* search hit can be expanded into the biome's real frontend URL.
|
|
99
|
+
*
|
|
100
|
+
* Keys are the CLOSED {@link SEARCH_ROUTE_PROJECTED_VARIABLES} subset of
|
|
101
|
+
* {@link SearchRouteTemplateVariable} — the placeholders the platform cannot
|
|
102
|
+
* derive from a hit's scope/identity (`slug`, `containerSlug`). This is
|
|
103
|
+
* deliberately NOT `metadata`: it is a typed, closed-key contract validated
|
|
104
|
+
* at the wire, because a mistyped key here is a dead deep link, and search's
|
|
105
|
+
* whole route story depends on it being exactly what the owning biome's
|
|
106
|
+
* `SearchType` contribution declared in `renderHints.routeParams`.
|
|
107
|
+
*
|
|
108
|
+
* Omitted ⇒ the source projects no route params; a template referencing one
|
|
109
|
+
* simply does not expand for this document (no URL, never a broken URL).
|
|
110
|
+
*/
|
|
111
|
+
readonly routeParams?: Readonly<
|
|
112
|
+
Partial<Record<SearchRouteTemplateVariable, string>>
|
|
113
|
+
>;
|
|
92
114
|
/**
|
|
93
115
|
* Whether this document should receive a vector embedding (semantic search),
|
|
94
116
|
* declared by the SOURCE. Search is source-agnostic: it never inspects a
|
|
@@ -104,6 +126,27 @@ export interface IndexableDocument {
|
|
|
104
126
|
readonly embeddingEligible?: boolean;
|
|
105
127
|
}
|
|
106
128
|
|
|
129
|
+
/** Max length of a single projected route-param value (one URL path segment). */
|
|
130
|
+
const SEARCH_ROUTE_PARAM_VALUE_MAX = 200;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Wire schema for `IndexableDocument.routeParams`. `.strict()` on purpose: the
|
|
134
|
+
* key set is the CLOSED projected vocabulary, so a platform-derived key
|
|
135
|
+
* (`orgId`, `sourceId`, …) or a typo (`pageSlug`) is a hard validation failure
|
|
136
|
+
* at the wire — exactly the class of bug that used to surface only as a dead
|
|
137
|
+
* link in production.
|
|
138
|
+
*/
|
|
139
|
+
export const SearchRouteParamsSchema = z
|
|
140
|
+
.object(
|
|
141
|
+
Object.fromEntries(
|
|
142
|
+
[...SEARCH_ROUTE_PROJECTED_VARIABLES].map((variable) => [
|
|
143
|
+
variable,
|
|
144
|
+
z.string().min(1).max(SEARCH_ROUTE_PARAM_VALUE_MAX).optional(),
|
|
145
|
+
]),
|
|
146
|
+
) as Record<SearchRouteTemplateVariable, z.ZodOptional<z.ZodString>>,
|
|
147
|
+
)
|
|
148
|
+
.strict();
|
|
149
|
+
|
|
107
150
|
/**
|
|
108
151
|
* Wire schema for {@link IndexableDocument}. search-api validates every consumed
|
|
109
152
|
* upsert against this and dead-letters on failure — no silent thin-index.
|
|
@@ -130,6 +173,7 @@ export const IndexableDocumentSchema = z
|
|
|
130
173
|
createdAt: z.string().optional(),
|
|
131
174
|
updatedAt: z.string().optional(),
|
|
132
175
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
176
|
+
routeParams: SearchRouteParamsSchema.optional(),
|
|
133
177
|
embeddingEligible: z.boolean().optional(),
|
|
134
178
|
})
|
|
135
179
|
.strict();
|