@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.
Files changed (194) hide show
  1. package/dist/biome/lib/biome-manifest.d.ts +9 -0
  2. package/dist/biome/lib/biome-manifest.d.ts.map +1 -1
  3. package/dist/biome/lib/biome-manifest.js +2 -0
  4. package/dist/biome/lib/biome-manifest.js.map +1 -1
  5. package/dist/capability/index.d.ts +3 -1
  6. package/dist/capability/index.d.ts.map +1 -1
  7. package/dist/capability/index.js +3 -1
  8. package/dist/capability/index.js.map +1 -1
  9. package/dist/capability/lib/authority-effect.d.ts +8 -0
  10. package/dist/capability/lib/authority-effect.d.ts.map +1 -0
  11. package/dist/capability/lib/authority-effect.js +17 -0
  12. package/dist/capability/lib/authority-effect.js.map +1 -0
  13. package/dist/capability/lib/capability-contribution.d.ts +4 -0
  14. package/dist/capability/lib/capability-contribution.d.ts.map +1 -1
  15. package/dist/capability/lib/capability-contribution.js +4 -0
  16. package/dist/capability/lib/capability-contribution.js.map +1 -1
  17. package/dist/capability/lib/capability-domain.d.ts +8 -0
  18. package/dist/capability/lib/capability-domain.d.ts.map +1 -0
  19. package/dist/capability/lib/capability-domain.js +24 -0
  20. package/dist/capability/lib/capability-domain.js.map +1 -0
  21. package/dist/capability/lib/capability-reach.d.ts +13 -0
  22. package/dist/capability/lib/capability-reach.d.ts.map +1 -0
  23. package/dist/capability/lib/capability-reach.js +34 -0
  24. package/dist/capability/lib/capability-reach.js.map +1 -0
  25. package/dist/capability/lib/meta-tool.d.ts +51 -14
  26. package/dist/capability/lib/meta-tool.d.ts.map +1 -1
  27. package/dist/capability/lib/meta-tool.js +66 -12
  28. package/dist/capability/lib/meta-tool.js.map +1 -1
  29. package/dist/capability-projection/index.d.ts +4 -0
  30. package/dist/capability-projection/index.d.ts.map +1 -0
  31. package/dist/capability-projection/index.js +20 -0
  32. package/dist/capability-projection/index.js.map +1 -0
  33. package/dist/capability-projection/lib/capability-invocation-binding.d.ts +72 -0
  34. package/dist/capability-projection/lib/capability-invocation-binding.d.ts.map +1 -0
  35. package/dist/capability-projection/lib/capability-invocation-binding.js +101 -0
  36. package/dist/capability-projection/lib/capability-invocation-binding.js.map +1 -0
  37. package/dist/capability-projection/lib/capability-sync-manifest.d.ts +8 -0
  38. package/dist/capability-projection/lib/capability-sync-manifest.d.ts.map +1 -0
  39. package/dist/capability-projection/lib/capability-sync-manifest.js +8 -0
  40. package/dist/capability-projection/lib/capability-sync-manifest.js.map +1 -0
  41. package/dist/capability-projection/lib/projected-capability.d.ts +15 -0
  42. package/dist/capability-projection/lib/projected-capability.d.ts.map +1 -0
  43. package/dist/capability-projection/lib/projected-capability.js +17 -0
  44. package/dist/capability-projection/lib/projected-capability.js.map +1 -0
  45. package/dist/connector/lib/connector-adapter-contribution.d.ts +2 -0
  46. package/dist/connector/lib/connector-adapter-contribution.d.ts.map +1 -1
  47. package/dist/connector/lib/onboarding-manifest.d.ts +2 -0
  48. package/dist/connector/lib/onboarding-manifest.d.ts.map +1 -1
  49. package/dist/connector/lib/onboarding-manifest.js +1 -0
  50. package/dist/connector/lib/onboarding-manifest.js.map +1 -1
  51. package/dist/connector/lib/provider-descriptor.d.ts +2 -0
  52. package/dist/connector/lib/provider-descriptor.d.ts.map +1 -1
  53. package/dist/contribution/lib/contribution-kind.d.ts +1 -6
  54. package/dist/contribution/lib/contribution-kind.d.ts.map +1 -1
  55. package/dist/contribution/lib/contribution-kind.js +1 -6
  56. package/dist/contribution/lib/contribution-kind.js.map +1 -1
  57. package/dist/distribution/index.d.ts +1 -0
  58. package/dist/distribution/index.d.ts.map +1 -1
  59. package/dist/distribution/index.js +1 -0
  60. package/dist/distribution/index.js.map +1 -1
  61. package/dist/distribution/lib/distribution-lock.d.ts +6 -0
  62. package/dist/distribution/lib/distribution-lock.d.ts.map +1 -1
  63. package/dist/distribution/lib/distribution-lock.js +9 -1
  64. package/dist/distribution/lib/distribution-lock.js.map +1 -1
  65. package/dist/distribution/lib/first-party-capability-domains.d.ts +15 -0
  66. package/dist/distribution/lib/first-party-capability-domains.d.ts.map +1 -0
  67. package/dist/distribution/lib/first-party-capability-domains.js +52 -0
  68. package/dist/distribution/lib/first-party-capability-domains.js.map +1 -0
  69. package/dist/object/lib/xema-object-ref.d.ts +0 -5
  70. package/dist/object/lib/xema-object-ref.d.ts.map +1 -1
  71. package/dist/object/lib/xema-object-ref.js +12 -62
  72. package/dist/object/lib/xema-object-ref.js.map +1 -1
  73. package/dist/output-surface-inspect/index.d.ts +1 -0
  74. package/dist/output-surface-inspect/index.d.ts.map +1 -1
  75. package/dist/output-surface-inspect/index.js +1 -0
  76. package/dist/output-surface-inspect/index.js.map +1 -1
  77. package/dist/output-surface-inspect/lib/buffers.d.ts +52 -0
  78. package/dist/output-surface-inspect/lib/buffers.d.ts.map +1 -0
  79. package/dist/output-surface-inspect/lib/buffers.js +132 -0
  80. package/dist/output-surface-inspect/lib/buffers.js.map +1 -0
  81. package/dist/output-surface-inspect/lib/output-surface-inspect.d.ts +6 -1
  82. package/dist/output-surface-inspect/lib/output-surface-inspect.d.ts.map +1 -1
  83. package/dist/output-surface-inspect/lib/output-surface-inspect.js +12 -1
  84. package/dist/output-surface-inspect/lib/output-surface-inspect.js.map +1 -1
  85. package/dist/output-surface-inspect/lib/step-schema.d.ts +2 -1
  86. package/dist/output-surface-inspect/lib/step-schema.d.ts.map +1 -1
  87. package/dist/output-surface-inspect/lib/step-schema.js +9 -2
  88. package/dist/output-surface-inspect/lib/step-schema.js.map +1 -1
  89. package/dist/output-surface-inspect/runtime/index.d.ts +1 -0
  90. package/dist/output-surface-inspect/runtime/index.d.ts.map +1 -1
  91. package/dist/output-surface-inspect/runtime/index.js +1 -0
  92. package/dist/output-surface-inspect/runtime/index.js.map +1 -1
  93. package/dist/search/index.d.ts +1 -0
  94. package/dist/search/index.d.ts.map +1 -1
  95. package/dist/search/index.js +1 -0
  96. package/dist/search/index.js.map +1 -1
  97. package/dist/search/lib/route-template.d.ts +31 -0
  98. package/dist/search/lib/route-template.d.ts.map +1 -0
  99. package/dist/search/lib/route-template.js +142 -0
  100. package/dist/search/lib/route-template.js.map +1 -0
  101. package/dist/search/lib/search-contribution.d.ts +4 -2
  102. package/dist/search/lib/search-contribution.d.ts.map +1 -1
  103. package/dist/search/lib/search-contribution.js +46 -4
  104. package/dist/search/lib/search-contribution.js.map +1 -1
  105. package/dist/search-source/lib/indexable-document.d.ts +22 -0
  106. package/dist/search-source/lib/indexable-document.d.ts.map +1 -1
  107. package/dist/search-source/lib/indexable-document.js +10 -1
  108. package/dist/search-source/lib/indexable-document.js.map +1 -1
  109. package/dist/search-source/lib/search-index-event.d.ts +10 -0
  110. package/dist/search-source/lib/search-index-event.d.ts.map +1 -1
  111. package/dist/search-source/lib/search-ref.d.ts.map +1 -1
  112. package/dist/search-source/lib/search-ref.js +11 -2
  113. package/dist/search-source/lib/search-ref.js.map +1 -1
  114. package/dist/search-source/lib/search-replay.d.ts +10 -0
  115. package/dist/search-source/lib/search-replay.d.ts.map +1 -1
  116. package/dist/service-registry/lib/service-descriptor.d.ts +1 -0
  117. package/dist/service-registry/lib/service-descriptor.d.ts.map +1 -1
  118. package/dist/space/lib/space-ref-parser.d.ts +6 -4
  119. package/dist/space/lib/space-ref-parser.d.ts.map +1 -1
  120. package/dist/space/lib/space-ref-parser.js +90 -126
  121. package/dist/space/lib/space-ref-parser.js.map +1 -1
  122. package/dist/space/lib/space-traversal.d.ts.map +1 -1
  123. package/dist/space/lib/space-traversal.js +9 -13
  124. package/dist/space/lib/space-traversal.js.map +1 -1
  125. package/dist/space/lib/space.d.ts +7 -0
  126. package/dist/space/lib/space.d.ts.map +1 -1
  127. package/dist/space/lib/space.js +48 -29
  128. package/dist/space/lib/space.js.map +1 -1
  129. package/dist/subject/lib/subject.d.ts +1 -0
  130. package/dist/subject/lib/subject.d.ts.map +1 -1
  131. package/dist/subject/lib/subject.js +1 -0
  132. package/dist/subject/lib/subject.js.map +1 -1
  133. package/dist/surface/index.d.ts +4 -0
  134. package/dist/surface/index.d.ts.map +1 -0
  135. package/dist/surface/index.js +20 -0
  136. package/dist/surface/index.js.map +1 -0
  137. package/dist/surface/lib/surface-contribution.d.ts +24 -0
  138. package/dist/surface/lib/surface-contribution.d.ts.map +1 -0
  139. package/dist/surface/lib/surface-contribution.js +23 -0
  140. package/dist/surface/lib/surface-contribution.js.map +1 -0
  141. package/dist/surface/lib/surface-kind.d.ts +23 -0
  142. package/dist/surface/lib/surface-kind.d.ts.map +1 -0
  143. package/dist/surface/lib/surface-kind.js +27 -0
  144. package/dist/surface/lib/surface-kind.js.map +1 -0
  145. package/dist/surface/lib/vista-surface.d.ts +11 -0
  146. package/dist/surface/lib/vista-surface.d.ts.map +1 -0
  147. package/dist/surface/lib/vista-surface.js +13 -0
  148. package/dist/surface/lib/vista-surface.js.map +1 -0
  149. package/dist/widget/lib/chat-widget-envelope.d.ts.map +1 -1
  150. package/dist/widget/lib/chat-widget-envelope.js.map +1 -1
  151. package/dist/workflow/lib/enums.d.ts.map +1 -1
  152. package/dist/workflow/lib/enums.js.map +1 -1
  153. package/package.json +1 -1
  154. package/src/biome/lib/biome-manifest.ts +41 -1
  155. package/src/capability/index.ts +3 -1
  156. package/src/capability/lib/authority-effect.ts +88 -0
  157. package/src/capability/lib/capability-contribution.ts +44 -0
  158. package/src/capability/lib/capability-domain.ts +51 -0
  159. package/src/capability/lib/capability-grant.ts +1 -1
  160. package/src/capability/lib/capability-reach.ts +119 -0
  161. package/src/capability/lib/meta-tool.ts +172 -39
  162. package/src/capability-projection/index.ts +3 -0
  163. package/src/capability-projection/lib/capability-invocation-binding.ts +223 -0
  164. package/src/capability-projection/lib/capability-sync-manifest.ts +52 -0
  165. package/src/capability-projection/lib/projected-capability.ts +63 -0
  166. package/src/connector/lib/onboarding-manifest.ts +16 -0
  167. package/src/contribution/lib/contribution-kind.ts +165 -61
  168. package/src/distribution/index.ts +1 -0
  169. package/src/distribution/lib/distribution-lock.ts +36 -0
  170. package/src/distribution/lib/first-party-capability-domains.ts +166 -0
  171. package/src/object/lib/xema-object-ref.ts +27 -82
  172. package/src/output-surface-inspect/index.ts +2 -0
  173. package/src/output-surface-inspect/lib/buffers.ts +252 -0
  174. package/src/output-surface-inspect/lib/output-surface-inspect.ts +65 -6
  175. package/src/output-surface-inspect/lib/step-schema.ts +36 -3
  176. package/src/output-surface-inspect/runtime/index.ts +1 -0
  177. package/src/search/index.ts +1 -0
  178. package/src/search/lib/route-template.ts +381 -0
  179. package/src/search/lib/search-contribution.ts +138 -31
  180. package/src/search-source/lib/indexable-document.ts +44 -0
  181. package/src/search-source/lib/search-ref.ts +22 -9
  182. package/src/service-registry/lib/service-descriptor.ts +16 -0
  183. package/src/space/lib/skill-mirror-path.ts +1 -1
  184. package/src/space/lib/space-ref-parser.ts +190 -165
  185. package/src/space/lib/space-traversal.ts +34 -17
  186. package/src/space/lib/space.ts +110 -35
  187. package/src/subject/lib/subject.ts +16 -0
  188. package/src/surface/index.ts +3 -0
  189. package/src/surface/lib/surface-contribution.ts +101 -0
  190. package/src/surface/lib/surface-kind.ts +65 -0
  191. package/src/surface/lib/vista-surface.ts +44 -0
  192. package/src/widget/lib/chat-widget-envelope.ts +6 -2
  193. package/src/workflow/lib/enums.ts +6 -7
  194. package/src/capability/lib/first-party-capability-domains.ts +0 -77
