@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
@@ -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
  }
@@ -56,6 +56,44 @@ export interface SpaceRef {
56
56
  export const SpaceKindSchema = z.nativeEnum(SpaceKind);
57
57
  export const DataClassificationSchema = z.nativeEnum(DataClassification);
58
58
 
59
+ /**
60
+ * Total order over {@link DataClassification} — least → most restrictive.
61
+ *
62
+ * Public(0) < Internal(1) < Confidential(2) < Secret(3) < Regulated(4)
63
+ *
64
+ * This is the SINGLE authoritative ordering of the closed set (consumed by the
65
+ * capability-router's `restrict-output-classification` obligation clamp, and by
66
+ * any future declared-vs-cap comparison). Every enum member has a rank so the
67
+ * comparison is total and deterministic — never an ad-hoc string compare.
68
+ *
69
+ * `Regulated` ranks HIGHEST (most restrictive) on purpose: data under a
70
+ * regulatory regime is at least as sensitive as `Secret`, and putting it at the
71
+ * top makes the cap comparison fail CLOSED — a capability declaring `regulated`
72
+ * output is refused unless the cap is also `regulated`, never accidentally
73
+ * treated as below `secret`.
74
+ */
75
+ export const DATA_CLASSIFICATION_RANK: Readonly<
76
+ Record<DataClassification, number>
77
+ > = {
78
+ [DataClassification.Public]: 0,
79
+ [DataClassification.Internal]: 1,
80
+ [DataClassification.Confidential]: 2,
81
+ [DataClassification.Secret]: 3,
82
+ [DataClassification.Regulated]: 4,
83
+ };
84
+
85
+ /**
86
+ * `true` iff `candidate` is STRICTLY more restrictive than `cap` in the
87
+ * {@link DATA_CLASSIFICATION_RANK} order — i.e. `candidate` "exceeds the cap".
88
+ * Equal or lower ranks return `false` (the cap is satisfied).
89
+ */
90
+ export function dataClassificationExceeds(
91
+ candidate: DataClassification,
92
+ cap: DataClassification,
93
+ ): boolean {
94
+ return DATA_CLASSIFICATION_RANK[candidate] > DATA_CLASSIFICATION_RANK[cap];
95
+ }
96
+
59
97
  type SegmentKey =
60
98
  | 'orgId'
61
99
  | 'projectId'
@@ -110,48 +148,123 @@ const REQUIRED_SEGMENTS: Readonly<Record<SpaceKind, readonly SegmentKey[]>> = {
110
148
  };
111
149
 
112
150
  /**
113
- * Render a `SpaceRef` as its XVFS path prefix (used by `XemaObjectRef`
114
- * formatting and by object-registry-api projections). Returns a string
115
- * that starts with `/`. Fails fast if a required segment is missing.
151
+ * Structured, fail-fast error raised by every Space-ref grammar operation
152
+ * ({@link spaceRefToPath}, `parseSpaceRef`, `formatSpaceRef`). Carries a
153
+ * stable `code` so services can map it to a typed 400 without string-matching
154
+ * the message.
155
+ */
156
+ export class SpaceRefParseError extends Error {
157
+ public readonly code = 'SPACE_REF_INVALID';
158
+ public readonly raw: string;
159
+
160
+ public constructor(message: string, raw: string) {
161
+ super(message);
162
+ this.name = 'SpaceRefParseError';
163
+ this.raw = raw;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * A single Space path segment id (an orgId, projectId, appId, sessionId,
169
+ * biomeId or userId). Deliberately value-identical to the `XemaObjectRef`
170
+ * slug-segment pattern so a Space path and an object ref agree on what a
171
+ * legal segment is: starts alphanumeric, then alphanumerics plus `_`, `.`,
172
+ * `-`. No `/`, no whitespace, no `@` — those are grammar separators and
173
+ * would make the ref ambiguous.
174
+ */
175
+ export const SPACE_SEGMENT_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
176
+
177
+ /**
178
+ * Fail-fast guard for a Space path segment id. Enforced by BOTH the renderer
179
+ * ({@link spaceRefToPath}) and the parser, which is what makes parse/format
180
+ * exact inverses: a ref that cannot be rendered can never be produced, and a
181
+ * string that cannot be parsed can never be rendered.
182
+ */
183
+ export function assertValidSpaceSegmentId(
184
+ segment: keyof SpaceRef,
185
+ value: string,
186
+ raw: string,
187
+ ): void {
188
+ if (!SPACE_SEGMENT_ID_PATTERN.test(value)) {
189
+ throw new SpaceRefParseError(
190
+ `Space segment "${String(segment)}" id "${value}" must match ${SPACE_SEGMENT_ID_PATTERN}.`,
191
+ raw,
192
+ );
193
+ }
194
+ }
195
+
196
+ function requireSpaceSegment(
197
+ ref: SpaceRef,
198
+ segment: SegmentKey,
199
+ raw: string,
200
+ ): string {
201
+ const value = ref[segment];
202
+ if (value === undefined || value.length === 0) {
203
+ throw new SpaceRefParseError(
204
+ `SpaceKind.${ref.tier} requires "${segment}".`,
205
+ raw,
206
+ );
207
+ }
208
+ assertValidSpaceSegmentId(segment, value, raw);
209
+ return value;
210
+ }
211
+
212
+ /**
213
+ * Render a `SpaceRef` as its canonical XVFS path prefix. Returns a string
214
+ * that starts with `/`. Fails fast if a required segment is missing or its id
215
+ * violates {@link SPACE_SEGMENT_ID_PATTERN}.
216
+ *
217
+ * This is the SINGLE canonical Space grammar — the plural-head form shared by
218
+ * `XemaObjectRef`, search refs, the object-registry projection and (since the
219
+ * X9 convergence) `formatSpaceRef`/`parseSpaceRef`:
220
+ *
221
+ * ```
222
+ * /system
223
+ * /users/<userId>
224
+ * /biomes/<biomeId>
225
+ * /sessions/<sessionId>
226
+ * /orgs/<orgId>
227
+ * /orgs/<orgId>/projects/<projectId>
228
+ * /orgs/<orgId>/projects/<projectId>/apps/<appId>
229
+ * ```
230
+ *
231
+ * `User`, `Biome` and `Session` are ROOT-addressable on purpose — that is
232
+ * exactly what {@link REQUIRED_SEGMENTS} already declares (they require only
233
+ * their own id), and it is what every real producer emits: a biome-contributed
234
+ * skill lives at `xema://biomes/<id>/skill/<slug>` and is org-less. A ref's
235
+ * ancestor segments are NOT part of its identity for those tiers.
116
236
  */
117
237
  export function spaceRefToPath(ref: SpaceRef): string {
238
+ const raw = ref.path ?? `<SpaceKind.${ref.tier}>`;
118
239
  switch (ref.tier) {
119
240
  case SpaceKind.System:
120
241
  return '/system';
121
242
  case SpaceKind.Biome:
122
- if (!ref.biomeId) {
123
- throw new Error('spaceRefToPath: SpaceKind.Biome requires biomeId');
124
- }
125
- return `/biomes/${ref.biomeId}`;
126
- case SpaceKind.Org:
127
- if (!ref.orgId) {
128
- throw new Error('spaceRefToPath: SpaceKind.Org requires orgId');
129
- }
130
- return `/orgs/${ref.orgId}`;
131
- case SpaceKind.Project:
132
- if (!ref.orgId || !ref.projectId) {
133
- throw new Error(
134
- 'spaceRefToPath: SpaceKind.Project requires orgId and projectId',
135
- );
136
- }
137
- return `/orgs/${ref.orgId}/projects/${ref.projectId}`;
138
- case SpaceKind.App:
139
- if (!ref.orgId || !ref.projectId || !ref.appId) {
140
- throw new Error(
141
- 'spaceRefToPath: SpaceKind.App requires orgId, projectId, and appId',
142
- );
143
- }
144
- return `/orgs/${ref.orgId}/projects/${ref.projectId}/apps/${ref.appId}`;
145
- case SpaceKind.Session:
146
- if (!ref.sessionId) {
147
- throw new Error('spaceRefToPath: SpaceKind.Session requires sessionId');
148
- }
149
- return `/sessions/${ref.sessionId}`;
243
+ return `/biomes/${requireSpaceSegment(ref, 'biomeId', raw)}`;
150
244
  case SpaceKind.User:
151
- if (!ref.userId) {
152
- throw new Error('spaceRefToPath: SpaceKind.User requires userId');
153
- }
154
- return `/users/${ref.userId}`;
245
+ return `/users/${requireSpaceSegment(ref, 'userId', raw)}`;
246
+ case SpaceKind.Session:
247
+ return `/sessions/${requireSpaceSegment(ref, 'sessionId', raw)}`;
248
+ case SpaceKind.Org:
249
+ return `/orgs/${requireSpaceSegment(ref, 'orgId', raw)}`;
250
+ case SpaceKind.Project: {
251
+ const orgId = requireSpaceSegment(ref, 'orgId', raw);
252
+ const projectId = requireSpaceSegment(ref, 'projectId', raw);
253
+ return `/orgs/${orgId}/projects/${projectId}`;
254
+ }
255
+ case SpaceKind.App: {
256
+ const orgId = requireSpaceSegment(ref, 'orgId', raw);
257
+ const projectId = requireSpaceSegment(ref, 'projectId', raw);
258
+ const appId = requireSpaceSegment(ref, 'appId', raw);
259
+ return `/orgs/${orgId}/projects/${projectId}/apps/${appId}`;
260
+ }
261
+ default: {
262
+ const exhaustive: never = ref.tier;
263
+ throw new SpaceRefParseError(
264
+ `spaceRefToPath: unhandled SpaceKind "${String(exhaustive)}".`,
265
+ raw,
266
+ );
267
+ }
155
268
  }
156
269
  }
157
270