@xemahq/kernel-contracts 0.50.0 → 0.56.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 +2 -1
- package/dist/capability/index.d.ts.map +1 -1
- package/dist/capability/index.js +2 -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 +2 -0
- package/dist/capability/lib/capability-contribution.d.ts.map +1 -1
- package/dist/capability/lib/capability-contribution.js +2 -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/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 -4
- package/dist/contribution/lib/contribution-kind.d.ts.map +1 -1
- package/dist/contribution/lib/contribution-kind.js +1 -4
- 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 +2 -0
- package/dist/search/lib/search-contribution.d.ts.map +1 -1
- package/dist/search/lib/search-contribution.js +34 -1
- 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 +2 -1
- package/src/capability/lib/authority-effect.ts +88 -0
- package/src/capability/lib/capability-contribution.ts +21 -0
- package/src/capability/lib/capability-domain.ts +51 -0
- package/src/capability/lib/capability-grant.ts +1 -1
- 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 +30 -29
- 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 +84 -11
- 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/dist/capability/lib/first-party-capability-domains.d.ts +0 -30
- package/dist/capability/lib/first-party-capability-domains.d.ts.map +0 -1
- package/dist/capability/lib/first-party-capability-domains.js +0 -36
- package/dist/capability/lib/first-party-capability-domains.js.map +0 -1
- 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
|
|
@@ -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();
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { type XemaObjectRef } from '../../object';
|
|
1
2
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
parseSpaceRefScopeSegments,
|
|
4
|
+
SpaceKind,
|
|
5
|
+
SpaceRefParseError,
|
|
6
|
+
spaceRefToPath,
|
|
7
|
+
type SpaceRef,
|
|
8
|
+
} from '../../space';
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* The natural-key / address grammar for a searchable document:
|
|
@@ -15,9 +18,10 @@ import { SpaceKind, spaceRefToPath, type SpaceRef } from '../../space';
|
|
|
15
18
|
* the type discriminator is `docType` (biome-owned, open), so a searchable
|
|
16
19
|
* document (an email, a design-idea, a backlog item) can be addressed WITHOUT a
|
|
17
20
|
* kernel object kind. The scope grammar itself is shared: this helper reuses
|
|
18
|
-
* {@link
|
|
19
|
-
* `<scope-path>` portion stays value-identical to `XemaObjectRef
|
|
20
|
-
* model ref validation is untouched for object-model
|
|
21
|
+
* {@link parseSpaceRefScopeSegments} + {@link spaceRefToPath} so the
|
|
22
|
+
* `<scope-path>` portion stays value-identical to `XemaObjectRef` and to a
|
|
23
|
+
* Space ref, and object-model ref validation is untouched for object-model
|
|
24
|
+
* callers.
|
|
21
25
|
*
|
|
22
26
|
* `docType` occupies the segment where an object ref carries `<kind>`; it is a
|
|
23
27
|
* lowercase, slash-free facet slug (e.g. `kb_page`, `email_thread`). `sourceId`
|
|
@@ -96,7 +100,7 @@ export function formatSearchRef(
|
|
|
96
100
|
/**
|
|
97
101
|
* Parse a search ref back into `{ scope, docType, sourceId }`. Fail-fast on
|
|
98
102
|
* every malformed input. The scope grammar is validated by the shared
|
|
99
|
-
* {@link
|
|
103
|
+
* {@link parseSpaceRefScopeSegments}; the trailing two segments are the
|
|
100
104
|
* `docType` and the (slash-free) `sourceId`.
|
|
101
105
|
*/
|
|
102
106
|
export function parseSearchRef(ref: string): SearchRefParts {
|
|
@@ -115,7 +119,16 @@ export function parseSearchRef(ref: string): SearchRefParts {
|
|
|
115
119
|
);
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
|
|
122
|
+
let scope: SpaceRef;
|
|
123
|
+
let consumed: number;
|
|
124
|
+
try {
|
|
125
|
+
({ scope, consumed } = parseSpaceRefScopeSegments(segments, ref));
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (err instanceof SpaceRefParseError) {
|
|
128
|
+
throw new SearchRefParseError(ref, err.message);
|
|
129
|
+
}
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
119
132
|
const remaining = segments.slice(consumed);
|
|
120
133
|
if (remaining.length !== 2) {
|
|
121
134
|
throw new SearchRefParseError(
|
|
@@ -61,6 +61,11 @@ export interface ServiceEndpoint {
|
|
|
61
61
|
* resolved and must not register.
|
|
62
62
|
* - `exposesCapabilities` / `requiresServices` carry only canonical
|
|
63
63
|
* refs / service names — no env-var URLs.
|
|
64
|
+
* - `openapiUrl`, when present, is ABSOLUTE and points at the service's
|
|
65
|
+
* PUBLIC-surface OpenAPI document. It is declared, never derived by
|
|
66
|
+
* convention: a consumer must not guess `<endpoint>/api/openapi.json`,
|
|
67
|
+
* because that path serves the merged public+internal+operator document and
|
|
68
|
+
* because a service may mount its docs elsewhere or not at all.
|
|
64
69
|
*/
|
|
65
70
|
export interface ServiceDescriptor {
|
|
66
71
|
name: string;
|
|
@@ -70,6 +75,17 @@ export interface ServiceDescriptor {
|
|
|
70
75
|
exposesCapabilities: string[];
|
|
71
76
|
requiresServices: string[];
|
|
72
77
|
healthEndpoint?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Absolute URL of the service's PUBLIC-surface OpenAPI document (the same
|
|
80
|
+
* subset shipped as the `openapi.public.json` build artifact).
|
|
81
|
+
*
|
|
82
|
+
* Optional because a service MAY serve no spec — Swagger can be disabled per
|
|
83
|
+
* deployment, and a non-HTTP service has none at all. Absent means exactly
|
|
84
|
+
* "this instance advertises no machine-readable API description"; consumers
|
|
85
|
+
* MUST treat absence as a hard skip and never fall back to a conventional
|
|
86
|
+
* path.
|
|
87
|
+
*/
|
|
88
|
+
openapiUrl?: string;
|
|
73
89
|
region?: string;
|
|
74
90
|
labels?: Record<string, string>;
|
|
75
91
|
}
|
|
@@ -34,7 +34,7 @@ const SKILL_KIND_SEGMENT = 'skill';
|
|
|
34
34
|
* the org's own RO mount; another org's worker never mounts it.
|
|
35
35
|
*
|
|
36
36
|
* Returns `null` when the scope is not mirrorable from the ref alone:
|
|
37
|
-
* • User — the ref is org-less (`xema://
|
|
37
|
+
* • User — the ref is org-less (`xema://users/<userId>`), so it can't be
|
|
38
38
|
* placed under any org's templates subtree.
|
|
39
39
|
* • App / Session — runtime spaces, never own a skill bundle.
|
|
40
40
|
* • a mirrorable tier whose required id segment is absent (malformed ref).
|