@@ -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).
@@ -1,198 +1,223 @@
1
- import { SpaceKind, type SpaceRef } from './space';
2
-
3
- // SpaceRef URI grammar:
1
+ import {
2
+ assertValidSpaceSegmentId,
3
+ SpaceKind,
4
+ SpaceRefParseError,
5
+ spaceRefToPath,
6
+ type SpaceRef,
7
+ } from './space';
8
+
9
+ // ═══════════════════════════════════════════════════════════════════════════
10
+ // THE Space URI grammar. ONE grammar, platform-wide.
4
11
  //
5
12
  // xema://system
6
- // xema://org/<org>
7
- // xema://org/<org>/project/<project>
8
- // xema://org/<org>/project/<project>/app/<app>
9
- // xema://org/<org>/project/<project>/app/<app>/session/<session>
10
- // xema://org/<org>/project/<project>/app/<app>/biome/<biome>
11
- // xema://user/<user>
13
+ // xema://users/<userId>
14
+ // xema://biomes/<biomeId>
15
+ // xema://sessions/<sessionId>
16
+ // xema://orgs/<orgId>
17
+ // xema://orgs/<orgId>/projects/<projectId>
18
+ // xema://orgs/<orgId>/projects/<projectId>/apps/<appId>
19
+ //
20
+ // Historical note (X9 convergence): this module used to implement a SECOND,
21
+ // incompatible grammar — singular `org`/`project`/`app` key/id pairs that
22
+ // additionally NESTED session and biome under app
23
+ // (`xema://org/a/project/p/app/x/session/y`). It disagreed with
24
+ // `spaceRefToPath` (the plural form that `XemaObjectRef`, search refs and the
25
+ // object-registry projection all use) for the Biome, Session and User tiers,
26
+ // so `formatSpaceRef` and `spaceRefToPath` were not inverses and a ref could
27
+ // not survive a round trip through both. The singular grammar LOST and was
28
+ // deleted outright — no shim, no dual-read. Reasons, in order of weight:
12
29
  //
