@milaboratories/pl-model-common 1.47.3 → 1.49.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 (242) hide show
  1. package/dist/author_marker.d.ts +1 -2
  2. package/dist/author_marker.d.ts.map +1 -1
  3. package/dist/base64.d.ts +4 -6
  4. package/dist/base64.d.ts.map +1 -1
  5. package/dist/block_state.d.ts +13 -9
  6. package/dist/block_state.d.ts.map +1 -1
  7. package/dist/bmodel/block_config.d.ts +51 -29
  8. package/dist/bmodel/block_config.d.ts.map +1 -1
  9. package/dist/bmodel/block_kind_ref.cjs +45 -0
  10. package/dist/bmodel/block_kind_ref.cjs.map +1 -0
  11. package/dist/bmodel/block_kind_ref.d.ts +55 -0
  12. package/dist/bmodel/block_kind_ref.d.ts.map +1 -0
  13. package/dist/bmodel/block_kind_ref.js +43 -0
  14. package/dist/bmodel/block_kind_ref.js.map +1 -0
  15. package/dist/bmodel/code.d.ts +4 -5
  16. package/dist/bmodel/code.d.ts.map +1 -1
  17. package/dist/bmodel/container.d.ts +13 -5
  18. package/dist/bmodel/container.d.ts.map +1 -1
  19. package/dist/bmodel/index.cjs +4 -0
  20. package/dist/bmodel/index.d.ts +2 -1
  21. package/dist/bmodel/index.js +2 -1
  22. package/dist/bmodel/normalization.d.ts +1 -3
  23. package/dist/bmodel/normalization.d.ts.map +1 -1
  24. package/dist/bmodel/types.d.ts +1 -2
  25. package/dist/bmodel/types.d.ts.map +1 -1
  26. package/dist/branding.d.ts +2 -3
  27. package/dist/branding.d.ts.map +1 -1
  28. package/dist/columns/accessor_traversal.d.ts +6 -8
  29. package/dist/columns/accessor_traversal.d.ts.map +1 -1
  30. package/dist/columns/column_registry.d.ts +2 -3
  31. package/dist/columns/column_registry.d.ts.map +1 -1
  32. package/dist/columns/column_selector.d.ts +9 -9
  33. package/dist/columns/column_selector.d.ts.map +1 -1
  34. package/dist/columns/dedup.cjs +1 -1
  35. package/dist/columns/dedup.cjs.map +1 -1
  36. package/dist/columns/dedup.d.ts +2 -4
  37. package/dist/columns/dedup.d.ts.map +1 -1
  38. package/dist/columns/dedup.js +1 -1
  39. package/dist/columns/dedup.js.map +1 -1
  40. package/dist/columns/providers.cjs +1 -1
  41. package/dist/columns/providers.cjs.map +1 -1
  42. package/dist/columns/providers.d.ts +4 -5
  43. package/dist/columns/providers.d.ts.map +1 -1
  44. package/dist/columns/providers.js +1 -1
  45. package/dist/columns/providers.js.map +1 -1
  46. package/dist/columns/types.d.ts +12 -9
  47. package/dist/columns/types.d.ts.map +1 -1
  48. package/dist/common_types.d.ts +4 -6
  49. package/dist/common_types.d.ts.map +1 -1
  50. package/dist/dialog/index.d.ts +4 -5
  51. package/dist/dialog/index.d.ts.map +1 -1
  52. package/dist/driver_kit.d.ts +2 -2
  53. package/dist/driver_kit.d.ts.map +1 -1
  54. package/dist/drivers/ChunkedStreamReader.d.ts +3 -5
  55. package/dist/drivers/ChunkedStreamReader.d.ts.map +1 -1
  56. package/dist/drivers/blob.d.ts +36 -25
  57. package/dist/drivers/blob.d.ts.map +1 -1
  58. package/dist/drivers/columns/columns_collection_driver.d.ts +6 -7
  59. package/dist/drivers/columns/columns_collection_driver.d.ts.map +1 -1
  60. package/dist/drivers/columns/discover_columns_options.d.ts +9 -8
  61. package/dist/drivers/columns/discover_columns_options.d.ts.map +1 -1
  62. package/dist/drivers/index.cjs +5 -0
  63. package/dist/drivers/index.d.ts +5 -3
  64. package/dist/drivers/index.js +3 -3
  65. package/dist/drivers/interfaces.d.ts +2 -3
  66. package/dist/drivers/interfaces.d.ts.map +1 -1
  67. package/dist/drivers/log.d.ts +26 -28
  68. package/dist/drivers/log.d.ts.map +1 -1
  69. package/dist/drivers/ls.d.ts +37 -27
  70. package/dist/drivers/ls.d.ts.map +1 -1
  71. package/dist/drivers/pframe/column_filter.d.ts +1 -3
  72. package/dist/drivers/pframe/column_filter.d.ts.map +1 -1
  73. package/dist/drivers/pframe/data_info.d.ts +77 -50
  74. package/dist/drivers/pframe/data_info.d.ts.map +1 -1
  75. package/dist/drivers/pframe/data_types.d.ts +50 -45
  76. package/dist/drivers/pframe/data_types.d.ts.map +1 -1
  77. package/dist/drivers/pframe/driver.d.ts +5 -5
  78. package/dist/drivers/pframe/driver.d.ts.map +1 -1
  79. package/dist/drivers/pframe/filter_spec.d.ts +10 -12
  80. package/dist/drivers/pframe/filter_spec.d.ts.map +1 -1
  81. package/dist/drivers/pframe/find_columns.d.ts +2 -4
  82. package/dist/drivers/pframe/find_columns.d.ts.map +1 -1
  83. package/dist/drivers/pframe/index.cjs +5 -0
  84. package/dist/drivers/pframe/index.d.ts +5 -3
  85. package/dist/drivers/pframe/index.js +3 -3
  86. package/dist/drivers/pframe/linker_columns.d.ts +4 -10
  87. package/dist/drivers/pframe/linker_columns.d.ts.map +1 -1
  88. package/dist/drivers/pframe/pframe.d.ts +3 -3
  89. package/dist/drivers/pframe/pframe.d.ts.map +1 -1
  90. package/dist/drivers/pframe/query/query_common.d.ts +52 -46
  91. package/dist/drivers/pframe/query/query_common.d.ts.map +1 -1
  92. package/dist/drivers/pframe/query/query_data.d.ts +26 -21
  93. package/dist/drivers/pframe/query/query_data.d.ts.map +1 -1
  94. package/dist/drivers/pframe/query/query_spec.d.ts +22 -19
  95. package/dist/drivers/pframe/query/query_spec.d.ts.map +1 -1
  96. package/dist/drivers/pframe/query/utils.d.ts +15 -11
  97. package/dist/drivers/pframe/query/utils.d.ts.map +1 -1
  98. package/dist/drivers/pframe/spec/anchored.d.ts +3 -5
  99. package/dist/drivers/pframe/spec/anchored.d.ts.map +1 -1
  100. package/dist/drivers/pframe/spec/discovered_column.d.ts +10 -11
  101. package/dist/drivers/pframe/spec/discovered_column.d.ts.map +1 -1
  102. package/dist/drivers/pframe/spec/filtered_column.d.ts +17 -18
  103. package/dist/drivers/pframe/spec/filtered_column.d.ts.map +1 -1
  104. package/dist/drivers/pframe/spec/ids.cjs +151 -0
  105. package/dist/drivers/pframe/spec/ids.cjs.map +1 -1
  106. package/dist/drivers/pframe/spec/ids.d.ts +63 -11
  107. package/dist/drivers/pframe/spec/ids.d.ts.map +1 -1
  108. package/dist/drivers/pframe/spec/ids.js +150 -3
  109. package/dist/drivers/pframe/spec/ids.js.map +1 -1
  110. package/dist/drivers/pframe/spec/index.cjs +5 -0
  111. package/dist/drivers/pframe/spec/index.d.ts +3 -3
  112. package/dist/drivers/pframe/spec/index.js +3 -3
  113. package/dist/drivers/pframe/spec/native_id.d.ts +3 -3
  114. package/dist/drivers/pframe/spec/native_id.d.ts.map +1 -1
  115. package/dist/drivers/pframe/spec/overridden.d.ts +14 -16
  116. package/dist/drivers/pframe/spec/overridden.d.ts.map +1 -1
  117. package/dist/drivers/pframe/spec/selectors.d.ts +22 -22
  118. package/dist/drivers/pframe/spec/selectors.d.ts.map +1 -1
  119. package/dist/drivers/pframe/spec/spec.cjs +31 -0
  120. package/dist/drivers/pframe/spec/spec.cjs.map +1 -1
  121. package/dist/drivers/pframe/spec/spec.d.ts +94 -70
  122. package/dist/drivers/pframe/spec/spec.d.ts.map +1 -1
  123. package/dist/drivers/pframe/spec/spec.js +31 -1
  124. package/dist/drivers/pframe/spec/spec.js.map +1 -1
  125. package/dist/drivers/pframe/spec_driver.d.ts +30 -25
  126. package/dist/drivers/pframe/spec_driver.d.ts.map +1 -1
  127. package/dist/drivers/pframe/table.d.ts +1 -3
  128. package/dist/drivers/pframe/table.d.ts.map +1 -1
  129. package/dist/drivers/pframe/table_calculate.d.ts +55 -46
  130. package/dist/drivers/pframe/table_calculate.d.ts.map +1 -1
  131. package/dist/drivers/pframe/table_common.d.ts +9 -10
  132. package/dist/drivers/pframe/table_common.d.ts.map +1 -1
  133. package/dist/drivers/pframe/unique_values.d.ts +3 -3
  134. package/dist/drivers/pframe/unique_values.d.ts.map +1 -1
  135. package/dist/drivers/upload.d.ts +2 -3
  136. package/dist/drivers/upload.d.ts.map +1 -1
  137. package/dist/drivers/urls.d.ts +7 -8
  138. package/dist/drivers/urls.d.ts.map +1 -1
  139. package/dist/errors.d.ts +35 -36
  140. package/dist/errors.d.ts.map +1 -1
  141. package/dist/flags/block_flags.cjs.map +1 -1
  142. package/dist/flags/block_flags.d.ts +7 -7
  143. package/dist/flags/block_flags.d.ts.map +1 -1
  144. package/dist/flags/block_flags.js.map +1 -1
  145. package/dist/flags/flag_utils.d.ts +6 -8
  146. package/dist/flags/flag_utils.d.ts.map +1 -1
  147. package/dist/flags/type_utils.d.ts +6 -7
  148. package/dist/flags/type_utils.d.ts.map +1 -1
  149. package/dist/httpAuth.d.ts +4 -5
  150. package/dist/httpAuth.d.ts.map +1 -1
  151. package/dist/index.cjs +30 -0
  152. package/dist/index.d.ts +15 -3
  153. package/dist/index.js +9 -3
  154. package/dist/json.d.ts +11 -12
  155. package/dist/json.d.ts.map +1 -1
  156. package/dist/navigation.d.ts +16 -11
  157. package/dist/navigation.d.ts.map +1 -1
  158. package/dist/plid.cjs +1 -1
  159. package/dist/plid.cjs.map +1 -1
  160. package/dist/plid.d.ts +8 -9
  161. package/dist/plid.d.ts.map +1 -1
  162. package/dist/plid.js +1 -1
  163. package/dist/plid.js.map +1 -1
  164. package/dist/pool/entry.d.ts +8 -6
  165. package/dist/pool/entry.d.ts.map +1 -1
  166. package/dist/pool/query.d.ts +3 -4
  167. package/dist/pool/query.d.ts.map +1 -1
  168. package/dist/pool/spec.d.ts +33 -30
  169. package/dist/pool/spec.d.ts.map +1 -1
  170. package/dist/pool_entry.d.ts +2 -3
  171. package/dist/pool_entry.d.ts.map +1 -1
  172. package/dist/project_id.d.ts +1 -3
  173. package/dist/project_id.d.ts.map +1 -1
  174. package/dist/ref.d.ts +22 -23
  175. package/dist/ref.d.ts.map +1 -1
  176. package/dist/resource_types.d.ts +2 -3
  177. package/dist/resource_types.d.ts.map +1 -1
  178. package/dist/services/node_service_handlers.d.ts +1 -3
  179. package/dist/services/node_service_handlers.d.ts.map +1 -1
  180. package/dist/services/service_capabilities.d.ts +7 -9
  181. package/dist/services/service_capabilities.d.ts.map +1 -1
  182. package/dist/services/service_declarations.d.ts +2 -2
  183. package/dist/services/service_declarations.d.ts.map +1 -1
  184. package/dist/services/service_injectors.d.ts +7 -8
  185. package/dist/services/service_injectors.d.ts.map +1 -1
  186. package/dist/services/service_registry.d.ts +2 -4
  187. package/dist/services/service_registry.d.ts.map +1 -1
  188. package/dist/services/service_types.d.ts +24 -26
  189. package/dist/services/service_types.d.ts.map +1 -1
  190. package/dist/template/index.cjs +20 -0
  191. package/dist/template/index.d.ts +5 -0
  192. package/dist/template/index.js +5 -0
  193. package/dist/template/kind_selector.cjs +92 -0
  194. package/dist/template/kind_selector.cjs.map +1 -0
  195. package/dist/template/kind_selector.d.ts +78 -0
  196. package/dist/template/kind_selector.d.ts.map +1 -0
  197. package/dist/template/kind_selector.js +87 -0
  198. package/dist/template/kind_selector.js.map +1 -0
  199. package/dist/template/project_template_v1.cjs +231 -0
  200. package/dist/template/project_template_v1.cjs.map +1 -0
  201. package/dist/template/project_template_v1.d.ts +217 -0
  202. package/dist/template/project_template_v1.d.ts.map +1 -0
  203. package/dist/template/project_template_v1.js +225 -0
  204. package/dist/template/project_template_v1.js.map +1 -0
  205. package/dist/template/template_ref_form.cjs +73 -0
  206. package/dist/template/template_ref_form.cjs.map +1 -0
  207. package/dist/template/template_ref_form.d.ts +75 -0
  208. package/dist/template/template_ref_form.d.ts.map +1 -0
  209. package/dist/template/template_ref_form.js +72 -0
  210. package/dist/template/template_ref_form.js.map +1 -0
  211. package/dist/template/template_relocate.cjs +67 -0
  212. package/dist/template/template_relocate.cjs.map +1 -0
  213. package/dist/template/template_relocate.d.ts +36 -0
  214. package/dist/template/template_relocate.d.ts.map +1 -0
  215. package/dist/template/template_relocate.js +67 -0
  216. package/dist/template/template_relocate.js.map +1 -0
  217. package/dist/utag.d.ts +2 -4
  218. package/dist/utag.d.ts.map +1 -1
  219. package/dist/util.d.ts +2 -3
  220. package/dist/util.d.ts.map +1 -1
  221. package/dist/value_or_error.d.ts +2 -3
  222. package/dist/value_or_error.d.ts.map +1 -1
  223. package/package.json +5 -5
  224. package/src/bmodel/block_kind_ref.ts +59 -0
  225. package/src/bmodel/container.ts +9 -0
  226. package/src/bmodel/index.ts +1 -0
  227. package/src/columns/dedup.ts +1 -1
  228. package/src/columns/providers.ts +1 -1
  229. package/src/drivers/pframe/spec/ids.test.ts +90 -0
  230. package/src/drivers/pframe/spec/ids.ts +191 -1
  231. package/src/drivers/pframe/spec/spec.ts +31 -0
  232. package/src/flags/block_flags.ts +1 -1
  233. package/src/index.ts +1 -0
  234. package/src/plid.ts +5 -5
  235. package/src/template/index.ts +4 -0
  236. package/src/template/kind_selector.ts +126 -0
  237. package/src/template/project_template_v1.test.ts +315 -0
  238. package/src/template/project_template_v1.ts +444 -0
  239. package/src/template/template_ref_form.test.ts +86 -0
  240. package/src/template/template_ref_form.ts +108 -0
  241. package/src/template/template_relocate.test.ts +239 -0
  242. package/src/template/template_relocate.ts +89 -0
