@xemahq/kernel-contracts 0.49.3 → 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.
Files changed (196) hide show
  1. package/dist/app-definition/lib/automation.d.ts.map +1 -1
  2. package/dist/app-definition/lib/automation.js.map +1 -1
  3. package/dist/biome/lib/biome-manifest.d.ts +9 -0
  4. package/dist/biome/lib/biome-manifest.d.ts.map +1 -1
  5. package/dist/biome/lib/biome-manifest.js +2 -0
  6. package/dist/biome/lib/biome-manifest.js.map +1 -1
  7. package/dist/capability/index.d.ts +2 -1
  8. package/dist/capability/index.d.ts.map +1 -1
  9. package/dist/capability/index.js +2 -1
  10. package/dist/capability/index.js.map +1 -1
  11. package/dist/capability/lib/authority-effect.d.ts +8 -0
  12. package/dist/capability/lib/authority-effect.d.ts.map +1 -0
  13. package/dist/capability/lib/authority-effect.js +17 -0
  14. package/dist/capability/lib/authority-effect.js.map +1 -0
  15. package/dist/capability/lib/capability-contribution.d.ts +4 -0
  16. package/dist/capability/lib/capability-contribution.d.ts.map +1 -1
  17. package/dist/capability/lib/capability-contribution.js +4 -0
  18. package/dist/capability/lib/capability-contribution.js.map +1 -1
  19. package/dist/capability/lib/capability-domain.d.ts +8 -0
  20. package/dist/capability/lib/capability-domain.d.ts.map +1 -0
  21. package/dist/capability/lib/capability-domain.js +24 -0
  22. package/dist/capability/lib/capability-domain.js.map +1 -0
  23. package/dist/capability/lib/meta-tool.d.ts +51 -14
  24. package/dist/capability/lib/meta-tool.d.ts.map +1 -1
  25. package/dist/capability/lib/meta-tool.js +66 -12
  26. package/dist/capability/lib/meta-tool.js.map +1 -1
  27. package/dist/capability-projection/index.d.ts +4 -0
  28. package/dist/capability-projection/index.d.ts.map +1 -0
  29. package/dist/capability-projection/index.js +20 -0
  30. package/dist/capability-projection/index.js.map +1 -0
  31. package/dist/capability-projection/lib/capability-invocation-binding.d.ts +72 -0
  32. package/dist/capability-projection/lib/capability-invocation-binding.d.ts.map +1 -0
  33. package/dist/capability-projection/lib/capability-invocation-binding.js +101 -0
  34. package/dist/capability-projection/lib/capability-invocation-binding.js.map +1 -0
  35. package/dist/capability-projection/lib/capability-sync-manifest.d.ts +8 -0
  36. package/dist/capability-projection/lib/capability-sync-manifest.d.ts.map +1 -0
  37. package/dist/capability-projection/lib/capability-sync-manifest.js +8 -0
  38. package/dist/capability-projection/lib/capability-sync-manifest.js.map +1 -0
  39. package/dist/capability-projection/lib/projected-capability.d.ts +15 -0
  40. package/dist/capability-projection/lib/projected-capability.d.ts.map +1 -0
  41. package/dist/capability-projection/lib/projected-capability.js +17 -0
  42. package/dist/capability-projection/lib/projected-capability.js.map +1 -0
  43. package/dist/connector/lib/connector-adapter-contribution.d.ts +2 -0
  44. package/dist/connector/lib/connector-adapter-contribution.d.ts.map +1 -1
  45. package/dist/connector/lib/onboarding-manifest.d.ts +2 -0
  46. package/dist/connector/lib/onboarding-manifest.d.ts.map +1 -1
  47. package/dist/connector/lib/onboarding-manifest.js +1 -0
  48. package/dist/connector/lib/onboarding-manifest.js.map +1 -1
  49. package/dist/connector/lib/provider-descriptor.d.ts +2 -0
  50. package/dist/connector/lib/provider-descriptor.d.ts.map +1 -1
  51. package/dist/contribution/lib/contribution-kind.d.ts +1 -4
  52. package/dist/contribution/lib/contribution-kind.d.ts.map +1 -1
  53. package/dist/contribution/lib/contribution-kind.js +1 -4
  54. package/dist/contribution/lib/contribution-kind.js.map +1 -1
  55. package/dist/distribution/index.d.ts +1 -0
  56. package/dist/distribution/index.d.ts.map +1 -1
  57. package/dist/distribution/index.js +1 -0
  58. package/dist/distribution/index.js.map +1 -1
  59. package/dist/distribution/lib/distribution-lock.d.ts +6 -0
  60. package/dist/distribution/lib/distribution-lock.d.ts.map +1 -1
  61. package/dist/distribution/lib/distribution-lock.js +9 -1
  62. package/dist/distribution/lib/distribution-lock.js.map +1 -1
  63. package/dist/distribution/lib/first-party-capability-domains.d.ts +15 -0
  64. package/dist/distribution/lib/first-party-capability-domains.d.ts.map +1 -0
  65. package/dist/distribution/lib/first-party-capability-domains.js +52 -0
  66. package/dist/distribution/lib/first-party-capability-domains.js.map +1 -0
  67. package/dist/object/lib/xema-object-ref.d.ts +0 -5
  68. package/dist/object/lib/xema-object-ref.d.ts.map +1 -1
  69. package/dist/object/lib/xema-object-ref.js +12 -62
  70. package/dist/object/lib/xema-object-ref.js.map +1 -1
  71. package/dist/output-surface-inspect/index.d.ts +1 -0
  72. package/dist/output-surface-inspect/index.d.ts.map +1 -1
  73. package/dist/output-surface-inspect/index.js +1 -0
  74. package/dist/output-surface-inspect/index.js.map +1 -1
  75. package/dist/output-surface-inspect/lib/buffers.d.ts +52 -0
  76. package/dist/output-surface-inspect/lib/buffers.d.ts.map +1 -0
  77. package/dist/output-surface-inspect/lib/buffers.js +132 -0
  78. package/dist/output-surface-inspect/lib/buffers.js.map +1 -0
  79. package/dist/output-surface-inspect/lib/output-surface-inspect.d.ts +6 -1
  80. package/dist/output-surface-inspect/lib/output-surface-inspect.d.ts.map +1 -1
  81. package/dist/output-surface-inspect/lib/output-surface-inspect.js +12 -1
  82. package/dist/output-surface-inspect/lib/output-surface-inspect.js.map +1 -1
  83. package/dist/output-surface-inspect/lib/step-schema.d.ts +2 -1
  84. package/dist/output-surface-inspect/lib/step-schema.d.ts.map +1 -1
  85. package/dist/output-surface-inspect/lib/step-schema.js +9 -2
  86. package/dist/output-surface-inspect/lib/step-schema.js.map +1 -1
  87. package/dist/output-surface-inspect/runtime/index.d.ts +1 -0
  88. package/dist/output-surface-inspect/runtime/index.d.ts.map +1 -1
  89. package/dist/output-surface-inspect/runtime/index.js +1 -0
  90. package/dist/output-surface-inspect/runtime/index.js.map +1 -1
  91. package/dist/search/index.d.ts +1 -0
  92. package/dist/search/index.d.ts.map +1 -1
  93. package/dist/search/index.js +1 -0
  94. package/dist/search/index.js.map +1 -1
  95. package/dist/search/lib/route-template.d.ts +31 -0
  96. package/dist/search/lib/route-template.d.ts.map +1 -0
  97. package/dist/search/lib/route-template.js +142 -0
  98. package/dist/search/lib/route-template.js.map +1 -0
  99. package/dist/search/lib/search-contribution.d.ts +2 -0
  100. package/dist/search/lib/search-contribution.d.ts.map +1 -1
  101. package/dist/search/lib/search-contribution.js +34 -1
  102. package/dist/search/lib/search-contribution.js.map +1 -1
  103. package/dist/search-source/lib/indexable-document.d.ts +22 -0
  104. package/dist/search-source/lib/indexable-document.d.ts.map +1 -1
  105. package/dist/search-source/lib/indexable-document.js +10 -1
  106. package/dist/search-source/lib/indexable-document.js.map +1 -1
  107. package/dist/search-source/lib/search-index-event.d.ts +10 -0
  108. package/dist/search-source/lib/search-index-event.d.ts.map +1 -1
  109. package/dist/search-source/lib/search-ref.d.ts.map +1 -1
  110. package/dist/search-source/lib/search-ref.js +11 -2
  111. package/dist/search-source/lib/search-ref.js.map +1 -1
  112. package/dist/search-source/lib/search-replay.d.ts +10 -0
  113. package/dist/search-source/lib/search-replay.d.ts.map +1 -1
  114. package/dist/service-registry/lib/service-descriptor.d.ts +1 -0
  115. package/dist/service-registry/lib/service-descriptor.d.ts.map +1 -1
  116. package/dist/space/lib/space-ref-parser.d.ts +6 -4
  117. package/dist/space/lib/space-ref-parser.d.ts.map +1 -1
  118. package/dist/space/lib/space-ref-parser.js +90 -126
  119. package/dist/space/lib/space-ref-parser.js.map +1 -1
  120. package/dist/space/lib/space-traversal.d.ts.map +1 -1
  121. package/dist/space/lib/space-traversal.js +9 -13
  122. package/dist/space/lib/space-traversal.js.map +1 -1
  123. package/dist/space/lib/space.d.ts +9 -0
  124. package/dist/space/lib/space.d.ts.map +1 -1
  125. package/dist/space/lib/space.js +59 -29
  126. package/dist/space/lib/space.js.map +1 -1
  127. package/dist/subject/lib/subject.d.ts +1 -0
  128. package/dist/subject/lib/subject.d.ts.map +1 -1
  129. package/dist/subject/lib/subject.js +1 -0
  130. package/dist/subject/lib/subject.js.map +1 -1
  131. package/dist/surface/index.d.ts +4 -0
  132. package/dist/surface/index.d.ts.map +1 -0
  133. package/dist/surface/index.js +20 -0
  134. package/dist/surface/index.js.map +1 -0
  135. package/dist/surface/lib/surface-contribution.d.ts +24 -0
  136. package/dist/surface/lib/surface-contribution.d.ts.map +1 -0
  137. package/dist/surface/lib/surface-contribution.js +23 -0
  138. package/dist/surface/lib/surface-contribution.js.map +1 -0
  139. package/dist/surface/lib/surface-kind.d.ts +23 -0
  140. package/dist/surface/lib/surface-kind.d.ts.map +1 -0
  141. package/dist/surface/lib/surface-kind.js +27 -0
  142. package/dist/surface/lib/surface-kind.js.map +1 -0
  143. package/dist/surface/lib/vista-surface.d.ts +11 -0
  144. package/dist/surface/lib/vista-surface.d.ts.map +1 -0
  145. package/dist/surface/lib/vista-surface.js +13 -0
  146. package/dist/surface/lib/vista-surface.js.map +1 -0
  147. package/dist/widget/lib/chat-widget-envelope.d.ts.map +1 -1
  148. package/dist/widget/lib/chat-widget-envelope.js.map +1 -1
  149. package/dist/workflow/lib/enums.d.ts.map +1 -1
  150. package/dist/workflow/lib/enums.js.map +1 -1
  151. package/package.json +1 -1
  152. package/src/app-definition/lib/automation.ts +3 -2
  153. package/src/biome/lib/biome-manifest.ts +41 -1
  154. package/src/capability/index.ts +2 -1
  155. package/src/capability/lib/authority-effect.ts +88 -0
  156. package/src/capability/lib/capability-contribution.ts +46 -0
  157. package/src/capability/lib/capability-domain.ts +51 -0
  158. package/src/capability/lib/capability-grant.ts +1 -1
  159. package/src/capability/lib/meta-tool.ts +172 -39
  160. package/src/capability-projection/index.ts +3 -0
  161. package/src/capability-projection/lib/capability-invocation-binding.ts +223 -0
  162. package/src/capability-projection/lib/capability-sync-manifest.ts +52 -0
  163. package/src/capability-projection/lib/projected-capability.ts +63 -0
  164. package/src/connector/lib/onboarding-manifest.ts +16 -0
  165. package/src/contribution/lib/contribution-kind.ts +30 -29
  166. package/src/distribution/index.ts +1 -0
  167. package/src/distribution/lib/distribution-lock.ts +36 -0
  168. package/src/distribution/lib/first-party-capability-domains.ts +166 -0
  169. package/src/object/lib/xema-object-ref.ts +27 -82
  170. package/src/output-surface-inspect/index.ts +2 -0
  171. package/src/output-surface-inspect/lib/buffers.ts +252 -0
  172. package/src/output-surface-inspect/lib/output-surface-inspect.ts +65 -6
  173. package/src/output-surface-inspect/lib/step-schema.ts +36 -3
  174. package/src/output-surface-inspect/runtime/index.ts +1 -0
  175. package/src/search/index.ts +1 -0
  176. package/src/search/lib/route-template.ts +381 -0
  177. package/src/search/lib/search-contribution.ts +84 -11
  178. package/src/search-source/lib/indexable-document.ts +44 -0
  179. package/src/search-source/lib/search-ref.ts +22 -9
  180. package/src/service-registry/lib/service-descriptor.ts +16 -0
  181. package/src/space/lib/skill-mirror-path.ts +1 -1
  182. package/src/space/lib/space-ref-parser.ts +190 -165
  183. package/src/space/lib/space-traversal.ts +34 -17
  184. package/src/space/lib/space.ts +148 -35
  185. package/src/subject/lib/subject.ts +16 -0
  186. package/src/surface/index.ts +3 -0
  187. package/src/surface/lib/surface-contribution.ts +101 -0
  188. package/src/surface/lib/surface-kind.ts +65 -0
  189. package/src/surface/lib/vista-surface.ts +44 -0
  190. package/src/widget/lib/chat-widget-envelope.ts +6 -2
  191. package/src/workflow/lib/enums.ts +6 -7
  192. package/dist/capability/lib/first-party-capability-domains.d.ts +0 -30
  193. package/dist/capability/lib/first-party-capability-domains.d.ts.map +0 -1
  194. package/dist/capability/lib/first-party-capability-domains.js +0 -36
  195. package/dist/capability/lib/first-party-capability-domains.js.map +0 -1
  196. 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
- * frontend fills from a result's fields (e.g. `/kb/{orgId}/pages/{slug}`).
45
- * ≤ 500 chars. Absent ⇒ the result is not directly deep-linkable.
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.object({
52
- label: z.string().min(1).max(SEARCH_TYPE_STRING_MAX),
53
- icon: z.string().min(1).max(SEARCH_TYPE_STRING_MAX).optional(),
54
- routeTemplate: z
55
- .string()
56
- .min(1)
57
- .max(SEARCH_TYPE_ROUTE_TEMPLATE_MAX)
58
- .optional(),
59
- }) as z.ZodType<SearchTypeRenderHints>;
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
- parseObjectRefScopeSegments,
3
- type XemaObjectRef,
4
- } from '../../object';
5
- import { SpaceKind, spaceRefToPath, type SpaceRef } from '../../space';
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 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
+ * {@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 parseObjectRefScopeSegments}; the trailing two segments are the
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
- const { scope, consumed } = parseObjectRefScopeSegments(segments, ref);
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://user/<userId>`), so it can't be
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).