13
- // The grammar is closed and ordered. Every segment identifier matches
14
- // `/^[a-z0-9][a-z0-9_-]*$/`. `system` is the rootless kind. `user` is a
15
- // sibling of `org` (a User Space is owned by the person, not an org).
16
- // Bound to the SpaceKind enum adding a new kind requires extending both.
30
+ // 1. `REQUIRED_SEGMENTS` in `./space` already declares Session/Biome/User as
31
+ // root-addressable (they require ONLY their own id). The singular parser
32
+ // contradicted the kernel's own SpaceRef schema.
33
+ // 2. Every real producer of a Biome- or Session-scoped ref in the platform
34
+ // (skill-registry, llm-registry, artifact-store, workflow-engine, xvfs)
35
+ // constructs `{ tier, biomeId }` / `{ tier, sessionId }` with no org,
36
+ // project or app. `formatSpaceRef` THREW on all of them.
37
+ // 3. The plural grammar was already the persisted/wire form for object refs,
38
+ // search refs and XVFS scope paths; only Space rows used the singular one.
39
+ //
40
+ // `parse` and `format` here are exact inverses over canonical inputs, and
41
+ // `formatSpaceRef` is defined as `xema://` + `spaceRefToPath`, so the two
42
+ // renderers cannot drift again.
43
+ // ═══════════════════════════════════════════════════════════════════════════
17
44
 