@@ -83,6 +83,37 @@ export const Domain = {
83
83
  },
84
84
  } as const;
85
85
 
86
+ /**
87
+ * Domain keys whose value is the id of a block, rather than a qualifier of the data.
88
+ *
89
+ * Applying a template repoints these at the blocks of the project being built: an axis a block
90
+ * produced names that block in its domain, so an axis left naming the exported-from project
91
+ * resolves to nothing. Listing the keys is what makes that safe — a template's entry ids are
92
+ * arbitrary non-empty strings, so a hand-written template may name an entry `closest`, and a
93
+ * qualifier that happens to read `closest` must not be mistaken for a reference to it.
94
+ *
95
+ * A key absent from here is simply left alone, which is the behaviour from before relocation
96
+ * reached domains at all. Adding one is therefore safe; omitting one costs only that a template
97
+ * carrying it applies still pointing at the project it came from.
98
+ */
99
+ export const BlockScopedDomain: ReadonlySet<string> = new Set<string>([
100
+ "pl7.app/blockId",
101
+ "pl7.app/block",
102
+ "pl7.app/redefined-by",
103
+ "pl7.app/annotationRunId",
104
+ "pl7.app/clonotypeAnnotationRunId",
105
+ "pl7.app/clustering/blockId",
106
+ "pl7.app/umap/blockId",
107
+ "pl7.app/peptide/extractionRunId",
108
+ "pl7.app/repertoire/extractionRunId",
109
+ "pl7.app/antibodyVariantDesigner/designRunId",
110
+ "pl7.app/vdj/clonotypingRunId",
111
+ "pl7.app/vdj/clustering/blockId",
112
+ "pl7.app/vdj/integration/blockId",
113
+ "pl7.app/vdj/spatiotemporalAnalysis/blockId",
114
+ "pl7.app/vdj/libraryId",
115
+ ]);
116
+
86
117
  export type Domain = Metadata &