18
45
  const SCHEME = 'xema://';
19
- const IDENT_REGEX = /^[a-z0-9][a-z0-9_-]*$/;
20
46
 
21
- const SEGMENT_KEYS = ['org', 'project', 'app', 'session', 'biome', 'user'] as const;
22
- type SegmentKey = (typeof SEGMENT_KEYS)[number];
47
+ /** Plural scope heads the grammar admits. Closed set. */
48
+ enum SpaceRefScopeHead {
49
+ System = 'system',
50
+ Users = 'users',
51
+ Biomes = 'biomes',
52
+ Sessions = 'sessions',
53
+ Orgs = 'orgs',
54
+ Projects = 'projects',
55
+ Apps = 'apps',
56
+ }
23
57
 
24
- const SEGMENT_KEY_SET: ReadonlySet<string> = new Set(SEGMENT_KEYS);
58
+ const SPACE_REF_SCOPE_HEADS: readonly string[] =
59
+ Object.values(SpaceRefScopeHead);
25
60
 
26
- // Ordered prefix chains — the deepest segment determines `tier`.
27
- // Each chain must be a strict prefix of one canonical path.
28
- const KIND_BY_DEEPEST_SEGMENT: Readonly<Record<SegmentKey, SpaceKind>> = {
29
- org: SpaceKind.Org,
30
- project: SpaceKind.Project,
31
- app: SpaceKind.App,
32
- session: SpaceKind.Session,
33
- biome: SpaceKind.Biome,
34
- user: SpaceKind.User,
35
- };
61
+ export { SpaceRefParseError };
36
62
 
37
- // Maps URI segment keys to the corresponding SpaceRef field name.
38
- const FIELD_OF: Readonly<Record<SegmentKey, keyof SpaceRef>> = {
39
- org: 'orgId',
40
- project: 'projectId',
41
- app: 'appId',
42
- session: 'sessionId',
43
- biome: 'biomeId',
44
- user: 'userId',
45
- };
63
+ /** Result of consuming the leading scope segments of a `xema://` path. */
64
+ export interface SpaceRefScopeParse {
65
+ scope: SpaceRef;
66
+ consumed: number;
67
+ }
68
+
69
+ function requireSegment(
70
+ segments: readonly string[],
71
+ index: number,
72
+ raw: string,
73
+ label: string,
74
+ ): string {
75
+ const value = segments[index];
76
+ if (value === undefined || value.length === 0) {
77
+ throw new SpaceRefParseError(
78
+ `SpaceRef URI is missing the "${label}" segment.`,
79
+ raw,
80
+ );
81
+ }
82
+ return value;
83
+ }
46
84
 
47
- // Allowed parent for each segment — enforces the canonical ordering.
48
- const PARENT_OF: Readonly<Record<SegmentKey, SegmentKey | null>> = {
49
- org: null,
50
- project: 'org',
51
- app: 'project',
52
- session: 'app',
53
- biome: 'app',
54
- user: null,
55
- };
85
+ function takeId(
86
+ segments: readonly string[],
87
+ index: number,
88
+ raw: string,
89
+ field: keyof SpaceRef,
90
+ ): string {
91
+ const value = requireSegment(segments, index, raw, String(field));
92
+ assertValidSpaceSegmentId(field, value, raw);
93
+ return value;
94
+ }
56
95
 