87
118
  Partial<{
88
119
  [Domain.Alphabet]: "nucleotide" | "aminoacid" | (string & {});
@@ -13,7 +13,7 @@ export type BlockCodeFeatureFlags = Record<`supports${string}`, boolean | number
13
13
  Record<`requires${string}`, boolean | number | undefined>;
14
14
 
15
15
  /**
16
- * Known block flags. Flags are set during model compilation, see `BlockModel.create` for more details and for initial values.
16
+ * Known block flags. Flags are set during model compilation, see `BlockModelV3` feature flags for more details and for initial values.
17
17
  */
18
18
  export type BlockCodeKnownFeatureFlags = {
19
19
  readonly supportsLazyState?: boolean;
package/src/index.ts CHANGED
@@ -23,3 +23,4 @@ export * from "./services";
23
23
  export * from "./pool_entry";
24
24
  export * from "./project_id";
25
25
  export * from "./columns";
26
+ export * from "./template";
package/src/plid.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { base32Encode } from "./base32_encode";
3
+ import { Branded } from "@milaboratories/helpers";
3
4
 
4
5
  /** Number of raw bytes in the PlId. */
5
6
  export const PlIdBytes = 15;
@@ -9,19 +10,18 @@ export const PlIdLength = 24; // = 15 bytes * 8 bits / 5 bits per char in base32
9
10
  export const PlId = z
10
11
  .string()
11
12
  .length(PlIdLength)
12
- .regex(/[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]/) // RFC4648
13
- .brand("PlId");
14
- export type PlId = z.infer<typeof PlId>;
13
+ .regex(/[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]/); // RFC4648
14
+ export type PlId = Branded<z.infer<typeof PlId>, "PlId">;
15
15
 
16
16
  export function uniquePlId(): PlId {
17
17
  const data = new Uint8Array(PlIdBytes);
18
18
  crypto.getRandomValues(data);
19
- return PlId.parse(base32Encode(data, "RFC4648"));
19
+ return PlId.parse(base32Encode(data, "RFC4648")) as PlId;
20
20
  }
21
21
 
22
22
  export function plId(bytes: Uint8Array): PlId {
23
23
  if (bytes.length !== PlIdBytes) throw new Error(`Wrong number of bytes: ${bytes.length}`);
24
- return PlId.parse(base32Encode(bytes, "RFC4648"));
24
+ return PlId.parse(base32Encode(bytes, "RFC4648")) as PlId;
25
25
  }
26
26
 
27
27
  export async function digestPlId(data: string): Promise<PlId> {
@@ -0,0 +1,4 @@
1
+ export * from "./kind_selector";
2
+ export * from "./project_template_v1";
3
+ export * from "./template_ref_form";
4
+ export * from "./template_relocate";
@@ -0,0 +1,126 @@
1
+ import type { Branded } from "@milaboratories/helpers";
2
+ import type { BlockKindReference } from "../bmodel/block_kind_ref";
3
+ import { parseKindRef, splitVersionedName } from "../bmodel/block_kind_ref";
4
+
5
+ /**
6
+ * Version-selection tier of a template entry's `kind` field.
7
+ *
8
+ * - `exact` — `X.Y.Z`: this version and no other.
9
+ * - `patch` — `~X.Y.Z`: patch floor, behavior frozen.
10
+ * - `minor` — `^X.Y.Z`: minor floor, behavior floats.
11
+ */
12
+ export type KindSelectorOp = "exact" | "patch" | "minor";
13
+
14
+ /** The version half of a `{name}@{selector}` kind reference, split into parts. */
15
+ export type KindSelector = {
16
+ readonly op: KindSelectorOp;
17
+ readonly version: string;
18
+ };
19
+
20
+ /**
21
+ * On-wire reference to a *set* of block kind versions: `{name}@{selector}`, e.g.
22
+ * `@platforma-open/milaboratories.mixcr-clonotyping.kind@~1.2.0`.
23
+ *
24
+ * The template-file form of a kind reference, and the only form the
25
+ * `template-v1` schema accepts in an entry's `kind` field. It is the same string
26
+ * shape as {@link BlockKindReference} widened by the `~`/`^` tiers, but branded
27
+ * separately so a *resolved* kind reference is never silently passed where a
28
+ * selector is expected, or vice versa. Widen an exact reference explicitly with
29
+ * {@link kindReferenceToSelectorReference}.
30
+ */
31
+ export type BlockKindSelectorReference = Branded<string, "BlockKindSelectorReference">;
32
+
33
+ /** `X.Y.Z` with optional semver prerelease and build metadata. */
34
+ const semVerRegex =
35
+ /^\d+\.\d+\.\d+(?:-[\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*)?(?:\+[\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*)?$/;
36
+
37
+ /**
38
+ * Split a raw selector string (`1.2.0`, `~1.2.0`, `^1.2.0`) into its parts.
39
+ *
40
+ * The version is validated as `X.Y.Z`, so a range that is legal npm but not part
41
+ * of the kind grammar (`>=1.0.0`, `1.x`, `latest`) is rejected here rather than
42
+ * reaching resolution. Note the deliberate divergence from
43
+ * `tools/block-tools`'s `parseSelector`, which additionally tolerates a leading
44
+ * `@` as `exact`: after the `{name}@{selector}` split a leading `@` can only
45
+ * come from a doubled separator, which is malformed.
46
+ *
47
+ * Mapping a selector onto a concrete version is resolution, not parsing, and
48
+ * lives with the resolver (`kind_resolver.selectorToRange`).
49
+ *
50
+ * @throws if the version part is not `X.Y.Z`
51
+ */
52
+ export function parseKindSelector(raw: string): KindSelector {
53
+ const s = raw.trim();
54
+ const op: KindSelectorOp = s.startsWith("~") ? "patch" : s.startsWith("^") ? "minor" : "exact";
55
+ const version = op === "exact" ? s : s.slice(1);
56
+ if (!semVerRegex.test(version)) {
57
+ throw new Error(
58
+ `Malformed kind version selector (expected 'X.Y.Z', '~X.Y.Z' or '^X.Y.Z'): ${raw}`,
59
+ );
60
+ }
61
+ return { op, version };
62
+ }
63
+
64
+ /** Render a {@link KindSelector} back to its on-wire string. */
65
+ export function formatKindSelector(sel: KindSelector): string {
66
+ switch (sel.op) {
67
+ case "exact":
68
+ return sel.version;
69
+ case "patch":
70
+ return `~${sel.version}`;
71
+ case "minor":
72
+ return `^${sel.version}`;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Split a {@link BlockKindSelectorReference} into `{ name, selector }`.
78
+ *
79
+ * @throws if the reference carries no version segment, or the selector is
80
+ * outside the `X.Y.Z` / `~X.Y.Z` / `^X.Y.Z` grammar
81
+ */
82
+ export function parseKindSelectorReference(ref: BlockKindSelectorReference): {
83
+ name: string;
84
+ selector: KindSelector;
85
+ } {
86
+ const { name, version } = splitVersionedName(ref, "kind selector reference", "{name}@{selector}");
87
+ return { name, selector: parseKindSelector(version) };
88
+ }
89
+
90
+ /**
91
+ * Compose a {@link BlockKindSelectorReference} from a name and selector.
92
+ *
93
+ * A formatter, not a validator — pass a selector that came from
94
+ * {@link parseKindSelector} or that you constructed from a known-good version.
95
+ */
96
+ export function formatKindSelectorReference(k: {
97
+ name: string;
98
+ selector: KindSelector;
99
+ }): BlockKindSelectorReference {
100
+ return `${k.name}@${formatKindSelector(k.selector)}` as BlockKindSelectorReference;
101
+ }
102
+
103
+ /**
104
+ * Widen a resolved {@link BlockKindReference} to its `exact`-tier selector form.
105
+ *
106
+ * The export direction: a block implements exactly one kind version, so export
107
+ * always emits `{name}@X.Y.Z`. Validates on the way through, so a
108
+ * malformed stored reference fails at the boundary rather than in the file.
109
+ */
110
+ export function kindReferenceToSelectorReference(
111
+ ref: BlockKindReference,
112
+ ): BlockKindSelectorReference {
113
+ const { name, version } = parseKindRef(ref);
114
+ return formatKindSelectorReference({ name, selector: parseKindSelector(version) });
115
+ }
116
+
117
+ /** Whether `value` is a well-formed `{name}@{selector}` string. */
118
+ export function isBlockKindSelectorReference(value: unknown): value is BlockKindSelectorReference {
119
+ if (typeof value !== "string") return false;
120
+ try {
121
+ parseKindSelectorReference(value as BlockKindSelectorReference);
122
+ return true;
123
+ } catch {
124
+ return false;
125
+ }
126
+ }
@@ -0,0 +1,315 @@
1
+ import { describe, expect, expectTypeOf, test } from "vitest";
2
+ import type { BlockKindReference } from "../bmodel/block_kind_ref";
3
+ import { formatKindRef } from "../bmodel/block_kind_ref";
4
+ import {
5
+ formatKindSelector,
6
+ formatKindSelectorReference,
7
+ isBlockKindSelectorReference,
8
+ kindReferenceToSelectorReference,
9
+ parseKindSelector,
10
+ parseKindSelectorReference,
11
+ type BlockKindSelectorReference,
12
+ } from "./kind_selector";
13
+ import {
14
+ parseBlockPackLocation,
15
+ parseBlockPackReference,
16
+ parseProjectTemplateV1,
17
+ readProjectTemplateV1,
18
+ type BlockPackLocationReference,
19
+ type BlockPackReference,
20
+ type ProjectTemplateV1,
21
+ } from "./project_template_v1";
22
+
23
+ //
24
+ // The reference example for the format, as the value a YAML reader hands back:
25
+ //
26
+ // schema: template-v1
27
+ // blocks:
28
+ // - id: samples
29
+ // kind: "@platforma-open/milaboratories.samples-and-data.kind@^1.0.0"
30
+ // params:
31
+ // dataset: bulk-rna
32
+ // - id: mixcr
33
+ // kind: "@platforma-open/milaboratories.mixcr-clonotyping.kind@~1.2.0"
34
+ // params:
35
+ // input: { __isRef: true, blockId: samples, name: reads }
36
+ // species: human
37
+ // preset: milab-human-tcr-rna
38
+ // - id: browser
39
+ // kind: "@platforma-open/milaboratories.clonotype-browser.kind@1.0.0"
40
+ // block: "@platforma-open/milaboratories.clonotype-browser@2.4.1"
41
+ // params:
42
+ // clonotypes: { __isRef: true, blockId: mixcr, name: clonotypes }
43
+ //
44
+ // The YAML text <-> value step is not this package's job (no `yaml` dependency);
45
+ // byte-level round-tripping belongs to the middle-layer serializer.
46
+ //
47
+ const referenceExample = {
48
+ schema: "template-v1",
49
+ blocks: [
50
+ {
51
+ id: "samples",
52
+ kind: "@platforma-open/milaboratories.samples-and-data.kind@^1.0.0",
53
+ params: { dataset: "bulk-rna" },
54
+ },
55
+ {
56
+ id: "mixcr",
57
+ kind: "@platforma-open/milaboratories.mixcr-clonotyping.kind@~1.2.0",
58
+ params: {
59
+ input: { __isRef: true, blockId: "samples", name: "reads" },
60
+ species: "human",
61
+ preset: "milab-human-tcr-rna",
62
+ },
63
+ },
64
+ {
65
+ id: "browser",
66
+ kind: "@platforma-open/milaboratories.clonotype-browser.kind@1.0.0",
67
+ block: "@platforma-open/milaboratories.clonotype-browser@2.4.1",
68
+ params: {
69
+ clonotypes: { __isRef: true, blockId: "mixcr", name: "clonotypes" },
70
+ },
71
+ },
72
+ ],
73
+ };
74
+
75
+ describe("kind selector grammar", () => {
76
+ test("the three tiers round-trip", () => {
77
+ expect(parseKindSelector("1.2.0")).toEqual({ op: "exact", version: "1.2.0" });
78
+ expect(parseKindSelector("~1.2.0")).toEqual({ op: "patch", version: "1.2.0" });
79
+ expect(parseKindSelector("^1.2.0")).toEqual({ op: "minor", version: "1.2.0" });
80
+
81
+ for (const raw of ["1.2.0", "~1.2.0", "^1.2.0"]) {
82
+ expect(formatKindSelector(parseKindSelector(raw))).toBe(raw);
83
+ }
84
+ });
85
+
86
+ test("prerelease and build metadata survive", () => {
87
+ expect(parseKindSelector("~1.2.0-rc.1")).toEqual({ op: "patch", version: "1.2.0-rc.1" });
88
+ expect(parseKindSelector("1.2.0+build.5")).toEqual({ op: "exact", version: "1.2.0+build.5" });
89
+ });
90
+
91
+ test("npm ranges outside the kind grammar are rejected", () => {
92
+ for (const raw of [">=1.0.0", "1.x", "1.2", "latest", "*", ""]) {
93
+ expect(() => parseKindSelector(raw)).toThrow(/Malformed kind version selector/);
94
+ }
95
+ });
96
+
97
+ test("a scoped npm name keeps its leading @", () => {
98
+ const ref = "@platforma-open/milaboratories.mixcr-clonotyping.kind@~1.2.0";
99
+ expect(parseKindSelectorReference(ref as BlockKindSelectorReference)).toEqual({
100
+ name: "@platforma-open/milaboratories.mixcr-clonotyping.kind",
101
+ selector: { op: "patch", version: "1.2.0" },
102
+ });
103
+ expect(
104
+ formatKindSelectorReference({
105
+ name: "@platforma-open/milaboratories.mixcr-clonotyping.kind",
106
+ selector: { op: "patch", version: "1.2.0" },
107
+ }),
108
+ ).toBe(ref);
109
+ });
110
+
111
+ test("a reference with no version segment is malformed", () => {
112
+ expect(() =>
113
+ parseKindSelectorReference("@platforma-open/foo.kind" as BlockKindSelectorReference),
114
+ ).toThrow(/Malformed kind selector reference/);
115
+ expect(isBlockKindSelectorReference("@platforma-open/foo.kind")).toBe(false);
116
+ expect(isBlockKindSelectorReference(42)).toBe(false);
117
+ expect(isBlockKindSelectorReference("@platforma-open/foo.kind@1.0.0")).toBe(true);
118
+ });
119
+
120
+ test("an exact kind reference widens to the exact selector tier", () => {
121
+ // The export direction: a block implements exactly one version.
122
+ const resolved: BlockKindReference = formatKindRef({
123
+ name: "@platforma-open/milaboratories.clonotype-browser.kind",
124
+ version: "1.0.0",
125
+ });
126
+ const selector = kindReferenceToSelectorReference(resolved);
127
+
128
+ expect(selector).toBe("@platforma-open/milaboratories.clonotype-browser.kind@1.0.0");
129
+ expect(parseKindSelectorReference(selector).selector.op).toBe("exact");
130
+ });
131
+ });
132
+
133
+ describe("block pack override", () => {
134
+ test("parses an exact reference", () => {
135
+ expect(
136
+ parseBlockPackReference(
137
+ "@platforma-open/milaboratories.clonotype-browser@2.4.1" as BlockPackReference,
138
+ ),
139
+ ).toEqual({
140
+ name: "@platforma-open/milaboratories.clonotype-browser",
141
+ version: "2.4.1",
142
+ });
143
+ });
144
+
145
+ test("rejects a range — an override must pin", () => {
146
+ expect(() =>
147
+ parseBlockPackReference("@platforma-open/foo@^2.4.1" as BlockPackReference),
148
+ ).toThrow(/must pin an exact version/);
149
+ });
150
+ });
151
+
152
+ describe("block pack location", () => {
153
+ const location = (raw: string) => parseBlockPackLocation(raw as BlockPackLocationReference);
154
+
155
+ test("reads the scheme, which is all this layer needs to know", () => {
156
+ // Everything past the scheme belongs to whoever can reach it: this layer cannot
157
+ // know which schemes a given environment serves, and must not decide for it.
158
+ expect(location("file:///Users/dev/blocks/enter-numbers/block")).toEqual({ scheme: "file" });
159
+ expect(location("https://blocks.internal/enter-numbers")).toEqual({ scheme: "https" });
160
+ });
161
+
162
+ test("the scheme is compared case-insensitively", () => {
163
+ expect(location("FILE:///Users/dev/blocks/x")).toEqual({ scheme: "file" });
164
+ });
165
+
166
+ test("a bare path is rejected, because it would be read against the wrong directory", () => {
167
+ // The whole point of a locator is removing the question "relative to what".
168
+ expect(() => location("/Users/dev/blocks/enter-numbers/block")).toThrow(
169
+ /absolute URI with a scheme/,
170
+ );
171
+ expect(() => location("./blocks/enter-numbers")).toThrow(/absolute URI with a scheme/);
172
+ });
173
+
174
+ test("a Windows path is rejected rather than read as a one-letter scheme", () => {
175
+ // `C:\blocks\x` satisfies the URI scheme grammar with scheme `c`, so without the
176
+ // two-character floor it would be accepted here and fail somewhere unrelated.
177
+ expect(() => location("C:\\blocks\\enter-numbers")).toThrow(/absolute URI with a scheme/);
178
+ expect(location("file:///C:/blocks/enter-numbers")).toEqual({ scheme: "file" });
179
+ });
180
+ });
181
+
182
+ describe("parseProjectTemplateV1", () => {
183
+ test("parses the reference example unchanged", () => {
184
+ const doc = parseProjectTemplateV1(referenceExample);
185
+
186
+ expect(doc).toEqual(referenceExample);
187
+ expect(doc.blocks.map((b) => b.id)).toEqual(["samples", "mixcr", "browser"]);
188
+ expect(doc.blocks[2].block).toBe("@platforma-open/milaboratories.clonotype-browser@2.4.1");
189
+ });
190
+
191
+ test("params may be omitted in the file, and reads as `{}`", () => {
192
+ // The one field the parser fills in. Every reader past it gets a mapping, so none of
193
+ // them carries a `?? {}` that one of them would eventually forget.
194
+ const doc = parseProjectTemplateV1({
195
+ schema: "template-v1",
196
+ blocks: [{ id: "samples", kind: "@platforma-open/foo.kind@^1.0.0" }],
197
+ });
198
+
199
+ expect(doc.blocks[0].params).toEqual({});
200
+ });
201
+
202
+ test("kind is required — it carries the params contract", () => {
203
+ expect(() =>
204
+ parseProjectTemplateV1({
205
+ schema: "template-v1",
206
+ blocks: [{ id: "samples", params: { dataset: "bulk-rna" } }],
207
+ }),
208
+ ).toThrow(/kind/);
209
+ });
210
+
211
+ test("an entry may pin where its implementation comes from", () => {
212
+ const doc = parseProjectTemplateV1({
213
+ schema: "template-v1",
214
+ blocks: [
215
+ {
216
+ id: "9f3c",
217
+ kind: "@milaboratories/milaboratories.test-enter-numbers.kind@^1.0.0",
218
+ location: "file:///Users/dev/blocks/enter-numbers/block",
219
+ params: { numbers: [1, 2, 3] },
220
+ },
221
+ ],
222
+ });
223
+
224
+ expect(doc.blocks[0].location).toBe("file:///Users/dev/blocks/enter-numbers/block");
225
+ // The kind stays required alongside it: the locator says where to get the
226
+ // implementation, not what contract the params are written against.
227
+ expect(doc.blocks[0].kind).toBe(
228
+ "@milaboratories/milaboratories.test-enter-numbers.kind@^1.0.0",
229
+ );
230
+ });
231
+
232
+ test("an entry cannot pin both a version and a place", () => {
233
+ // Two different statements with nothing to reconcile them, so it is refused
234
+ // rather than settled by precedence.
235
+ const result = readProjectTemplateV1({
236
+ schema: "template-v1",
237
+ blocks: [
238
+ {
239
+ id: "a",
240
+ kind: "@o/a.kind@1.0.0",
241
+ block: "@o/a@1.0.0",
242
+ location: "file:///blocks/a",
243
+ },
244
+ ],
245
+ });
246
+
247
+ expect(result.ok).toBe(false);
248
+ if (result.ok) return;
249
+ expect(result.issues[0].path).toEqual(["blocks", 0, "location"]);
250
+ expect(result.issues[0].message).toMatch(/cannot carry both 'block' and 'location'/);
251
+ });
252
+
253
+ test("a malformed location is reported on its own path", () => {
254
+ const result = readProjectTemplateV1({
255
+ schema: "template-v1",
256
+ blocks: [{ id: "a", kind: "@o/a.kind@1.0.0", location: "/blocks/a" }],
257
+ });
258
+
259
+ expect(result.ok).toBe(false);
260
+ if (result.ok) return;
261
+ expect(result.issues[0].path).toEqual(["blocks", 0, "location"]);
262
+ // The grammar's own message, reported as it comes rather than restated.
263
+ expect(result.issues[0].message).toMatch(/absolute URI with a scheme/);
264
+ });
265
+
266
+ test("the format marker is checked", () => {
267
+ expect(() => parseProjectTemplateV1({ schema: "template-v2", blocks: [] })).toThrow();
268
+ expect(() => parseProjectTemplateV1({ blocks: [] })).toThrow();
269
+ });
270
+
271
+ test("unknown fields are rejected at both levels", () => {
272
+ // No `label` field: a template does not name block instances for display.
273
+ expect(() =>
274
+ parseProjectTemplateV1({
275
+ schema: "template-v1",
276
+ blocks: [{ id: "a", kind: "@o/a.kind@1.0.0", label: "Samples" }],
277
+ }),
278
+ ).toThrow(/label/);
279
+
280
+ expect(() =>
281
+ parseProjectTemplateV1({ schema: "template-v1", blocks: [], metadata: {} }),
282
+ ).toThrow(/metadata/);
283
+ });
284
+
285
+ test("a malformed kind selector is reported on its own path", () => {
286
+ const result = readProjectTemplateV1({
287
+ schema: "template-v1",
288
+ blocks: [{ id: "a", kind: "@o/a.kind@>=1.0.0" }],
289
+ });
290
+
291
+ expect(result.ok).toBe(false);
292
+ if (!result.ok) {
293
+ expect(result.issues[0].path).toEqual(["blocks", 0, "kind"]);
294
+ expect(result.issues[0].message).toMatch(/Malformed kind version selector/);
295
+ }
296
+ });
297
+
298
+ test("duplicate template-local ids are rejected", () => {
299
+ expect(() =>
300
+ parseProjectTemplateV1({
301
+ schema: "template-v1",
302
+ blocks: [
303
+ { id: "a", kind: "@o/a.kind@1.0.0" },
304
+ { id: "a", kind: "@o/b.kind@1.0.0" },
305
+ ],
306
+ }),
307
+ ).toThrow(/Duplicate template-local id: a/);
308
+ });
309
+ });
310
+
311
+ test("the parser's output type is exactly ProjectTemplateV1", () => {
312
+ // NOTE: vitest runs without `--typecheck` and tsconfig excludes *.test.ts, so this
313
+ // assertion is authoring-time only; it is verified by running tsc over this file.
314
+ expectTypeOf<ReturnType<typeof parseProjectTemplateV1>>().toEqualTypeOf<ProjectTemplateV1>();
315
+ });