57
- export class SpaceRefParseError extends Error {
58
- readonly raw: string;
59
- constructor(message: string, raw: string) {
60
- super(message);
61
- this.name = 'SpaceRefParseError';
62
- this.raw = raw;
96
+ /**
97
+ * Consume the LEADING scope segments of a `xema://` path body and return the
98
+ * {@link SpaceRef} they denote plus how many segments were eaten. Shared by
99
+ * {@link parseSpaceRef} (which additionally requires the scope to consume the
100
+ * WHOLE body), `parseXemaObjectRef` (which owns the trailing `<kind>/<slug>`)
101
+ * and `parseSearchRef` (which owns the trailing `<docType>/<sourceId>`), so
102
+ * the scope grammar lives in exactly ONE place.
103
+ */
104
+ export function parseSpaceRefScopeSegments(
105
+ segments: readonly string[],
106
+ raw: string,
107
+ ): SpaceRefScopeParse {
108
+ const head = requireSegment(segments, 0, raw, 'scope');
109
+ switch (head) {
110
+ case SpaceRefScopeHead.System:
111
+ return { scope: { tier: SpaceKind.System }, consumed: 1 };
112
+ case SpaceRefScopeHead.Biomes:
113
+ return {
114
+ scope: {
115
+ tier: SpaceKind.Biome,
116
+ biomeId: takeId(segments, 1, raw, 'biomeId'),
117
+ },
118
+ consumed: 2,
119
+ };
120
+ case SpaceRefScopeHead.Users:
121
+ return {
122
+ scope: {
123
+ tier: SpaceKind.User,
124
+ userId: takeId(segments, 1, raw, 'userId'),
125
+ },
126
+ consumed: 2,
127
+ };
128
+ case SpaceRefScopeHead.Sessions:
129
+ return {
130
+ scope: {
131
+ tier: SpaceKind.Session,
132
+ sessionId: takeId(segments, 1, raw, 'sessionId'),
133
+ },
134
+ consumed: 2,
135
+ };
136
+ case SpaceRefScopeHead.Orgs: {
137
+ const orgId = takeId(segments, 1, raw, 'orgId');
138
+ if (segments.length >= 4 && segments[2] === SpaceRefScopeHead.Projects) {
139
+ const projectId = takeId(segments, 3, raw, 'projectId');
140
+ // …/projects/<p>/apps/<a> — the App tier (most specific under a project).
141
+ if (segments.length >= 6 && segments[4] === SpaceRefScopeHead.Apps) {
142
+ return {
143
+ scope: {
144
+ tier: SpaceKind.App,
145
+ orgId,
146
+ projectId,
147
+ appId: takeId(segments, 5, raw, 'appId'),
148
+ },
149
+ consumed: 6,
150
+ };
151
+ }
152
+ return {
153
+ scope: { tier: SpaceKind.Project, orgId, projectId },
154
+ consumed: 4,
155
+ };
156
+ }
157
+ return { scope: { tier: SpaceKind.Org, orgId }, consumed: 2 };
158
+ }
159
+ default:
160
+ throw new SpaceRefParseError(
161
+ `SpaceRef URI has unknown scope head "${head}". Allowed: ${SPACE_REF_SCOPE_HEADS.join(', ')}.`,
162
+ raw,
163
+ );
63
164
  }
64
165
  }
65
166
 
167
+ /**
168
+ * Parse a full `xema://…` Space URI. Unlike
169
+ * {@link parseSpaceRefScopeSegments}, the scope MUST consume the entire body —
170
+ * a trailing `<kind>/<slug>` makes it an object ref, not a Space ref, and is a
171
+ * fail-fast error here rather than a silently-ignored suffix.
172
+ *
173
+ * The returned ref carries `path` set to the CANONICAL rendering, so
174
+ * `formatSpaceRef(parseSpaceRef(uri)) === uri` for every canonical `uri`.
175
+ */
66
176
  export function parseSpaceRef(uri: string): SpaceRef {
67
177
  if (typeof uri !== 'string' || uri.length === 0) {
68
- throw new SpaceRefParseError('SpaceRef URI must be a non-empty string.', String(uri));
178
+ throw new SpaceRefParseError(
179
+ 'SpaceRef URI must be a non-empty string.',
180
+ String(uri),
181
+ );
69
182
  }
70
183
  if (!uri.startsWith(SCHEME)) {
71
- throw new SpaceRefParseError(`SpaceRef URI must start with "${SCHEME}".`, uri);
184
+ throw new SpaceRefParseError(
185
+ `SpaceRef URI must start with "${SCHEME}".`,
186
+ uri,
187
+ );
72
188
  }
73
189
  const body = uri.slice(SCHEME.length);
74
190
  if (body.length === 0) {
75
- throw new SpaceRefParseError('SpaceRef URI has empty body after scheme.', uri);
76
- }
77
- const parts = body.split('/');
78
-
79
- if (parts.length === 1 && parts[0] === 'system') {
80
- return { tier: SpaceKind.System, path: uri };
81
- }
82
-
83
- if (parts.length % 2 !== 0) {
84
191
  throw new SpaceRefParseError(
85
- `SpaceRef URI body must be pairs of <segment>/<id> (got ${parts.length} parts).`,
192
+ 'SpaceRef URI has empty body after scheme.',
86
193
  uri,
87
194
  );
88
195
  }
89
-
90
- const segments: Partial<Record<SegmentKey, string>> = {};
91
- let lastSegment: SegmentKey | null = null;
92
-
93
- for (let i = 0; i < parts.length; i += 2) {
94
- const key = parts[i]!;
95
- const value = parts[i + 1]!;
96
- if (!SEGMENT_KEY_SET.has(key)) {
97
- throw new SpaceRefParseError(
98
- `SpaceRef URI has unknown segment key "${key}". Allowed: ${SEGMENT_KEYS.join(', ')}.`,
99
- uri,
100
- );
101
- }
102
- const typedKey = key as SegmentKey;
103
- if (segments[typedKey] !== undefined) {
104
- throw new SpaceRefParseError(`SpaceRef URI repeats segment "${key}".`, uri);
105
- }
106
- if (!IDENT_REGEX.test(value)) {
107
- throw new SpaceRefParseError(
108
- `SpaceRef URI segment "${key}" has invalid id "${value}".`,
109
- uri,
110
- );
111
- }
112
- const expectedParent = PARENT_OF[typedKey];
113
- if (expectedParent !== lastSegment) {
114
- throw new SpaceRefParseError(
115
- `SpaceRef URI segment "${key}" must follow "${expectedParent ?? '(root)'}", got "${lastSegment ?? '(root)'}".`,
116
- uri,
117
- );
118
- }
119
- segments[typedKey] = value;
120
- lastSegment = typedKey;
196
+ const segments = body.split('/');
197
+ if (segments.some((segment) => segment.length === 0)) {
198
+ throw new SpaceRefParseError(
199
+ 'SpaceRef URI has an empty path segment (leading, trailing or doubled "/").',
200
+ uri,
201
+ );
121
202
  }
122
203
 
123
- if (lastSegment === null) {
124
- throw new SpaceRefParseError('SpaceRef URI parsed no segments.', uri);
204
+ const { scope, consumed } = parseSpaceRefScopeSegments(segments, uri);
205
+ if (consumed !== segments.length) {
206
+ throw new SpaceRefParseError(
207
+ `SpaceRef URI has ${segments.length - consumed} trailing segment(s) after the scope — a Space ref addresses a Space, not an object inside it.`,
208
+ uri,
209
+ );
125
210
  }
126
211
 
127
- const tier = KIND_BY_DEEPEST_SEGMENT[lastSegment];
128
- const ref: SpaceRef = { tier, path: uri };
129
- for (const segKey of SEGMENT_KEYS) {
130
- const segValue = segments[segKey];
131
- if (segValue !== undefined) {
132
- const fieldName = FIELD_OF[segKey];
133
- (ref as unknown as Record<string, string>)[fieldName] = segValue;
134
- }
135
- }
136
- return ref;
212
+ scope.path = formatSpaceRef(scope);
213
+ return scope;
137
214
  }
138
215
 
216
+ /**
217
+ * Render a `SpaceRef` as its canonical `xema://…` URI. Defined as the scheme
218
+ * plus {@link spaceRefToPath}, so the URI form and the XVFS path form can
219
+ * never disagree again. Fails fast on a missing or malformed segment.
220
+ */
139
221
  export function formatSpaceRef(ref: SpaceRef): string {
140
- if (ref.tier === SpaceKind.System) {
141
- return `${SCHEME}system`;
142
- }
143
- const out: string[] = [SCHEME];
144
- const append = (key: SegmentKey, value: string | undefined): void => {
145
- if (value === undefined) {
146
- throw new SpaceRefParseError(
147
- `formatSpaceRef: missing required segment "${key}" for tier "${ref.tier}".`,
148
- ref.tier,
149
- );
150
- }
151
- if (!IDENT_REGEX.test(value)) {
152
- throw new SpaceRefParseError(
153
- `formatSpaceRef: segment "${key}" id "${value}" violates grammar.`,
154
- ref.tier,
155
- );
156
- }
157
- if (out.length > 1) out.push('/');
158
- out.push(`${key}/${value}`);
159
- };
160
-
161
- switch (ref.tier) {
162
- case SpaceKind.User:
163
- append('user', ref.userId);
164
- break;
165
- case SpaceKind.Org:
166
- append('org', ref.orgId);
167
- break;
168
- case SpaceKind.Project:
169
- append('org', ref.orgId);
170
- append('project', ref.projectId);
171
- break;
172
- case SpaceKind.App:
173
- append('org', ref.orgId);
174
- append('project', ref.projectId);
175
- append('app', ref.appId);
176
- break;
177
- case SpaceKind.Session:
178
- append('org', ref.orgId);
179
- append('project', ref.projectId);
180
- append('app', ref.appId);
181
- append('session', ref.sessionId);
182
- break;
183
- case SpaceKind.Biome:
184
- append('org', ref.orgId);
185
- append('project', ref.projectId);
186
- append('app', ref.appId);
187
- append('biome', ref.biomeId);
188
- break;
189
- default: {
190
- const exhaustive: never = ref.tier;
191
- throw new SpaceRefParseError(
192
- `formatSpaceRef: unhandled SpaceKind "${String(exhaustive)}".`,
193
- String(exhaustive),
194
- );
195
- }
196
- }
197
- return out.join('');
222
+ return `${SCHEME}${spaceRefToPath(ref).slice(1)}`;
198
223
  }
@@ -1,13 +1,28 @@
1
1
  import { formatSpaceRef } from './space-ref-parser';
2
2
  import { SpaceKind, type SpaceRef } from './space';
3
3
 
4
- // Closest-first ancestor walk. For a Project space the walk is
5
- // [Project, Org, System]; for a Session space it is
6
- // [Session, App, Project, Org, System]; for a User space it is
7
- // [User, System]. Used by policy resolution: "most-specific match wins".
4
+ // Closest-first ancestor walk. The chain MUST agree with the URI grammar in
5
+ // `./space-ref-parser` an ancestor is exactly "this ref with its deepest
6
+ // segment stripped", so the walk is the structural reading of the same
7
+ // canonical path:
8
8
  //
9
- // Implementation: derive each ancestor by stripping the deepest segment.
10
- // We rebuild via formatSpaceRef so the returned `path` is always canonical.
9
+ // /orgs/a/projects/p/apps/x → [App, Project, Org, System]
10
+ // /orgs/a/projects/p → [Project, Org, System]
11
+ // /sessions/s → [Session, System]
12
+ // /biomes/b → [Biome, System]
13
+ // /users/u → [User, System]
14
+ //
15
+ // Session, Biome and User are ROOTS (parent = System). Before the X9
16
+ // convergence this file nested Session and Biome under App while the path
17
+ // renderer emitted them at the root, so `walkAncestors` THREW for every
18
+ // Biome-scoped ref the platform actually produces (`{ tier: Biome, biomeId }`
19
+ // carries no appId, and rendering the synthesised App parent failed). Keeping
20
+ // the hierarchy and the grammar in lockstep is what makes `parentOf` total.
21
+ //
22
+ // Used by policy resolution ("most-specific match wins"), by
23
+ // space-registry-api to pin `Space.parentId` from `walkAncestors(ref)[1]`, and
24
+ // by capability-registry-api's `SPACE_KIND_PARENT` derivation — all three read
25
+ // the hierarchy from HERE and never restate it.
11
26
  export function walkAncestors(ref: SpaceRef): SpaceRef[] {
12
27
  const out: SpaceRef[] = [];
13
28
  let current: SpaceRef | null = ref;
@@ -18,13 +33,22 @@ export function walkAncestors(ref: SpaceRef): SpaceRef[] {
18
33
  return out;
19
34
  }
20
35
 
36
+ function systemParent(): SpaceRef {
37
+ return {
38
+ tier: SpaceKind.System,
39
+ path: formatSpaceRef({ tier: SpaceKind.System }),
40
+ };
41
+ }
42
+
21
43
  function parentOf(ref: SpaceRef): SpaceRef | null {
22
44
  switch (ref.tier) {
23
45
  case SpaceKind.System:
24
46
  return null;
25
47
  case SpaceKind.User:
26
48
  case SpaceKind.Org:
27
- return { tier: SpaceKind.System, path: formatSpaceRef({ tier: SpaceKind.System }) };
49
+ case SpaceKind.Session:
50
+ case SpaceKind.Biome:
51
+ return systemParent();
28
52
  case SpaceKind.Project: {
29
53
  const parent: SpaceRef = { tier: SpaceKind.Org };
30
54
  if (ref.orgId !== undefined) parent.orgId = ref.orgId;
@@ -38,18 +62,11 @@ function parentOf(ref: SpaceRef): SpaceRef | null {
38
62
  parent.path = formatSpaceRef(parent);
39
63
  return parent;
40
64
  }
41
- case SpaceKind.Session:
42
- case SpaceKind.Biome: {
43
- const parent: SpaceRef = { tier: SpaceKind.App };
44
- if (ref.orgId !== undefined) parent.orgId = ref.orgId;
45
- if (ref.projectId !== undefined) parent.projectId = ref.projectId;
46
- if (ref.appId !== undefined) parent.appId = ref.appId;
47
- parent.path = formatSpaceRef(parent);
48
- return parent;
49
- }
50
65
  default: {
51
66
  const exhaustive: never = ref.tier;
52
- throw new Error(`walkAncestors: unhandled SpaceKind "${String(exhaustive)}".`);
67
+ throw new Error(
68
+ `walkAncestors: unhandled SpaceKind "${String(exhaustive)}".`,
69
+ );
53
70
  }
54
71
  }
55
72
  }